Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

RAG with pgvector and Laravel Practical Setup

By Kokil Thapa | Last reviewed: August 2026

Building a RAG with pgvector and Laravel practical setup requires moving beyond basic full-text search to implement true semantic understanding within your application. While traditional MySQL or PostgreSQL text search matches keywords, Retrieval Augmented Generation (RAG) matches meaning by converting content into vectors and finding the closest mathematical neighbors in high-dimensional space. This guide provides the exact configuration, migration code, and query patterns needed to deploy this architecture on Laravel 12 with PostgreSQL 17 and pgvector 0.8+.

How do you install and configure pgvector for a RAG with pgvector and Laravel practical setup?

Before writing any PHP code, your database server must support vector operations natively. In my experience deploying database-driven web applications, skipping this infrastructure verification is the most common cause of deployment failure. You cannot simply add a package via Composer and expect vector math to work; the database engine itself must be patched.

For production environments running Ubuntu 22.04 or 24.04 LTS with PostgreSQL 17, install the official PGDG repository packages rather than compiling from source. Compiling introduces build-time dependencies that complicate future security patching and upgrades.

# Install pgvector for PostgreSQL 17 on Ubuntu
sudo apt install postgresql-17-pgvector

# Verify installation inside psql
CREATE EXTENSION IF NOT EXISTS vector;
SELECT extversion FROM pg_extension WHERE extname = 'vector';

Once the extension is active, create the Laravel migration. The critical detail here is dimension matching. If you plan to use OpenAI’s text-embedding-3-small, set dimensions to 1536. For nomic-embed-text via Ollama, use 768. Mismatched dimensions will cause silent failures or runtime exceptions during insertion.

// database/migrations/xxxx_xx_xx_create_document_chunks_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('document_chunks', function (Blueprint $table) {
            $table->id();
            $table->foreignId('document_id')->constrained()->cascadeOnDelete();
            $table->text('content');
            $table->json('metadata')->nullable();
            // 1536 dimensions for text-embedding-3-small
            $table->rawColumn('embedding', 'vector(1536)'); 
            $table->timestamps();
            
            // Index for cosine similarity search
            $table->rawIndex('USING hnsw (embedding vector_cosine_ops)', 'idx_chunks_embedding');
        });
    }
};

Note the raw index definition. Laravel’s schema builder does not natively support HNSW indexes as of version 12.x. You must use rawIndex or a post-migration DB statement. Without this index, similarity searches on tables exceeding 10,000 rows will degrade from milliseconds to seconds, making real-time RAG impossible.

Raw DocumentPDF / Text / HTMLChunker + EmbedderLaravel Job + APIPostgreSQL 17pgvector HNSW IndexLLM ContextAugmented PromptFigure 1: Data pipeline for RAG with pgvector and Laravel practical setup
End-to-end data flow for RAG with pgvector and Laravel practical setup showing ingestion, embedding, storage, and retrieval stages.

How do you generate and store embeddings efficiently in Laravel?

Embedding generation is the bottleneck in any RAG system. Never perform this synchronously during a user request. On a legal-tech portal I built for document analysis, synchronous embedding added 800ms–2s per upload, destroying UX. Always offload to Laravel Queues.

Create a dedicated job that handles chunking and embedding atomically. Use the OpenAI PHP client or direct HTTP calls to your local Ollama instance. Store the resulting vector as a string representation that pgvector accepts.

// app/Jobs/GenerateDocumentEmbeddings.php
use App\Models\DocumentChunk;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use OpenAI\Laravel\Facades\OpenAI;

class GenerateDocumentEmbeddings implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(private DocumentChunk $chunk) {}

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

        // Convert float array to pgvector string format "[0.001, -0.023, ...]"
        $vectorString = '[' . implode(',', $response->embeddings[0]->embedding) . ']';

        $this->chunk->update([
            'embedding' => $vectorString,
        ]);
    }
}

A common mistake is storing embeddings in a separate table linked by foreign key. This forces JOINs during similarity search, which prevents pgvector from using the HNSW index efficiently. Keep content and embedding in the same row unless you have a specific multi-modal reason to separate them.

For cost-sensitive projects in Nepal where API budgets are tight, consider running nomic-embed-text or mxbai-embed-large locally via Ollama. These models produce 768-dimensional vectors with quality approaching proprietary APIs for domain-specific legal and technical content. Adjust your migration dimension accordingly and update the job to call http://localhost:11434/api/embeddings.

