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 in Laravel Without a Dedicated Vector DB? ](#why-rag-in-laravel-without-a-dedicated-vector-db)
2. [  1. Enable pgvector and Add the Column ](#1-enable-pgvector-and-add-the-column)
3. [  2. The Embedding Service ](#2-the-embedding-service)
4. [  3. Ingestion Pipeline ](#3-ingestion-pipeline)
5. [  4. Retrieval: Cosine Similarity with pgvector ](#4-retrieval-cosine-similarity-with-pgvector)
6. [  5. Prompt Assembly and Generation ](#5-prompt-assembly-and-generation)
7. [  Key Takeaways ](#key-takeaways)

  ![Practical RAG in Laravel: pgvector, Embeddings, and Retrieval Pipelines](https://cdn.msaied.com/411/8d5b164ce43b1c6b9b658b7f72a0c369.png)

  #laravel   #ai   #pgvector   #postgresql  

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

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

       Table of contents

1. [  01   Why RAG in Laravel Without a Dedicated Vector DB?  ](#why-rag-in-laravel-without-a-dedicated-vector-db)
2. [  02   1. Enable pgvector and Add the Column  ](#1-enable-pgvector-and-add-the-column)
3. [  03   2. The Embedding Service  ](#2-the-embedding-service)
4. [  04   3. Ingestion Pipeline  ](#3-ingestion-pipeline)
5. [  05   4. Retrieval: Cosine Similarity with pgvector  ](#4-retrieval-cosine-similarity-with-pgvector)
6. [  06   5. Prompt Assembly and Generation  ](#5-prompt-assembly-and-generation)
7. [  07   Key Takeaways  ](#key-takeaways)

 Why RAG in Laravel Without a Dedicated Vector DB?
-------------------------------------------------

Pinecone, Weaviate, and Qdrant are compelling, but they add operational overhead. If you are already running PostgreSQL, the `pgvector` extension gives you cosine-similarity search with a single `CREATE EXTENSION` and a new column type. For most SaaS workloads — knowledge bases, document Q&amp;A, semantic search — that is more than enough.

This article walks through a complete, opinionated RAG pipeline: ingestion, embedding storage, retrieval, and prompt assembly.

---

1. Enable pgvector and Add the Column
-------------------------------------

```sql
CREATE EXTENSION IF NOT EXISTS vector;

```

```php
// database/migrations/2024_06_01_create_document_chunks_table.php
Schema::create('document_chunks', function (Blueprint $table) {
    $table->id();
    $table->foreignId('document_id')->constrained()->cascadeOnDelete();
    $table->text('content');
    $table->vector('embedding', 1536); // OpenAI text-embedding-3-small
    $table->timestamps();
});

```

Laravel's `Blueprint` does not ship a `vector()` macro out of the box, so register one in a service provider:

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

Blueprint::macro('vector', function (string $column, int $dimensions = 1536) {
    /** @var Blueprint $this */
    return $this->addColumn('vector', $column, compact('dimensions'));
});

// In AppServiceProvider::boot()
\Illuminate\Database\Schema\Grammars\PostgresGrammar::macro(
    'typeVector',
    fn ($column) => "vector({$column->dimensions})"
);

```

---

2. The Embedding Service
------------------------

Keep the OpenAI HTTP call behind a thin interface so you can swap providers or stub in tests.

```php
interface EmbeddingProvider
{
    /** @return float[] */
    public function embed(string $text): array;
}

final class OpenAiEmbeddingProvider implements EmbeddingProvider
{
    public function __construct(
        private readonly \OpenAI\Client $client,
        private readonly string $model = 'text-embedding-3-small',
    ) {}

    public function embed(string $text): array
    {
        $response = $this->client->embeddings()->create([
            'model' => $this->model,
            'input' => $text,
        ]);

        return $response->embeddings[0]->embedding;
    }
}

```

Bind it in the container:

```php
$this->app->singleton(EmbeddingProvider::class, fn () =>
    new OpenAiEmbeddingProvider(app(\OpenAI\Client::class))
);

```

---

3. Ingestion Pipeline
---------------------

Chunk documents before embedding — 512-token chunks with a 64-token overlap is a solid default.

```php
final class IngestDocumentAction
{
    public function __construct(
        private readonly EmbeddingProvider $embedder,
    ) {}

    public function handle(Document $document): void
    {
        $chunks = $this->chunk($document->body, maxTokens: 512, overlap: 64);

        foreach ($chunks as $content) {
            $vector = $this->embedder->embed($content);

            DB::table('document_chunks')->insert([
                'document_id' => $document->id,
                'content'     => $content,
                'embedding'   => '[' . implode(',', $vector) . ']',
                'created_at'  => now(),
                'updated_at'  => now(),
            ]);
        }
    }

    /** @return string[] */
    private function chunk(string $text, int $maxTokens, int $overlap): array
    {
        // Naive word-boundary chunking; replace with tiktoken FFI for precision.
        $words  = explode(' ', $text);
        $chunks = [];
        $step   = max(1, $maxTokens - $overlap);

        for ($i = 0; $i < count($words); $i += $step) {
            $chunks[] = implode(' ', array_slice($words, $i, $maxTokens));
        }

        return array_filter($chunks);
    }
}

```

---

4. Retrieval: Cosine Similarity with pgvector
---------------------------------------------

```php
final class RetrievalService
{
    public function __construct(
        private readonly EmbeddingProvider $embedder,
    ) {}

    /** @return array */
    public function retrieve(string $query, int $topK = 5): array
    {
        $vector = '[' . implode(',', $this->embedder->embed($query)) . ']';

        return DB::select(
            "SELECT content,
                    1 - (embedding  ?::vector) AS score
             FROM document_chunks
             ORDER BY embedding  ?::vector
             LIMIT ?",
            [$vector, $vector, $topK]
        );
    }
}

```

The `` operator is pgvector's cosine distance. Subtract from 1 to get similarity. Add an `ivfflat` index once your chunk count exceeds ~100k rows:

```sql
CREATE INDEX ON document_chunks
    USING ivfflat (embedding vector_cosine_ops)
    WITH (lists = 100);

```

---

5. Prompt Assembly and Generation
---------------------------------

```php
final class AnswerQuestionAction
{
    public function __construct(
        private readonly RetrievalService $retrieval,
        private readonly \OpenAI\Client $client,
    ) {}

    public function handle(string $question): string
    {
        $chunks  = $this->retrieval->retrieve($question, topK: 4);
        $context = collect($chunks)
            ->pluck('content')
            ->implode("\n\n---\n\n");

        $response = $this->client->chat()->create([
            'model'    => 'gpt-4o-mini',
            'messages' => [
                ['role' => 'system', 'content' =>
                    "Answer using only the context below.\n\n{$context}"],
                ['role' => 'user', 'content' => $question],
            ],
        ]);

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

```

---

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

- **pgvector is production-ready** for most SaaS RAG workloads; skip the dedicated vector DB until you hit scale.
- **Chunk with overlap** to avoid splitting context across boundaries; tune chunk size per domain.
- **Abstract the embedding provider** behind an interface — it makes testing trivial and provider swaps painless.
- **Use `ivfflat` indexes** once chunk counts grow; `lists = sqrt(row_count)` is a reasonable starting heuristic.
- **Keep retrieval and generation separate actions** — retrieval is deterministic and cacheable; generation is not.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fpractical-rag-in-laravel-pgvector-embeddings-and-retrieval-pipelines-1&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-1) 

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

  3 questions  

     Q01  Do I need a dedicated vector database like Pinecone for RAG in Laravel?        Not for most workloads. The pgvector PostgreSQL extension supports cosine-similarity search with ivfflat indexing, which handles millions of vectors efficiently. A dedicated vector DB only becomes necessary at very large scale or when you need advanced filtering that pgvector cannot express. 

      Q02  How do I test the embedding and retrieval logic without hitting the OpenAI API?        Because the embedding provider is behind an interface, you can bind a fake in tests that returns a fixed float array. Retrieval tests can seed document_chunks with known vectors and assert that the correct chunks are returned for a given query vector. 

      Q03  What chunk size should I use for embeddings?        512 tokens with a 64-token overlap is a solid general-purpose default. For technical documentation you may go up to 1024 tokens; for conversational snippets 256 tokens often works better. Measure retrieval precision on your own dataset rather than relying on universal rules. 

  Continue reading

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

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

 [ ![Filament v3 Table Bulk Actions: Custom Confirmation, Progress Feedback, and Job Dispatch](https://cdn.msaied.com/412/68d290c667a619900211863678fa0b1f.png) filament laravel queues 

### Filament v3 Table Bulk Actions: Custom Confirmation, Progress Feedback, and Job Dispatch

Go beyond the default delete bulk action. Learn how to build Filament v3 bulk actions with custom confirmation...

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

 11 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/filament-v3-table-bulk-actions-custom-confirmation-progress-feedback-and-job-dispatch) [ ![Laravel Reverb WebSocket Scaling: Channels, Presence, and Horizontal Deployment](https://cdn.msaied.com/410/2698f47a7daff990e9807996fe0f493c.png) laravel reverb websockets 

### Laravel Reverb WebSocket Scaling: Channels, Presence, and Horizontal Deployment

Move beyond a single Reverb node. Learn how to scale Laravel Reverb horizontally with Redis pub/sub, configure...

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

 11 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-reverb-websocket-scaling-channels-presence-and-horizontal-deployment) [ ![Laravel Enum Casts, Backed Enums, and Value Semantics in PHP 8.3](https://cdn.msaied.com/408/3534df365ffb2c8f6c430180bfaba8f1.png) laravel php8.3 enums 

### Laravel Enum Casts, Backed Enums, and Value Semantics in PHP 8.3

PHP 8.3 backed enums combined with Laravel's native enum casting unlock type-safe domain models. Learn how to...

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

 10 Jul 2026     1 min read  

  Read    

 ](https://msaied.com/articles/laravel-enum-casts-backed-enums-and-value-semantics-in-php-83) 

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