PostgreSQL Full-Text Search in Laravel | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    PostgreSQL Full-Text Search in Laravel: Indexes, Ranking, and Multilingual Queries        On this page       1. [  Why PostgreSQL Full-Text Search Is Underused in Laravel ](#why-postgresql-full-text-search-is-underused-in-laravel)
2. [  Setting Up the tsvector Column ](#setting-up-the-tsvector-column)
3. [  Querying from Eloquent ](#querying-from-eloquent)
4. [  Phrase Queries and Prefix Matching ](#phrase-queries-and-prefix-matching)
5. [  Multilingual Configuration ](#multilingual-configuration)
6. [  Highlighting Snippets ](#highlighting-snippets)
7. [  Key Takeaways ](#key-takeaways)

  ![PostgreSQL Full-Text Search in Laravel: Indexes, Ranking, and Multilingual Queries](https://cdn.msaied.com/264/cbbcc9ce84c70c6a9e5dd361c48f440d.png)

  #laravel   #postgresql   #full-text-search   #performance  

 PostgreSQL Full-Text Search in Laravel: Indexes, Ranking, and Multilingual Queries 
====================================================================================

     22 Jun 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   Why PostgreSQL Full-Text Search Is Underused in Laravel  ](#why-postgresql-full-text-search-is-underused-in-laravel)
2. [  02   Setting Up the tsvector Column  ](#setting-up-the-tsvector-column)
3. [  03   Querying from Eloquent  ](#querying-from-eloquent)
4. [  04   Phrase Queries and Prefix Matching  ](#phrase-queries-and-prefix-matching)
5. [  05   Multilingual Configuration  ](#multilingual-configuration)
6. [  06   Highlighting Snippets  ](#highlighting-snippets)
7. [  07   Key Takeaways  ](#key-takeaways)

 Why PostgreSQL Full-Text Search Is Underused in Laravel
-------------------------------------------------------

Most Laravel projects reach for Algolia or Meilisearch the moment a client says "search". Both are excellent, but they add operational cost, sync complexity, and eventual-consistency headaches. PostgreSQL's built-in full-text search handles millions of rows with sub-10ms queries when set up correctly. This article shows you the exact migration, model wiring, and query patterns to make it production-ready.

---

Setting Up the tsvector Column
------------------------------

Store a pre-computed search vector alongside your data. A generated column keeps it in sync automatically — no triggers, no observers.

```php
// database/migrations/2024_06_01_000000_add_search_vector_to_articles.php
public function up(): void
{
    DB::statement("
        ALTER TABLE articles
        ADD COLUMN search_vector tsvector
        GENERATED ALWAYS AS (
            setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
            setweight(to_tsvector('english', coalesce(body, '')), 'B')
        ) STORED
    ");

    DB::statement(
        'CREATE INDEX articles_search_vector_gin ON articles USING GIN (search_vector)'
    );
}

```

The `GENERATED ALWAYS AS … STORED` syntax (PostgreSQL 12+) means the column is recomputed on every `INSERT` or `UPDATE` with no application-level code. `setweight` assigns priority: title matches outrank body matches during ranking.

---

Querying from Eloquent
----------------------

Wrap the raw SQL in a clean local scope so callers never see the plumbing.

```php
// app/Models/Article.php
use Illuminate\Database\Eloquent\Builder;

public function scopeSearch(Builder $query, string $term): Builder
{
    $tsQuery = 'plainto_tsquery(\'english\', ?)';

    return $query
        ->whereRaw("search_vector @@ {$tsQuery}", [$term])
        ->selectRaw(
            "*, ts_rank(search_vector, {$tsQuery}) AS rank",
            [$term]
        )
        ->orderByDesc('rank');
}

```

Usage is clean:

```php
$results = Article::search('event sourcing laravel')
    ->with('author')
    ->paginate(20);

```

`plainto_tsquery` tokenises a plain string safely — no need to sanitise operators. Use `phraseto_tsquery` when word order matters (e.g. "event sourcing" as a phrase).

---

Phrase Queries and Prefix Matching
----------------------------------

```php
// Exact phrase
DB::raw("search_vector @@ phraseto_tsquery('english', ?)")

// Prefix (autocomplete-style) — note the :* operator
DB::raw("search_vector @@ to_tsquery('english', ? || ':*')")

```

Prefix matching is useful for live search inputs. Combine it with a `LIMIT 10` and a covering index on `(search_vector, id, title)` to avoid a heap fetch.

---

Multilingual Configuration
--------------------------

PostgreSQL ships with text-search configurations for dozens of languages. Store the user's locale and pass the matching configuration name:

```php
public function scopeSearch(Builder $query, string $term, string $lang = 'english'): Builder
{
    // Allowlist to prevent SQL injection via the config name
    $allowed = ['english', 'french', 'german', 'spanish', 'portuguese'];
    $config  = in_array($lang, $allowed, true) ? $lang : 'english';

    return $query
        ->whereRaw(
            "search_vector @@ plainto_tsquery('{$config}', ?)",
            [$term]
        );
}

```

For truly multilingual content in a single table, store multiple vectors — one per language — and query the appropriate column based on the request locale.

---

Highlighting Snippets
---------------------

Return highlighted excerpts without a second round-trip:

```php
->selectRaw(
    "ts_headline(
        'english',
        body,
        plainto_tsquery('english', ?),
        'MaxWords=35, MinWords=15, StartSel=, StopSel='
    ) AS excerpt",
    [$term]
)

```

Bind the result to a virtual attribute on the model:

```php
protected $appends = ['excerpt'];

public function getExcerptAttribute(): ?string
{
    return $this->attributes['excerpt'] ?? null;
}

```

---

Key Takeaways
-------------

- **Generated stored columns** keep `tsvector` in sync with zero application code.
- **GIN indexes** make `@@` queries fast even on millions of rows.
- **`ts_rank`** gives relevance ordering; `setweight` lets you tune title vs. body priority.
- **`plainto_tsquery`** is safe for user input; `to_tsquery` with `:*` enables prefix search.
- **`ts_headline`** returns highlighted snippets in the same query, avoiding extra round-trips.
- Allowlist language config names before interpolating them into SQL.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fpostgresql-full-text-search-in-laravel-indexes-ranking-and-multilingual-queries&text=PostgreSQL+Full-Text+Search+in+Laravel%3A+Indexes%2C+Ranking%2C+and+Multilingual+Queries) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fpostgresql-full-text-search-in-laravel-indexes-ranking-and-multilingual-queries) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Does a generated tsvector column work with Laravel's Eloquent update methods?        Yes. Because the column is `GENERATED ALWAYS AS … STORED`, PostgreSQL recomputes it automatically on every INSERT or UPDATE regardless of how the write originates — Eloquent, raw queries, or migrations. You cannot manually set the column value; PostgreSQL will reject it. 

      Q02  When should I still choose Algolia or Meilisearch over PostgreSQL FTS?        Reach for a dedicated search engine when you need faceted filtering with real-time index updates across distributed replicas, typo-tolerance out of the box, or when your search index must span multiple databases or microservices. For a single PostgreSQL database with straightforward keyword and phrase search, the built-in FTS is usually sufficient and simpler to operate. 

      Q03  How do I handle accented characters and case folding in multilingual search?        Install the `unaccent` PostgreSQL extension and add it to your text-search configuration: `ALTER TEXT SEARCH CONFIGURATION english ALTER MAPPING FOR hword, hword_part, word WITH unaccent, english_stem;`. This normalises accented characters at index and query time so 'café' matches 'cafe'. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Showcase Your PhpStorm Expertise on LinkedIn with JetBrains' New Plugin](https://cdn.msaied.com/267/a94d1b197b4892a531075bc5ecda0ac2.png) PhpStorm JetBrains LinkedIn 

### Showcase Your PhpStorm Expertise on LinkedIn with JetBrains' New Plugin

JetBrains has launched a free LinkedIn Connected Apps plugin for PhpStorm and other JetBrains IDEs. It tracks...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 22 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/showcase-your-phpstorm-expertise-on-linkedin-with-jetbrains-new-plugin) [ ![Laravel Enums as First-Class Domain Citizens: Typed Casts, Backed Values, and Behaviour](https://cdn.msaied.com/266/81a05f630d54004d2a6689a02d6f0579.png) laravel php enums 

### Laravel Enums as First-Class Domain Citizens: Typed Casts, Backed Values, and Behaviour

PHP 8.1 backed enums are more than constants. Learn how to attach behaviour, use them as Eloquent casts, embed...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 22 Jun 2026     1 min read  

  Read    

 ](https://msaied.com/articles/laravel-enums-as-first-class-domain-citizens-typed-casts-backed-values-and-behaviour) [ ![MySQL EXPLAIN and Index Optimization for Laravel Developers](https://cdn.msaied.com/265/e97881aef9580e1f0b9e1bd6890d828a.png) laravel mysql performance 

### MySQL EXPLAIN and Index Optimization for Laravel Developers

Stop guessing why your Laravel queries are slow. Learn to read EXPLAIN output, spot full table scans, and desi...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 22 Jun 2026     4 min read  

  Read    

 ](https://msaied.com/articles/mysql-explain-and-index-optimization-for-laravel-developers) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
