RAG in Laravel with pgvector and Embeddings | 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)    Practical RAG in Laravel: pgvector, Embeddings, and Retrieval Pipelines        On this page       1. [  Why RAG Instead of Fine-Tuning? ](#why-rag-instead-of-fine-tuning)
2. [  Setting Up pgvector ](#setting-up-pgvector)
3. [  Generating and Storing Embeddings ](#generating-and-storing-embeddings)
4. [  Retrieval: Nearest-Neighbour Query ](#retrieval-nearest-neighbour-query)
5. [  Building the Prompt ](#building-the-prompt)
6. [  Caching Embeddings ](#caching-embeddings)
7. [  Key Takeaways ](#key-takeaways)

  ![Practical RAG in Laravel: pgvector, Embeddings, and Retrieval Pipelines](https://cdn.msaied.com/487/2db17c4f7927d4a229a4786c03e7b67b.png)

  #laravel   #ai   #pgvector   #postgresql   #rag  

 Practical RAG in Laravel: pgvector, Embeddings, and Retrieval Pipelines 
=========================================================================

     30 Jul 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   Why RAG Instead of Fine-Tuning?  ](#why-rag-instead-of-fine-tuning)
2. [  02   Setting Up pgvector  ](#setting-up-pgvector)
3. [  03   Generating and Storing Embeddings  ](#generating-and-storing-embeddings)
4. [  04   Retrieval: Nearest-Neighbour Query  ](#retrieval-nearest-neighbour-query)
5. [  05   Building the Prompt  ](#building-the-prompt)
6. [  06   Caching Embeddings  ](#caching-embeddings)
7. [  07   Key Takeaways  ](#key-takeaways)

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

Retrieval-augmented generation (RAG) lets you ground an LLM's answers in your own data without the cost and complexity of fine-tuning. The pattern is simple: embed your documents, store the vectors, retrieve the top-k nearest neighbours at query time, and inject them as context. Laravel's ecosystem — PostgreSQL, Eloquent, and the HTTP client — is more than enough to build this cleanly.

Setting Up pgvector
-------------------

Install the extension and add a migration:

```sql
CREATE EXTENSION IF NOT EXISTS vector;

```

```php
// database/migrations/2024_01_01_000000_add_embedding_to_documents.php
public function up(): void
{
    Schema::table('documents', function (Blueprint $table) {
        // 1536 dims for text-embedding-3-small
        $table->vector('embedding', 1536)->nullable();
    });

    DB::statement(
        'CREATE INDEX documents_embedding_hnsw_idx
         ON documents USING hnsw (embedding vector_cosine_ops)'
    );
}

```

Laravel's `Blueprint` doesn't know `vector` natively, so register a macro in a service provider:

```php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Blueprint::macro('vector', function (string $column, int $dimensions): \Illuminate\Database\Schema\ColumnDefinition {
    return $this->addColumn('vector', $column, compact('dimensions'));
});

// Register the type with Doctrine so migrations don't break
\Doctrine\DBAL\Types\Type::addType('vector', VectorType::class);

```

`VectorType` is a thin Doctrine type that serialises a PHP float array to the `[0.1,0.2,...]` string pgvector expects.

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

Wrap the OpenAI call in a dedicated action:

```php
final readonly class GenerateEmbedding
{
    public function __construct(private \Illuminate\Http\Client\Factory $http) {}

    /** @return float[] */
    public function handle(string $text): array
    {
        $response = $this->http
            ->withToken(config('services.openai.key'))
            ->post('https://api.openai.com/v1/embeddings', [
                'model' => 'text-embedding-3-small',
                'input' => $text,
            ])
            ->throw()
            ->json('data.0.embedding');

        return $response;
    }
}

```

Dispatch a job when a document is saved:

```php
final class EmbedDocument implements ShouldQueue
{
    use Dispatchable, Queueable;

    public function __construct(public readonly int $documentId) {}

    public function handle(GenerateEmbedding $action): void
    {
        $doc = Document::findOrFail($this->documentId);
        $vector = $action->handle($doc->body);

        // Store as pgvector literal
        DB::table('documents')
            ->where('id', $doc->id)
            ->update(['embedding' => '[' . implode(',', $vector) . ']']);
    }
}

```

Retrieval: Nearest-Neighbour Query
----------------------------------

A clean retrieval abstraction keeps the pgvector SQL out of your controllers:

```php
final readonly class DocumentRetriever
{
    public function __construct(
        private GenerateEmbedding $embedder,
        private int $topK = 5,
    ) {}

    /** @return \Illuminate\Support\Collection */
    public function retrieve(string $query): \Illuminate\Support\Collection
    {
        $vector = '[' . implode(',', $this->embedder->handle($query)) . ']';

        return Document::query()
            ->selectRaw('*, embedding  ? AS distance', [$vector])
            ->whereNotNull('embedding')
            ->orderBy('distance')
            ->limit($this->topK)
            ->get();
    }
}

```

The `` operator is pgvector's cosine distance. For inner-product similarity use ``.

Building the Prompt
-------------------

```php
final readonly class RagPipeline
{
    public function __construct(
        private DocumentRetriever $retriever,
        private \Illuminate\Http\Client\Factory $http,
    ) {}

    public function answer(string $question): string
    {
        $context = $this->retriever->retrieve($question)
            ->map(fn (Document $d) => "- {$d->title}: {$d->body}")
            ->implode("\n");

        return $this->http
            ->withToken(config('services.openai.key'))
            ->post('https://api.openai.com/v1/chat/completions', [
                'model' => 'gpt-4o-mini',
                'messages' => [
                    ['role' => 'system', 'content' => "Answer using only the context below.\n\n{$context}"],
                    ['role' => 'user',   'content' => $question],
                ],
            ])
            ->throw()
            ->json('choices.0.message.content');
    }
}

```

### Caching Embeddings

Embedding the same query repeatedly wastes tokens. Cache by hash:

```php
$cacheKey = 'embedding:' . hash('xxh128', $text);
$vector = Cache::remember($cacheKey, now()->addDay(), fn () => $action->handle($text));

```

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

- Register a `Blueprint::macro` for `vector` columns; pair it with a Doctrine type for migration compatibility.
- Use HNSW indexes (`vector_cosine_ops`) for sub-millisecond ANN at scale — IVFFlat requires a `VACUUM` before it becomes useful.
- Keep embedding generation in a queued job; retrieval at request time is fast enough for synchronous use.
- Wrap retrieval behind a `DocumentRetriever` class so you can swap pgvector for another store without touching controllers.
- Cache query embeddings by content hash to avoid redundant API calls on repeated questions.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fpractical-rag-in-laravel-pgvector-embeddings-and-retrieval-pipelines-2&text=Practical+RAG+in+Laravel%3A+pgvector%2C+Embeddings%2C+and+Retrieval+Pipelines) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fpractical-rag-in-laravel-pgvector-embeddings-and-retrieval-pipelines-2) 

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

  3 questions  

     Q01  Do I need a dedicated vector database, or is pgvector enough?        pgvector with an HNSW index handles millions of vectors comfortably for most SaaS workloads. A dedicated store like Qdrant or Pinecone adds operational overhead that is rarely justified until you exceed tens of millions of vectors or need advanced filtering that pgvector cannot express efficiently. 

      Q02  How do I handle documents longer than the embedding model's token limit?        Chunk the document before embedding — typically 512–1024 tokens with a small overlap (e.g. 64 tokens) to preserve context across chunk boundaries. Store each chunk as a separate row with a foreign key back to the parent document, then deduplicate retrieved chunks by document at query time. 

      Q03  Can I test the retrieval pipeline without hitting the OpenAI API?        Yes. Bind a fake implementation of GenerateEmbedding in your test service provider that returns a deterministic float array. Insert pre-computed embeddings into the test database and assert that DocumentRetriever returns the expected rows — no HTTP calls required. 

  Continue reading

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

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

 [ ![Laravel Octane + FrankenPHP: Persistent Services, Boot-Once Singletons, and Safe State](https://cdn.msaied.com/486/7a50572cb8b282ae36063a9213f55ed3.png) laravel octane frankenphp 

### Laravel Octane + FrankenPHP: Persistent Services, Boot-Once Singletons, and Safe State

Running Laravel under FrankenPHP with Octane means your service container lives across requests. Learn which s...

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

 30 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-octane-frankenphp-persistent-services-boot-once-singletons-and-safe-state) [ ![Everything Announced at Laracon US 2026: Laravel Framework, Cloud & AI Updates](https://cdn.msaied.com/485/8c49e044ff45a776167065579e1c93ac.png) Laracon 2026 Laravel Cloud Laravel AI SDK 

### Everything Announced at Laracon US 2026: Laravel Framework, Cloud &amp; AI Updates

Laracon US 2026 brought major updates across the Laravel ecosystem: scale-to-zero Flex compute, managed queues...

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

 29 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/everything-announced-at-laracon-us-2026-laravel-framework-cloud-ai-updates) [ ![Laravel Queues at Scale: Backpressure, Dead-Letter Patterns, and Job Observability](https://cdn.msaied.com/484/7ee0710bfc61c6ff7667dd4f60a8c5bd.png) laravel queues observability 

### Laravel Queues at Scale: Backpressure, Dead-Letter Patterns, and Job Observability

Beyond basic queue workers: learn how to implement backpressure signals, dead-letter queues, and structured jo...

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

 29 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-queues-at-scale-backpressure-dead-letter-patterns-and-job-observability) 

   [  ![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)
