⌨ Keyboard shortcuts available
G — waiting for next key…
Laravel 13 PHP AI 8 min read

Beyond Semantic Similarity: Implementing GraphRAG with pgvector and Laravel

Learn how to implement GraphRAG in Laravel using pgvector and Recursive CTEs. Move beyond flat similarity search by extracting entity triples for multi-hop AI reasoning.

P

Pradeep Bhandari

 · 7 views

Branded cover card: Beyond Semantic Similarity: Implementing GraphRAG with pgvector and Laravel

Vector search often falls short when a query requires connecting multiple distinct data points. Standard vector similarity excels at finding isolated chunks that "sound" right, but it fails at multi-hop reasoning—like identifying developers linked to a specific roadmap item through separate project descriptions.

Instead of adding the operational overhead of a dedicated graph database and a Python microservice, you can build a robust knowledge graph directly in your existing PostgreSQL database using pgvector and Laravel. This architecture combines the semantic power of embeddings with the structural precision of a relational graph.

The Relational Graph Schema

To model a knowledge graph without a specialized engine, structure your data into chunks (vector data), nodes (entities), and edges (relationships). This keeps your data and graph logic in the same ACID-compliant database, eliminating the need to coordinate across disjointed systems.

PHP

Schema::create('knowledge_edges', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->foreignUuid('source_node_id')->constrained('knowledge_nodes');
    $table->foreignUuid('target_node_id')->constrained('knowledge_nodes');
    $table->string('relation_type'); // e.g., 'depends_on', 'triggers', 'reports_to'
    $table->decimal('confidence_score', 3, 2);
    $table->timestamps();
    
    $table->index(['source_node_id', 'relation_type']);
});

This structural pivot-style format acts as the glue, allowing you to transition instantly from a vector match to its surrounding, logical dependencies.

Entity Extraction via Laravel AI SDK

To populate this schema, raw text must be parsed into semantic triples: Subject-Predicate-Object (e.g., OrderService -> triggers -> EmailNotification). The Laravel AI SDK handles this via structured agentic output, mapping the relational DNA of your documents predictably to prevent hallucinated connections.

PHP

$entities = AI::agent('extractor')
    ->withOutputSchema([
        'triples' => 'array',
        'triples.*.subject' => 'string',
        'triples.*.predicate' => 'string',
        'triples.*.object' => 'string',
    ])
    ->prompt("Extract relationships from: {$chunk}")
    ->predict();

High-Volume Ingestion

To prevent processing bottlenecks during high-volume document ingestion, decouple this step from your web thread. Wrap the extraction logic inside queued jobs using Laravel's queue worker or Concurrency::run().

PHP

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

    public function handle(GraphService $service): void
    {
        // Extract triples using Laravel AI SDK
        $triples = $service->extract($this->document->content);

        // Sync nodes and edges within a transaction
        // Uses pgvector to run entity resolution checks first
        $service->syncKnowledgeGraph($triples);
    }
}
Entity Resolution: To avoid graph duplication (e.g., treating "Tesla" and "Tesla Inc." as disparate nodes), run a rapid pgvector similarity query before creating a new node. It serves as a semantic "fuzzy join" to keep your map unified.

The Hybrid Retrieval Loop

GraphRAG operates as a two-stage retrieval process:

  1. Semantic Landing: Use vector similarity to identify the immediate neighborhood of a query.
  2. Graph Expansion: Take those vector hits as "seed nodes" and traverse their structural edges to discover deeply relevant adjacent context.

PHP

// Inside a Laravel AI Agent class
public function getContext(string $query): Collection
{
    // 1. Locate seed nodes via pgvector similarity
    $seeds = EntityNode::query()
        ->nearestNeighbors('embedding', $query)
        ->limit(3)
        ->get();

    // 2. Traversal: Gather adjacent graph nodes
    return $seeds->flatMap(function ($node) {
        return $node->neighbors()
            ->with('metadata')
            ->limit(10)
            ->get();
    });
}

Multi-Hop Traversal via Recursive CTEs

When queries demand deep cross-referencing, navigate your graph layout using PostgreSQL Recursive Common Table Expressions (CTEs). This executes a fast Breadth-First Search (BFS) directly inside your database engine.

SQL

WITH RECURSIVE graph_traversal AS (
    -- Anchor member: Start with the vector-matched seed entity
    SELECT target_id, 1 as depth, ARRAY[source_id, target_id] as path
    FROM edges
    WHERE source_id = :seed_id

    UNION ALL

    -- Recursive member: Hop to the next connected layer
    SELECT e.target_id, gt.depth + 1, gt.path || e.target_id
    FROM edges e
    JOIN graph_traversal gt ON e.source_id = gt.target_id
    WHERE gt.depth < 3 
    AND NOT e.target_id = ANY(gt.path) -- Infinite loop guard
)
SELECT * FROM graph_traversal;

Critical Traversal Guardrails

  • Infinite Loops: Complex data forms circular dependencies (Node A $\rightarrow$ Node B $\rightarrow$ Node A). The statement NOT target_id = ANY(path) tracks the visited history and safely kills cyclic recursion.
  • Depth Control: Cap graph expansion at 2 or 3 hops (gt.depth < 3). Unchecked depth triggers exponential data noise and blows past LLM token limits.

The Orchestration & Routing Layer

Running intensive graph traversals on trivial lookups will bottleneck your database. Implement an intent router using the Laravel AI SDK to classify incoming queries and dynamically choose the optimal retrieval path.

PHP

$intent = AI::agent('IntentRouter')
    ->instructions('Determine if the query requires connecting multiple entities or nodes.')
    ->predict($userInput);

if ($intent->is('relational')) {
    $context = $this->graphService->getMultiHopContext($userInput);
} else {
    $context = $this->vectorService->getSimilarityContext($userInput);
}

Context Serialization

LLMs cannot parse raw database adjacency lists natively. Serialize graph paths into descriptive natural language sentences before passing them to the prompt context window:

Instead of raw JSON arrays, output standard strings:
"System A depends on Service B, which is maintained by Team C."

Scaling Limits

While PostgreSQL handles millions of edges effectively, deep traversals over massive datasets will eventually degrade relational join performance. When your workload hits these physical limitations, plan an architectural migration path toward dedicated large-scale retrieval engines like Vespa.ai.

Quick Reference Summary

ConceptImplementation StrategyKey Benefit
Vector Searchpgvector Cosine SimilarityInstantly locates surface-level content ("Vibes").
Graph TraversalPostgreSQL Recursive CTEsSolves multi-hop logic without data duplication.
Entity ExtractionLaravel AI SDK & JSON SchemasEnforces predictable, clean node generation.
Ingestion PipelineKafka / SQS + Laravel Queued JobsProtects user-facing request latency.

Share this article

Related Articles

Discussion

No comments yet — be the first to share your thoughts.

Leave a comment

Comments are moderated before appearing.

Max 2,000 characters · not published

We respect your privacy