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 Pipelines

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

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

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

ShareCopy linkCopied

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

  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)

 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.

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

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

  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.

   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.

   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.

   ![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 articleLaravel Reverb WebSocket Scaling: Channels, Presence, and Horizontal Deployment](https://www.msaied.com/public/articles/laravel-reverb-websocket-scaling-channels-presence-and-horizontal-deployment) [Next articleFilament v3 Table Bulk Actions: Custom Confirmation, Progress Feedback, and Job Dispatch](https://www.msaied.com/public/articles/filament-v3-table-bulk-actions-custom-confirmation-progress-feedback-and-job-dispatch)  

   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)

 ###  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)
