Skip to content
Go back

Elegant RAG - Superior Search in Supabase with Just 3 Files

Plain RAG excels at finding facts in text passages, but struggles with relationship questions like “How many orders has this customer placed?” or “What’s their lifetime value?” The classic approach—chunk documents, embed them, dump into a vector store—works fine with small datasets. But as data grows, you get noise: irrelevant customer orders mixed together, hallucinations across departments.

Table of contents

Open Table of contents

The Context Mesh Breakthrough

Enter Context Mesh—a lightweight knowledge graph living inside Supabase that fuses semantic, lexical, and relational context into one ranked result set using RRF (Reciprocal Rank Fusion). This isn’t theoretical. It’s a battle-tested pattern that lives mostly in SQL + Supabase RPCs.

Real-World Example

User asks: “Show me order #889 and customer context.”

Plain RAG returns:

{
  "order_id": 889,
  "customer": "Alexis Chen", 
  "email": "alexis@example.com",
  "items": ["Ethiopia Natural 2x"],
  "ship_status": "Delivered 2024-03-11"
}

Context Mesh delivers:

{
  "order_id": 889,
  "customer": "Alexis Chen",
  "lifetime_orders": 7,
  "first_order_date": "2022-08-19", 
  "lifetime_value_eur": 642.80,
  "support_tickets": 3,
  "last_ticket_disposition": "Carrier delay - resolved"
}

How? The system links node(customer: Alexis Chen)orderstickets and stores those edges. Retrieval runs vector + FTS + graph search, then fuses with RRF.

The 3-File Architecture

[Files/CSVs] ──> [document] ──> [chunk] ─┬─> [chunk_embedding] (vector)
                                      ├─> [chunk.tsv] (FTS)  
                                      └─> [chunk_node] ─> [node] <─> [edge] (graph)

vector/full-text/graph ──> search_unified (RRF) ──> ranked results

1. Ingestion path - Standardizes content, writes to SQL + graph

2. Retrieval path - Vector + FTS + graph fusion

3. SQL migration - Tables, functions, indexes (one file)

The SQL Migration

Extensions

CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;

Core Tables

CREATE TABLE IF NOT EXISTS public.document (...);
CREATE TABLE IF NOT EXISTS public.chunk (..., tsv TSVECTOR, ...);
CREATE TABLE IF NOT EXISTS public.chunk_embedding (
  chunk_id BIGINT PRIMARY KEY REFERENCES public.chunk(id) ON DELETE CASCADE,
  embedding VECTOR(1536) NOT NULL
);
CREATE TABLE IF NOT EXISTS public.node (...);
CREATE TABLE IF NOT EXISTS public.edge (... PRIMARY KEY (src, dst, type));
CREATE TABLE IF NOT EXISTS public.chunk_node (... PRIMARY KEY (chunk_id, node_id, rel));

Speed Indexes

CREATE INDEX IF NOT EXISTS chunk_tsv_gin ON public.chunk USING GIN (tsv);
CREATE INDEX IF NOT EXISTS emb_hnsw_cos ON public.chunk_embedding 
  USING HNSW (embedding vector_cosine_ops);
CREATE INDEX IF NOT EXISTS edge_src_idx ON public.edge (src);
CREATE INDEX IF NOT EXISTS node_labels_gin ON public.node USING GIN (labels);

Search Primitives

CREATE OR REPLACE FUNCTION public.search_vector(
  p_embedding VECTOR(1536), p_limit INT
)
RETURNS TABLE(chunk_id BIGINT, score FLOAT8, rank INT) 
LANGUAGE sql STABLE AS $$
  SELECT ce.chunk_id, 
    1.0 / (1.0 + (ce.embedding <=> p_embedding)) AS score,
    row_number() OVER (ORDER BY ce.embedding <=> p_embedding) AS rank
  FROM public.chunk_embedding ce 
  LIMIT p_limit;
$$;

Unified Fusion

CREATE OR REPLACE FUNCTION public.search_unified(
  p_query_text TEXT, 
  p_query_embedding VECTOR(1536),
  p_keywords TEXT[], 
  p_query_sql TEXT, 
  p_limit INT DEFAULT 20
) 
RETURNS TABLE(..., final_score FLOAT8) 
LANGUAGE sql STABLE AS $$
  -- Fuses vector + FTS + graph + structured results via RRF
$$;

Benchmark Results

Query TypePlain RAGContext Mesh
Exact order lookup
Customer 360 roll-up😬
“First purchase when?”😬
“Top related tickets?”😬

Capturing relationships lets retrieval use them naturally.

Production Reality

Security:

Cost drivers:

Start simple: Customers → Orders → Tickets. Add edges as questions emerge.

Vibe Coding Takeaway

This isn’t quick-and-dirty. It’s elegant engineering:

Get started:

  1. Supabase project → enable vector + pg_trgm
  2. Run SQL migration
  3. Wire ingestion → ingest_document_chunk()
  4. Call search_unified() from your API

Sometimes the most powerful solutions are the simplest ones that just work.


Share this post on:

Previous Post
Marketing Automation with AI Services
Next Post
The AI Coding Trust Reality Check - From Hype to Pragmatism