RAG in Laravel with pgvector and Embeddings | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Practical RAG in Laravel: pgvector, Embeddings, and Retrieval-Augmented Generation

 Practical RAG in Laravel: pgvector, Embeddings, and Retrieval-Augmented Generation
===================================================================================

 Build a production-ready RAG pipeline in Laravel using pgvector, OpenAI embeddings, and a clean retrieval layer — without reaching for a dedicated vector database.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 19 Sep 2026 · Updated 19 Sep 2026 · 4 min read

ShareCopy linkCopied

 ![Practical RAG in Laravel: pgvector, Embeddings, and Retrieval-Augmented Generation](https://cdn.msaied.com/681/08058424f0e8433b83d9008c6b701cd8.png) 

  On this page +1. [Why RAG Instead of Fine-Tuning?](#why-rag-instead-of-fine-tuning)
2. [Enabling pgvector in a Laravel Migration](#enabling-pgvector-in-a-laravel-migration)
3. [Generating and Storing Embeddings](#generating-and-storing-embeddings)
4. [Retrieval: Nearest-Neighbour Search](#retrieval-nearest-neighbour-search)
5. [Assembling the Prompt](#assembling-the-prompt)
6. [Chunking Strategy Matters](#chunking-strategy-matters)
7. [Production Considerations](#production-considerations)
8. [Takeaways](#takeaways)

 Why RAG Instead of Fine-Tuning?
-------------------------------

Fine-tuning is expensive and goes stale. Retrieval-augmented generation (RAG) keeps your knowledge base in a database you already control, lets you update it without retraining, and gives you full auditability over what context the model sees. If you are already running PostgreSQL, the `pgvector` extension turns it into a capable vector store — no extra infrastructure required.

Enabling pgvector in a Laravel Migration
----------------------------------------

```php
// database/migrations/2024_01_01_000000_create_document_chunks_table.php
public function up(): void
{
    DB::statement('CREATE EXTENSION IF NOT EXISTS vector');

    Schema::create('document_chunks', function (Blueprint $table) {
        $table->id();
        $table->foreignId('document_id')->constrained()->cascadeOnDelete();
        $table->text('content');
        $table->integer('token_count');
        // 1536 dimensions for text-embedding-3-small
        $table->string('embedding', 8192)->nullable(); // stored as text, cast in queries
        $table->timestamps();
    });

    DB::statement(
        'ALTER TABLE document_chunks ADD COLUMN embedding_vec vector(1536)'
    );

    // IVFFlat index — tune lists to ~sqrt(row_count)
    DB::statement(
        'CREATE INDEX ON document_chunks USING ivfflat (embedding_vec vector_cosine_ops) WITH (lists = 100)'
    );
}

```

Using a raw `ALTER TABLE` keeps the migration readable while sidestepping Blueprint's lack of native vector type support.

Generating and Storing Embeddings
---------------------------------

Wrap the OpenAI call in a dedicated action so it is easy to swap providers later.

```php
namespace App\AI;

use App\Models\DocumentChunk;
use OpenAI\Laravel\Facades\OpenAI;

final class EmbedChunk
{
    public function handle(DocumentChunk $chunk): void
    {
        $response = OpenAI::embeddings()->create([
            'model' => 'text-embedding-3-small',
            'input' => $chunk->content,
        ]);

        $vector = $response->embeddings[0]->embedding; // float[]

        // pgvector expects '[0.1,0.2,...]' literal syntax
        $literal = '[' . implode(',', $vector) . ']';

        $chunk->updateQuietly(['embedding_vec' => DB::raw("'$literal'::vector")]);
    }
}

```

Dispatch this inside a queued job after chunking a document so the HTTP cycle stays fast.

Retrieval: Nearest-Neighbour Search
-----------------------------------

```php
namespace App\AI;

use App\Models\DocumentChunk;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

final class RetrieveChunks
{
    public function __construct(private EmbedQuery $embedQuery) {}

    /** @return Collection */
    public function topK(string $query, int $k = 5): Collection
    {
        $vector = $this->embedQuery->forText($query);
        $literal = '[' . implode(',', $vector) . ']';

        return DocumentChunk::query()
            ->select(['id', 'document_id', 'content', 'token_count'])
            ->selectRaw(
                'embedding_vec  ?::vector AS distance',
                [$literal]
            )
            ->orderBy('distance')
            ->limit($k)
            ->get();
    }
}

```

The `` operator is cosine distance. Use `` for inner product or `` for L2 depending on how your embeddings were normalised.

Assembling the Prompt
---------------------

```php
final class RagPipeline
{
    public function __construct(
        private RetrieveChunks $retriever,
    ) {}

    public function answer(string $question): string
    {
        $chunks = $this->retriever->topK($question, k: 5);

        $context = $chunks
            ->map(fn ($c) => "---\n" . $c->content)
            ->implode("\n");

        $response = OpenAI::chat()->create([
            'model' => 'gpt-4o-mini',
            'messages' => [
                ['role' => 'system', 'content' =>
                    'Answer using only the context below. If unsure, say so.'],
                ['role' => 'user', 'content' =>
                    "Context:\n$context\n\nQuestion: $question"],
            ],
            'max_tokens' => 512,
        ]);

        return $response->choices[0]->message->content;
    }
}

```

Chunking Strategy Matters
-------------------------

A naive split on paragraph breaks produces uneven chunks. Prefer a sliding-window approach: 512-token chunks with a 64-token overlap so context is not lost at boundaries. Count tokens with `tiktoken-php` before persisting.

```php
$chunks = collect($splitter->split($text, chunkSize: 512, overlap: 64));

```

Production Considerations
-------------------------

- **Index warm-up**: IVFFlat requires `SET ivfflat.probes = 10` at query time for better recall at the cost of speed. Tune per workload.
- **Embedding cache**: Hash the input text and cache the vector in Redis to avoid redundant API calls during re-indexing.
- **Tenant isolation**: Add a `tenant_id` column and include it in the `WHERE` clause before the vector search; pgvector will filter first if the planner cooperates.
- **Batch embedding**: Use the embeddings endpoint's array input to embed up to 2048 strings per request and cut API round-trips dramatically.

Takeaways
---------

- pgvector + PostgreSQL is a viable production vector store for most Laravel apps — no separate service needed.
- Keep embedding generation in queued jobs; keep retrieval in a dedicated class injectable via the service container.
- The `` cosine operator, IVFFlat index, and `probes` setting are the three knobs that control recall vs. latency.
- Chunking quality directly determines answer quality — invest time in your splitter before tuning the model.
- Cache embeddings aggressively; the embedding API is the main cost driver in a RAG pipeline.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [ai](https://www.msaied.com/public/articles?search=ai)
- [pgvector](https://www.msaied.com/public/articles?search=pgvector)
- [postgresql](https://www.msaied.com/public/articles?search=postgresql)
- [rag](https://www.msaied.com/public/articles?search=rag)

 Frequently asked questions 
---------------------------

  Do I need a dedicated vector database like Pinecone or Weaviate for RAG in Laravel?Not for most applications. If you are already running PostgreSQL, the pgvector extension provides cosine, L2, and inner-product similarity search with IVFFlat or HNSW indexes. A dedicated vector database makes sense only when you have hundreds of millions of vectors or need features like multi-tenancy at the vector-store level.

   How do I keep embeddings up to date when document content changes?Listen for the Eloquent `updated` event on your Document model, delete the old chunks, re-chunk the new content, and dispatch fresh embedding jobs. Wrapping this in a database transaction ensures you never serve stale vectors mid-update.

   What is the difference between IVFFlat and HNSW indexes in pgvector?IVFFlat partitions vectors into lists and searches a subset of them; it is faster to build but requires a `probes` setting to balance recall vs. speed. HNSW builds a hierarchical graph and generally delivers better recall at query time with no probe tuning, but takes longer to build and uses more memory. For most Laravel apps starting out, IVFFlat with a sensible `lists` value is the pragmatic choice.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

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

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleGate Responses, Policy Before-Hooks, and Ownership Guards in Laravel](https://www.msaied.com/public/articles/gate-responses-policy-before-hooks-and-ownership-guards-in-laravel) [Next articleMySQL EXPLAIN and Index Tuning for Laravel: Reading Query Plans in Production](https://www.msaied.com/public/articles/mysql-explain-and-index-tuning-for-laravel-reading-query-plans-in-production)  

   On this page
-------------

1. [Why RAG Instead of Fine-Tuning?](#why-rag-instead-of-fine-tuning)
2. [Enabling pgvector in a Laravel Migration](#enabling-pgvector-in-a-laravel-migration)
3. [Generating and Storing Embeddings](#generating-and-storing-embeddings)
4. [Retrieval: Nearest-Neighbour Search](#retrieval-nearest-neighbour-search)
5. [Assembling the Prompt](#assembling-the-prompt)
6. [Chunking Strategy Matters](#chunking-strategy-matters)
7. [Production Considerations](#production-considerations)
8. [Takeaways](#takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