How do you execute semantic similarity queries for RAG with pgvector and Laravel practical setup?

Retrieval is where the theoretical meets the practical. Eloquent does not understand vector operators, so you must drop to raw expressions for the similarity calculation while keeping the rest of the query builder intact for filtering and pagination.

The three primary distance operators in pgvector are:

  • <-> — Euclidean (L2) distance. Good for normalized vectors.
  • <#> — Inner product. Best when vectors are already normalized to unit length.
  • <=> — Cosine distance. Most versatile for text embeddings; measures angular similarity regardless of magnitude.

For text-based RAG, cosine distance is almost always correct. Here is the pattern I use repeatedly in production:

// app/Services/SemanticSearchService.php
namespace App\Services;

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

class SemanticSearchService
{
    public function search(string $query, int $limit = 5): array
    {
        // 1. Embed the query first
        $response = OpenAI::embeddings()->create([
            'model' => 'text-embedding-3-small',
            'input' => $query,
        ]);
        
        $queryVector = '[' . implode(',', $response->embeddings[0]->embedding) . ']';

        // 2. Retrieve nearest neighbors with metadata filtering
        return DocumentChunk::query()
            ->selectRaw("*, embedding <=> ? as distance", [$queryVector])
            ->whereNotNull('embedding')
            ->orderByRaw("embedding <=> ?", [$queryVector])
            ->limit($limit)
            ->get()
            ->map(fn ($chunk) => [
                'content'   => $chunk->content,
                'score'     => 1 - $chunk->distance, // Convert distance to similarity
                'metadata'  => $chunk->metadata,
            ])
            ->toArray();
    }
}

Always filter out NULL embeddings before ordering. Rows inserted before backfill jobs complete will have NULL vectors and cause sorting errors or misleading zero-distance results. The whereNotNull guard is non-negotiable in systems with asynchronous embedding pipelines.

