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) ⇄ orders ⇄ tickets 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 Type | Plain RAG | Context Mesh |
|---|---|---|
| Exact order lookup | ✅ | ✅ |
| Customer 360 roll-up | 😬 | ✅ |
| “First purchase when?” | 😬 | ✅ |
| “Top related tickets?” | 😬 | ✅ |
Capturing relationships lets retrieval use them naturally.
Production Reality
Security:
search_structured()blocks DDL/DML- RLS policies for PII (hash emails, tenant isolation)
- Allowlist dangerous SQL patterns
Cost drivers:
- Embedding generation per chunk
- HNSW index maintenance
- Graph storage growth
Start simple: Customers → Orders → Tickets. Add edges as questions emerge.
Vibe Coding Takeaway
This isn’t quick-and-dirty. It’s elegant engineering:
- ✅ Simple: 3 files of logic
- ✅ Cheap: Single Supabase project
- ✅ Fast: B-tree + HNSW + GIN indexes
- ✅ Precise: Relationships beat brute-force similarity
Get started:
- Supabase project → enable
vector+pg_trgm - Run SQL migration
- Wire ingestion →
ingest_document_chunk() - Call
search_unified()from your API
Sometimes the most powerful solutions are the simplest ones that just work.