Cosine Distance (<=>)Angle-based • Scale invariantBest for text embeddingsEuclidean (<->)Magnitude sensitiveRequires normalizationInner Product (<#>)Fastest computationUnit vectors onlyFigure 2: Choosing the right distance metric for RAG with pgvector and Laravel practical setup
Visual comparison of distance operators to select the correct metric for your RAG with pgvector and Laravel practical setup.

What are the performance trade-offs between pgvector and dedicated vector databases?

This question arises on nearly every project where I consult on Laravel API architecture. Dedicated vector databases like Pinecone, Weaviate, or Qdrant offer managed scaling and specialized indexing, but they introduce operational complexity that many teams underestimate.

Criteriapgvector (PostgreSQL 17)Dedicated Vector DB
Operational overheadLow — same backup, monitoring, replication as app DBHigh — separate infra, auth, networking, billing
Transactional consistencyFull ACID with relational dataEventual consistency across systems
Max scale (single node)~10M vectors comfortably with HNSW100M+ with sharding
Filtering + searchNative SQL WHERE clausesProprietary filter syntax
Cost at low-medium scaleFree (existing Postgres)$70–300+/month minimum
Ecosystem integrationNative Eloquent/QueryBuilderSDK required, no ORM support

For most Laravel applications serving SMEs, legal portals, or internal tools, pgvector wins decisively. The ability to join vector search results against users, permissions, documents, and audit logs in a single transactional query eliminates an entire class of synchronization bugs. Only move to a dedicated vector database when you exceed 10 million vectors or require sub-10ms latency at massive concurrent query volumes.

In Nepal specifically, where cloud infrastructure costs are paid in USD but revenue often comes in NPR, avoiding a $200/month vector database bill for a project earning Rs 150,000/month (~USD 1,125) is a meaningful margin difference. pgvector lets you ship semantic search on existing infrastructure without adding another vendor dependency.

How do you tune HNSW indexes and avoid common production pitfalls?

HNSW (Hierarchical Navigable Small World) indexes are powerful but misconfigured defaults cause real problems. Two parameters matter most:

  • m — Number of connections per layer. Default 16. Higher = better recall, more memory, slower builds.
  • ef_construction — Size of dynamic candidate list during index build. Default 64. Higher = better quality, slower builds.

For text RAG workloads, I’ve found m=16, ef_construction=128 provides excellent recall without excessive build times. Create the index with explicit parameters:

-- Run after initial data load, not during migration
CREATE INDEX CONCURRENTLY idx_chunks_embedding_hnsw 
ON document_chunks 
USING hnsw (embedding vector_cosine_ops) 
WITH (m = 16, ef_construction = 128);

Always use CONCURRENTLY in production. Building an HNSW index on a million-row table can take hours; without this flag, the table is locked and your application goes down. Schedule index creation during low-traffic windows and monitor progress via pg_stat_progress_create_index.

Another pitfall: forgetting to set hnsw.ef_search at query time. This session-level parameter controls search accuracy vs. speed trade-off. Set it per connection or per query:

// In your service provider or before search queries
DB::statement("SET LOCAL hnsw.ef_search = 100");

Values between 50–200 cover most RAG use cases. Below 50, recall drops noticeably. Above 200, latency increases without meaningful quality gains for text embeddings. Benchmark with your actual dataset rather than trusting generic advice.

Start: Dataset Size?< 500K vectors> 500K vectorsm=16, ef_construct=64Fast build, good recallm=32, ef_construct=256Slower build, high recallSET LOCAL hnsw.ef_search = 100Tune per-query based on latency SLAFigure 3: HNSW tuning decision tree for RAG with pgvector and Laravel practical setup
Decision framework for HNSW parameter selection in your RAG with pgvector and Laravel practical setup based on scale and performance requirements.

Implementing Your RAG with pgvector and Laravel Practical Setup Today

A successful RAG with pgvector and Laravel practical setup combines correct infrastructure, disciplined async processing, precise query construction, and informed index tuning. Start with PostgreSQL 17 and pgvector installed via system packages, create migrations with explicit vector dimensions and HNSW indexes, offload embedding generation to queued jobs, and use cosine distance with raw expressions for retrieval. Resist the urge to adopt a dedicated vector database until your scale genuinely demands it.

If you’re planning a semantic search feature, legal document analysis tool, or AI-powered knowledge base and need hands-on implementation support, reach out through my contact page. I’ve shipped multiple production RAG systems on Laravel and can help you avoid the pitfalls that only surface under real user load.

Frequently Asked Questions

Retrieval-Augmented Generation using PostgreSQL's pgvector extension for semantic search within a Laravel application to ground LLM responses in private data.

Self-hosted on a VPS costs Rs 2,500–4,000 monthly (~USD 19–30), plus LLM API fees; managed cloud databases start higher but reduce DevOps overhead significantly.

When your primary data already lives in PostgreSQL and you want to avoid managing separate vector infrastructure, reducing operational complexity and latency between transactional and semantic queries.

Laravel 12 running on PHP 8.2 or higher is the current stable baseline for production RAG systems. The pgvector-php library requires PHP 8.1 minimum, but matching your framework version prevents dependency conflicts during Composer installs. I have deployed this stack on Ubuntu 24 servers with PHP 8.4 without issues, though PHP 8.3 remains the most widely tested combination in my client projects. Always verify extension compatibility before upgrading production environments.

Install via apt using the official PostgreSQL repository to get the latest stable build rather than outdated distro packages. Run sudo apt install postgresql-17-pgvector for PostgreSQL 17, then execute CREATE EXTENSION vector inside your target database. On shared hosting or managed services like AWS RDS, enable it through the provider console instead. In my experience deploying legal-tech portals, missing this step causes silent failures where migrations pass but vector queries return empty results at runtime because the extension exists in one schema but not the default search path.

Multilingual models like sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 handle Nepali Devanagari script reasonably well for general retrieval tasks. For legal documents or specialized terminology, fine-tuning on domain-specific Nepali text improves recall significantly. OpenAI's text-embedding-3-small also performs adequately but adds API costs around USD 0.02 per million tokens. On projects like Court Marriage In Nepal, I found that chunking strategy matters more than model choice; splitting by semantic paragraphs rather than fixed character counts preserved context better for Bikram Sambat dates and legal citations.

Create a dedicated table with columns for source_id, chunk_text, metadata JSONB, and a vector column sized to match your embedding dimensions (typically 1536 for OpenAI or 384 for MiniLM). Add an HNSW index with CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops) after initial bulk insert completes, as building indexes during insertion slows imports dramatically. Include a generated_at timestamp for cache invalidation. In production Laravel applications, I keep embeddings separate from business entities to allow re-embedding without touching transactional tables during model upgrades or prompt refinements.

Between 300 and 500 tokens with 50-token overlap works well for most technical and legal content. Smaller chunks improve precision but lose context; larger chunks preserve meaning but introduce noise. Test empirically against your actual query patterns rather than following generic recommendations. On a legal information portal I built, 400-token chunks with paragraph-boundary detection outperformed fixed-size splitting because Nepali legal explanations often span multiple sentences before reaching actionable conclusions. Always store both raw text and chunked versions to enable re-chunking without re-extracting source documents.

Use Laravel's query builder parameter binding exclusively; never concatenate user input into vector search clauses. The pgvector-php library handles type casting safely when using bound parameters. Validate embedding dimensions server-side before querying to reject malformed vectors that could cause errors or unexpected behavior. In REST APIs exposing semantic search, implement rate limiting via Laravel Sanctum middleware since vector queries are computationally expensive. On client projects, I have seen developers accidentally expose raw similarity scores in API responses, leaking information about other users' embedded content through timing side channels.

Common causes include mismatched embedding models between indexing and query time, incorrect distance metric selection (cosine vs L2), missing normalization of input text, or chunks too small to contain meaningful context. Verify your HNSW index was built after data insertion completed. Check if metadata filters are excluding valid results. In one production deployment, stale embeddings from a previous model version caused gradual relevance degradation after a silent re-deployment. Implement embedding version tracking and automated re-indexing pipelines to catch drift before users report poor search quality.

Tune HNSW parameters m and ef_construction based on your recall-latency tradeoff requirements; defaults rarely suit production workloads. Enable connection pooling with PgBouncer since vector queries hold connections longer than typical CRUD operations. Warm the index cache after deployments by running sample queries. Monitor pg_stat_user_indexes for unused indexes consuming memory. On high-traffic eCommerce sites, I offload embedding generation to background queues and batch vector inserts during low-traffic windows. Consider read replicas dedicated to semantic search if transactional and analytical workloads compete for resources during peak hours.

Yes, using local models like Llama 3 or Mistral via Ollama alongside pgvector for retrieval. This eliminates API costs and keeps sensitive data on-premise, crucial for legal or medical applications. Hardware requirements are significant; expect to need 32GB+ RAM and GPU acceleration for acceptable latency. In my experience with Nepal-based clients concerned about data sovereignty, hybrid approaches work best: use local embeddings for indexing but call external APIs only for final response generation when quality matters more than privacy. Budget Rs 15,000–25,000 monthly for adequate GPU VPS hosting.

Store document hashes and chunk identifiers to enable incremental updates. When source content changes, delete only affected chunks and regenerate embeddings for modified sections rather than reprocessing entire documents. Use Laravel's model observers or event listeners to trigger async re-embedding jobs via Redis queues. Maintain an embedding_version column to track which model and chunking strategy produced each vector. On legal portals where statutes update frequently, this approach reduced re-indexing time from hours to minutes while preserving search continuity during maintenance windows.

Embeddings can leak semantic information about source content through inversion attacks; treat them as sensitive data requiring encryption at rest and in transit. Implement row-level security in PostgreSQL to ensure users only retrieve vectors they own. Sanitize LLM outputs before displaying to prevent prompt injection attacks from executing unintended actions. Log all semantic queries for audit trails, especially in regulated domains. On legal-tech platforms, I enforce strict tenant isolation at the database level rather than relying solely on application-layer checks, since vector similarity searches bypass traditional foreign key constraints.

Track retrieval precision, answer faithfulness, and user satisfaction signals like thumbs-up feedback or follow-up query rates. Log retrieved chunks alongside generated responses to diagnose failures post-hoc. A/B test different chunk sizes, embedding models, and prompt templates against real user queries rather than synthetic benchmarks. Monitor p95 latency separately for retrieval and generation phases. In client projects, I set up simple dashboards showing daily query volume, average similarity scores, and fallback-to-web-search rates. Business metrics like support ticket reduction or conversion improvements ultimately validate technical tuning efforts better than abstract NLP scores.

Share this article

Quick Contact Options
Choose how you want to connect me: