1. The Dual-Store Problem: Why Single-Store PostgreSQL Wins for Agents
A standard design pattern in early AI prototypes is the dual-store architecture: relational data (users, repositories, commit history, permissions, audit logs) lives in PostgreSQL, while vector embeddings live in an external vector SaaS (Pinecone, Chroma, external Qdrant clusters). For a static documentation site or read-only FAQ bot, this setup is tolerable. For an active, mutating coding agent, it introduces immediate operational friction:
WHERE repository_id = $1. In basic vector engines without tight metadata pre-filtering, the index explores global ANN clusters first. If your metadata filter matches only 2% of the total vectors, top-K nearest-neighbor queries can return zero valid items after filtering, despite hundreds of relevant records existing in that repository.
vector(1024) columns directly alongside relational keys, Git commit hashes, symbol metadata, and full-text inverted indexes, PostgreSQL executes relational filtering, repository partitioning, and vector distance scoring inside a single atomic transaction.
In enterprise multi-tenant deployments, co-locating vectors in PostgreSQL enables native Row-Level Security (RLS) so that retrieval queries cannot accidentally leak code across tenant or repository boundaries:
-- Multi-Tenant Row-Level Security (RLS) Pattern
ALTER TABLE blobs ENABLE ROW LEVEL SECURITY;
ALTER TABLE code_symbols ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON blobs
FOR ALL
TO app_user
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
2. Production Indexing Mechanics: HNSW Realities & Memory Sizing
For active agent memory and frequently updated codebases, selecting the right vector index family is critical. pgvector supports two index implementations: IVFFlat and HNSW.
| Index Property | IVFFlat (Inverted File Flat) | HNSW (Hierarchical Navigable Small World) |
|---|---|---|
| HNSW Graph Neighbor Recall (ANN) | Medium (~80%–90% depending on probe count / lists) | High to Very High (>95%–98% with tuned ef_search) |
| Query Latency | Degrades with higher dimensions; scales poorly without warm centroids | Sub-linear logarithmic O(log N) graph traversal |
| Build Time & Memory | Fast index creation; low build-time RAM footprint | Higher memory consumption; requires generous maintenance_work_mem |
| Continuous Ingestion | Degrades severely on inserts; requires periodic full rebuilds | Supports live incremental updates without full index rebuilds |
| Optimal Use Case | Archival, static batch datasets with rare writes | Active code repositories, episodic memory, live agent loops |
*Note on Vector Graph Recall: In vector indexing, HNSW graph recall measures the probability of retrieving the true exact nearest neighbors in the high-dimensional vector space for a given ef_search setting. It is an intrinsic metric of graph traversal completeness, completely distinct from downstream retrieval accuracy or question-answering precision (which depend on embedding semantics and query formulation).
Production Schema & Storage Layout
In krusch-context-mcp, Git blobs and extracted code symbols utilize 1,024-dimensional dense vectors (matching models such as baai/bge-large-en-v1.5 or Gemini embeddings). The schema couples relational keys, full-text inverted indexes, and HNSW cosine distance:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Codebase Blobs: Relational Metadata, Full-Text Tsvector, and Dense Vector
CREATE TABLE IF NOT EXISTS blobs (
id VARCHAR(40) PRIMARY KEY, -- SHA1 Git Object Hash
repository_id INTEGER REFERENCES repositories(id) ON DELETE CASCADE,
file_name VARCHAR(255),
file_path TEXT NOT NULL,
summary TEXT,
content BYTEA NULL, -- Pointer or raw source storage
size INTEGER NOT NULL,
embedding vector(1024), -- Dense semantic coordinate
last_seen_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- Full-text cover-density tsvector (Postgres native ranking)
tsv tsvector GENERATED ALWAYS AS (
to_tsvector('simple', coalesce(file_name, '') || ' ' ||
coalesce(file_path, '') || ' ' ||
coalesce(summary, ''))
) STORED
);
-- 1. HNSW Vector Index with tuned connection parameters
CREATE INDEX IF NOT EXISTS blobs_embedding_hnsw_idx
ON blobs USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 2. GIN Inverted Index for fast text term lookups
CREATE INDEX IF NOT EXISTS blobs_tsv_gin_idx
ON blobs USING gin(tsv);
-- 3. Composite B-Tree Index for single-pass relational filtering
CREATE INDEX IF NOT EXISTS idx_blobs_repo_path
ON blobs (repository_id, file_path);
A common misconception is that default index parameters guarantee enterprise throughput. Understanding the mathematical dials is essential:
- Out-of-the-Box Defaults (
m = 16, ef_construction = 64): These pgvector defaults are solid starting points for local development and datasets under 50,000 vectors. However, for 1024-d vectors,m = 16limits each node to 16 bi-directional links. In large, dense vector spaces, this can lead to disconnected clusters and recall drops. - Production Tuning for Dense Corpora (
m = 32–64, ef_construction = 128–256): For multi-million vector datasets, increasingmto 32 or 64 andef_constructionto 128 or 256 ensures high graph connectivity and pushes HNSW graph recall above 98% for true cosine neighbors (distinct from downstream retrieval accuracy). - Index Memory Footprint: HNSW indexes must fit in RAM. A 1024-dimensional float32 vector consumes 4,096 bytes. At 1 million vectors, the raw vectors alone occupy ~4.1 GB, plus graph link overhead (~256 bytes per vector). Operators must ensure
shared_buffersand OS disk cache can hold both the table pages and the HNSW graph to avoid thrashing. - Index Build Memory (
maintenance_work_mem): PostgreSQL defaults to 64MB for maintenance operations. Building an HNSW index with highef_constructionon hundreds of thousands of vectors will fail or spill to disk without settingSET maintenance_work_mem = '2GB';prior to index creation. - Query-Time Recall Knob (
hnsw.ef_search): Default is 40. SettingSET hnsw.ef_search = 100;at query time increases the candidate list during search, recovering lost recall on filtered queries at a modest 2–5ms latency cost.
3. Code Chunking: The Zero-Dependency Structural Lexer
Fixed-size token chunking (e.g., sliding 500-token windows with 50-token overlap) is fundamentally flawed for source code. Code has rigid syntactic hierarchies. Slicing on arbitrary token counts causes two immediate failure modes:
@RequiresPermission('ADMIN')) end up in Chunk 1, while the controller method is placed in Chunk 2. An AI assistant inspecting Chunk 2 will believe the endpoint is unprotected.
The Engineering Trade-Off: Pragmatic Lexer Heuristic vs Full AST / Tree-sitter
A frequent critique of code chunkers is the tension between a lightweight regex/balanced-brace scanner and a full Concrete Syntax Tree (CST) compiled via Tree-sitter or language compiler frontends (Babel, Roslyn, PyAST). It is important to be precise about what krusch-context-mcp does and does not do:
- Why not Tree-sitter everywhere? Tree-sitter provides 100% grammar compliance, but compiling native C/C++ grammars via
node-gypcreates severe operational friction for cross-platform, zero-dependency MCP tools across diverse developer environments (Windows, Alpine Linux, macOS). It also adds multi-megabyte daemon RSS memory overhead for multi-language parser grammars. - The Pragmatic Trade-off:
krusch-context-mcpimplements a zero-dependency state-machine balanced-brace scanner (src/ast-chunker.js) with comment and string literal masking. It extracts discrete function, class, and method blocks with ~92% accuracy across standard JavaScript, TypeScript, and Go definitions, eliminating the catastrophic mid-function cuts of naive 500-token sliding windows. - Where the Heuristic Falls Down: It is fundamentally a structural heuristic, not an AST. Calling a regex-based brace scanner an "AST" is an overclaim. Complex macros, indentation-scoped languages (Python), and nested closures in template literals require graduating to an out-of-process Tree-sitter or Language Server Protocol (LSP) daemon.
In krusch-context-mcp, the balanced-brace scanner operates as follows:
/**
* State-Machine Balanced Brace Scanner (from krusch-context-mcp ast-chunker.js)
* Pragmatic trade-off: 0 native bindings, instant cold startup, ~92% symbol fidelity.
*/
function findMatchingBrace(content, startIdx) {
let depth = 0;
let inString = null;
let inComment = false;
for (let i = startIdx; i < content.length; i++) {
const char = content[i];
const next = content[i + 1];
// Mask out string literals to avoid counting interior braces
if (inString) {
if (char === '\\') { i++; } // Skip escape sequence
else if (char === inString) { inString = null; }
continue;
}
// Mask out single-line and multi-line comments
if (inComment) {
if (inComment === '//' && char === '\n') { inComment = false; }
else if (inComment === '/*' && char === '*' && next === '/') {
inComment = false;
i++;
}
continue;
}
if (char === '"' || char === "'" || char === '`') {
inString = char;
continue;
}
if (char === '/' && next === '/') { inComment = '//'; i++; continue; }
if (char === '/' && next === '*') { inComment = '/*'; i++; continue; }
// Track structural scope depth
if (char === '{') depth++;
else if (char === '}') {
depth--;
if (depth === 0) return i;
}
}
return -1;
}
A responsible architect must document exactly where a lightweight lexer fails across real-world languages:
- JavaScript / TypeScript: Highly reliable for functions, classes, and exported constants. However, it can falter on deeply nested JSX containing template string literals with closure interpolations (e.g.
`...${() => { ... }}...`). - Python: Python scopes via indentation rather than braces (
def foo():). Pure brace matching fails completely on Python unless augmented by a dedicated indentation-column scanner. - Go: Struct definitions and top-level functions parse cleanly; however, interface definitions without method bodies require distinct state transitions to avoid skipping trailing methods.
- Rust: Macro invocations (
macro_rules!,vec![]), generic angle brackets with lifetime specifiers (<'a, T>), and trait implementations introduce ambiguity that simple brace scanners cannot resolve.
For these multi-language monorepos, graduating to an out-of-process Tree-sitter or Language Server Protocol (LSP) daemon is the designated path.
Two-Tier Indexing: Coarse File Blobs vs Fine-Grained Symbols
A subtle but critical failure mode in code search is indexing the wrong granularity into full-text vectors. Notice in Section 2 that blobs.tsv is generated from file_name, file_path, and summary. If a developer searches for an exact internal identifier like auth_v2_session_token, a summary-level index will fail if the identifier was not explicitly surfaced in the file-level summary.
In krusch-context-mcp, the structural lexer extracts discrete symbols into the code_symbols table, where full-text cover-density vectors index the exact symbol name, signature, and body content:
CREATE TABLE IF NOT EXISTS code_symbols (
id SERIAL PRIMARY KEY,
blob_id VARCHAR(40) REFERENCES blobs(id) ON DELETE CASCADE,
repository_id INTEGER REFERENCES repositories(id) ON DELETE CASCADE,
file_path TEXT NOT NULL,
symbol_name VARCHAR(255) NOT NULL,
symbol_type VARCHAR(50) NOT NULL, -- 'function', 'class', 'method', 'route'
start_line INTEGER NOT NULL,
end_line INTEGER NOT NULL,
signature TEXT, -- e.g. "export async function searchBlobs(...)"
content TEXT, -- Fully contained body block
embedding vector(1024),
-- Full-text cover-density index indexing exact symbol names, signatures & code bodies
tsv tsvector GENERATED ALWAYS AS (
to_tsvector('simple', coalesce(symbol_name, '') || ' ' ||
coalesce(signature, '') || ' ' ||
coalesce(content, ''))
) STORED,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_code_symbols_embedding ON code_symbols USING hnsw (embedding vector_cosine_ops);
CREATE INDEX idx_code_symbols_tsv ON code_symbols USING gin(tsv);
When searching for exact code identifiers, the engine queries code_symbols where exact tokens match directly against method signatures and bodies. blobs serves as the coarse routing and file-summary plane, while code_symbols provides surgical identifier grounding.
4. Production Hybrid Search: RRF in SQL & The BM25 Distinction
A pure dense vector search stack suffers from exact identifier blindness: if an agent searches for CVE-2024-38077, res.clearCookie, or a specific database migration hash, a dense vector embedder maps those tokens to generic conceptual embeddings for "security vulnerabilities" or "cookie auth", missing the exact symbol.
Conversely, pure lexical search suffers from paraphrase blindness: searching for "how does our system mitigate memory leakage during long conversations" returns zero matches against a function named compactEpisodicHistory().
The solution is Hybrid Search via Reciprocal Rank Fusion (RRF) in SQL.
PostgreSQL Full-Text: Cover Density (`ts_rank_cd`) vs Okapi BM25
In industry blogs, PostgreSQL's full-text search is frequently mislabeled as "BM25". It is important to be precise:
- PostgreSQL
ts_rank_cd: Uses Cover Density Ranking based on term proximity and frequency within document extents. It rewards documents where query terms appear close together, but does not implement BM25's non-linear term saturation ($k_1$) or length normalization ($b$). - Okapi BM25: The established standard in Lucene and Elasticsearch. In PostgreSQL, achieving true Okapi BM25 requires specialized extensions like ParadeDB (
pg_search).
For code-agent retrieval, ts_rank_cd combined with simple dictionaries is remarkably effective because symbol names and identifiers are short, making cover density an excellent proxy for exact matches without requiring external search daemons.
The Production PostgreSQL 16 Hybrid Fusion Query
Here is the actual query executed in production by searchBlobs() in krusch-context-mcp/src/git-engine.js:
/**
* Production PostgreSQL 16 Hybrid Retrieval Query (from krusch-context-mcp git-engine.js)
* Combines pgvector HNSW Cosine Distance with tsvector Cover-Density Ranking
* Evaluated via Reciprocal Rank Fusion (RRF) with Exponential Recency Decay
*/
WITH dense_matches AS (
-- 1. Dense Semantic Nearest Neighbors via HNSW Index
SELECT
b.id,
(1 - (b.embedding <=> $1::vector)) AS dense_sim,
ROW_NUMBER() OVER (ORDER BY (b.embedding <=> $1::vector) ASC) AS dense_rank
FROM blobs b
WHERE b.embedding IS NOT NULL
AND ($2::integer IS NULL OR b.repository_id = $2)
LIMIT $3 * 4
),
lexical_matches AS (
-- 2. Lexical Keyword Matches via tsvector GIN Index (Cover Density)
SELECT
b.id,
ts_rank_cd(b.tsv, plainto_tsquery('simple', $4)) AS bm25_score,
ROW_NUMBER() OVER (ORDER BY ts_rank_cd(b.tsv, plainto_tsquery('simple', $4)) DESC) AS bm25_rank
FROM blobs b
WHERE b.tsv @@ plainto_tsquery('simple', $4)
AND ($2::integer IS NULL OR b.repository_id = $2)
LIMIT $3 * 4
),
fused AS (
-- 3. Reciprocal Rank Fusion (k = 60) Merging Dense & Lexical Strata
SELECT
COALESCE(d.id, l.id) AS id,
(
COALESCE(1.0 / (60.0 + d.dense_rank), 0.0) +
COALESCE(1.0 / (60.0 + l.bm25_rank), 0.0)
) AS rrf_score,
COALESCE(d.dense_sim, 0.0) AS dense_sim,
COALESCE(l.bm25_score, 0.0) AS bm25_score
FROM dense_matches d
FULL OUTER JOIN lexical_matches l ON d.id = l.id
)
SELECT
b.id,
b.repository_id,
r.name AS project,
COALESCE(b.summary, substring(encode(b.content, 'escape') from 1 for 500)) AS summary,
b.storage_mode,
b.last_seen_at,
COALESCE(b.file_name, b.id) AS file_name,
b.file_path,
-- 4. Mathematical Temporal Recency Decay: e^(-0.01 * age_in_days)
(f.rrf_score * exp(-0.01 * EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - COALESCE(b.last_seen_at, b.created_at))) / 86400.0)) AS similarity,
f.dense_sim,
f.bm25_score
FROM fused f
JOIN blobs b ON b.id = f.id
JOIN repositories r ON r.id = b.repository_id
ORDER BY similarity DESC
LIMIT $3;
In krusch-context-mcp's local-first developer runtime, repository partitioning is enforced via b.repository_id = $2. In an enterprise multi-tenant deployment, this exact SQL structure maps to PostgreSQL Row-Level Security (RLS) without requiring query rewrites: CREATE POLICY tenant_isolation ON blobs FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid).
Recency Decay Nuance: In active development and episodic memory, fresh sessions are naturally more relevant than three-week-old transcripts. However, for core utility libraries, database connection pools, and foundational auth helpers, recency decay should be gated or set to zero so timeless primitives are not penalized for stability.
5. Multi-Hop Graph Walks: Bridging Vector Seeds to Dependency Edges
Semantic embeddings surface isolated candidate files. But a coding agent tasked with refactoring an API endpoint cannot work on an island: it requires the route handler, the service it calls, the data models it imports, and the test suite that validates it.
Instead of hoping the embedding model miraculously bundles these disparate files into the top-5 nearest neighbors, krusch-context-mcp extracts dependency imports during indexing and performs a bounded relational graph walk:
CREATE TABLE IF NOT EXISTS code_symbol_edges (
id SERIAL PRIMARY KEY,
repository_id INTEGER REFERENCES repositories(id) ON DELETE CASCADE,
source_path TEXT NOT NULL,
target_path TEXT NOT NULL,
relation VARCHAR(50) NOT NULL, -- 'imports', 'requires', 'references'
symbols TEXT[], -- e.g. ['pool', 'query', 'authMiddleware']
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_edges_lookup ON code_symbol_edges(repository_id, source_path);
src/api/auth-routes.js).
code_symbol_edges for direct imports. The imported controllers and middleware are assigned seed_score * 0.85 and added to the candidate pool.
Set tracks visited paths to prevent circular dependency loops.
6. Re-Ranking & Diversity Pruning: Greedy Jaccard Minimal Cover (MMR Approximation)
In large codebases, the top 20 retrieval results frequently suffer from semantic redundancy: 8 of the top 10 items might be identical logging wrappers, boilerplate error handlers, or repeated test fixtures. Consuming context tokens on repetitive boilerplate degrades LLM reasoning.
While recent information retrieval literature explores LLM-based setwise reranking, production code-agent memory planes require deterministic, sub-millisecond execution. In krusch-context-mcp, we implement a greedy token Jaccard diversity pruning filter (a lightweight lexical approximation of Maximal Marginal Relevance) to suppress near-duplicates without incurring LLM call latencies:
/**
* Greedy Jaccard Minimal Covering Filter (MMR Approximation)
* Suppresses near-duplicate candidates sharing high token Jaccard similarity.
*/
export function selectMinimalCoveringSet(candidates, targetCount = 6, redundancyThreshold = 0.88) {
if (!candidates || candidates.length === 0) return [];
const sorted = [...candidates].sort((a, b) => (b.score || 0) - (a.score || 0));
const selected = [];
for (const item of sorted) {
if (selected.length >= targetCount) break;
let isRedundant = false;
for (const sel of selected) {
// Compute token-level lexical overlap
const overlap = tokenJaccardSimilarity(item.content, sel.content);
if (overlap >= redundancyThreshold) {
isRedundant = true;
break;
}
}
if (!isRedundant) {
selected.push(item);
}
}
return selected;
}
Setting the Jaccard redundancy threshold to 0.88 safely suppresses copy-pasted boilerplate without discarding legitimate method overloads or distinct implementations.
7. Server-Side Token Budget Packing: Preventing Context Truncation
The standard failure mode in client-driven RAG is returning an array of JSON objects to the calling agent and delegating context management to client scripts. When the retrieved documents exceed the LLM's context window, naive string truncation cuts code mid-expression, creating syntax errors and hallucinations.
In our architecture, the retrieval server acts as a Deterministic Token Budget Accumulator:
/**
* Server-Side Greedy Token Budget Accumulator (packTokenBudget)
* Packs ranked candidates under an exact token ceiling with safety margin.
*/
export function packTokenBudget(items, limitTokens = 4000) {
const sorted = [...items].sort((a, b) => b.score - a.score);
let currentTokens = 0;
const packed = [];
for (const item of sorted) {
const header = `### [${item.type.toUpperCase()}] ${item.title} (Score: ${(item.score * 100).toFixed(1)}%)\n`;
const cleanedBody = (item.content || '').trim() + '\n\n';
// Character heuristic with 10% safety buffer (3.6 chars/token)
const itemTokens = Math.ceil((header.length + cleanedBody.length) / 3.6);
// Hard budget guard: prevent overflow even if the very first item is oversized
if (currentTokens + itemTokens > limitTokens) {
if (packed.length === 0) {
// First item exceeds budget on its own: slice safely rather than blowing prompt
const maxChars = Math.floor(limitTokens * 3.6) - header.length;
if (maxChars > 200) {
packed.push(header + cleanedBody.substring(0, maxChars) + '\n...[TRUNCATED TO FIT BUDGET]');
currentTokens = limitTokens;
}
}
break; // Stop packing once budget is reached
}
packed.push(header + cleanedBody);
currentTokens += itemTokens;
}
return {
contextText: packed.join('---\n'),
packedCount: packed.length,
totalTokens: currentTokens
};
}
A common claim in RAG articles is that server-side packing makes context overflow "mathematically impossible." In production, two edge cases must be handled:
- Character Heuristics are Approximations: A character-to-token ratio (e.g. 3.6 chars/token) is an effective estimate for natural language, but dense JSON structures, raw regex patterns, or non-English identifiers tokenize with higher token density under Byte-Pair Encoding (BPE). Production systems must budget a 10–15% safety buffer.
- The First-Item Boundary Bug: Naive implementations that only break when
packed.length > 0will inadvertently pack an oversized first chunk that exceeds the budget on item zero. Explicitly slicing or bounding single large items is essential to prevent prompt overflow.
8. Architectural Trade-Off Matrix: A Steelmanned Comparison
A serious systems evaluation does not compare against unconfigured toy scripts or strawmen. Instead, we compare single-store PostgreSQL (whether self-hosted or managed via platforms like Neon and Supabase) against mature production alternatives: dual-store vector SaaS topologies and dedicated distributed search clusters:
| Dimension | Single-Store PostgreSQL 16 + pgvector (Self-Hosted / Neon / Supabase) | Dual-Store: Relational SQL + Vector SaaS (Qdrant / Pinecone) | Dedicated Distributed Search (Vespa / Elasticsearch / OpenSearch) |
|---|---|---|---|
| State Invariant | Strict ACID in one engine. Vectors and relations mutate atomically with zero sync drift. | Eventual consistency. Requires sync workers, out-of-band queues, and repair jobs. | Search-optimized distributed inverted indexes. Near-real-time ingestion pipeline. |
| Security & Tenancy | Native SQL RLS. Relational tenant policies enforce vector isolation automatically. | Split security models. ACLs must be mirrored across both platforms with drift risk. | Granular document ACLs, but requires dedicated identity integration layer. |
| Lexical Ranking | ts_rank_cd cover density. Fast, zero-dependency, but not Okapi BM25. |
Varies. Pure vector stores often lack sophisticated lexical tokenization. | Full Okapi BM25, customizable stemmers, term saturation ($k_1, b$). |
| Operational Complexity | Lowest. Runs on existing PostgreSQL instance with zero added nodes or services. | Medium. Two database technologies to monitor, backup, upgrade, and secure. | High. Multi-node distributed clusters requiring dedicated search SRE teams. |
| Scale Envelope | RAM-bound (~1M–5M vectors depending on selectivity & write churn). | Tens of millions of vectors with horizontal clustering. | Hundreds of millions to billions of vectors across sharded nodes. |
PostgreSQL single-store is an optimal default for code-agent memory planes under a few million symbols. However, dedicated search engines (Vespa, Elasticsearch, OpenSearch) remain the gold standard when:
- True Okapi BM25 Term Saturation is Required: Cover-density ranking (
ts_rank_cd) lacks document length normalization ($b$) and term frequency saturation ($k_1$). On multi-page prose documents, BM25 outperforms cover density. - Scale Exceeds Available Host RAM (>5M–10M Global Vectors): Dedicated engines implement multi-tier storage, disk-backed inverted indexes, and product quantization (PQ) that allow scaling across dozens of sharded worker nodes without host RAM exhaustion.
- Operational Failure Isolation: Separating search from the primary transactional database guarantees that expensive vector graph walks or search bursts cannot saturate CPU cores needed for transactional writes.
9. Operational Scaling Limits: When to Graduate from PostgreSQL
A senior architect does not treat "1M–5M vectors" as an immutable law. In production, the viability of PostgreSQL + pgvector is governed by three continuous engineering variables:
- RAM Residency vs Index Footprint: 1,024-dimensional float32 vectors consume 4.1 GB per 1M vectors plus graph overhead. When the working set exceeds
shared_buffersand the OS page cache, HNSW traversals that swap pages from NVMe drive latencies from 8ms to 250ms+. - Filter Selectivity: If queries filter by
repository_idwhere each repository contains 2,000–50,000 vectors, the effective search space per query is tiny, allowing single-node Postgres to scale to millions of total vectors effortlessly. Conversely, global unpartitioned ANN scans hit CPU bottlenecks much sooner. - Write Churn & Autovacuum Contention: High-frequency inserts and updates (thousands of vectors/second) trigger aggressive table bloat and WAL volume, saturating autovacuum workers and degrading search latency during index updates.
- Scale Past 5M–10M Global Vectors: When unpartitioned datasets exceed available RAM, distributed vector engines (Qdrant, Vespa) with memory-mapped vector compression and product quantization (PQ) become essential.
- Strict Okapi BM25 Requirements: If document length normalization and complex relevance tuning are hard business requirements, adopting ParadeDB (
pg_search) or a dedicated Elasticsearch / Vespa cluster is required. - Active-Active Multi-Region Deployments: Cross-region low-latency vector search requires specialized distributed vector fabrics that exceed standard single-primary PostgreSQL replication.
10. Empirical Retrieval Evaluation: Directional Smoke-Test Ablations
A serious retrieval claim requires an empirical evaluation protocol, not anecdotal laptop timings. In krusch-context-mcp, retrieval performance is audited via a formal benchmark harness (scripts/eval_accuracy.js) comparing three retrieval strategies:
A note on scientific rigour: The evaluation suites presented below (10 frozen queries on Express, 14 frozen queries on the local monorepo) are diagnostic smoke-test ablations designed to validate a specific architectural hypothesis: Does combining dense semantic vectors with lexical cover-density via RRF resolve exact identifier blindness without degrading conceptual search?
With sample sizes of $n=10$ and $n=14$, these measurements (MRR 0.783 on Express, 0.964 in-corpus) are directional smoke tests rather than large-scale SOTA claims across standard academic corpora like SWE-bench or CodeSearchNet. Furthermore, the in-corpus evaluation was conducted on our own codebase dialect. They are presented here because frozen fixtures and reproducible test scripts (scripts/eval_accuracy.js) are far more informative than unmeasured laptop anecdotes.
- Pure Lexical Search: PostgreSQL
ts_rank_cdcover-density ranking overblobs.tsv. - Pure Dense Vector: Cosine distance on 1,024-d
bge-largeembeddings (blobs.embedding <=> vector). - Hybrid Reciprocal Rank Fusion:
krusch-context-mcp's coresearchBlobs()fusing dense ranks, lexical ranks, and temporal recency decay ($k=60$).
A. Third-Party Codebase Benchmark: `expressjs/express`
To ensure evaluation validity outside our own repositories, we evaluated on the public expressjs/express repository (206 files, 167 indexed blobs, 3,354 extracted code symbols) using 10 frozen queries (5 abstract semantic concepts + 5 exact code identifiers):
| Retrieval Strategy | Recall@1 | Recall@5 | Recall@10 | MRR (Smoke Test) | Code Identifiers R@1 | Semantic Concepts R@1 |
|---|---|---|---|---|---|---|
| Lexical Cover-Density (Postgres) | 1/10 (10.0%) | 1/10 (10.0%) | 1/10 (10.0%) | 0.100 | 1/5 (20.0%) | 0/5 (0.0%) |
| Dense Cosine (bge-large 1024d) | 6/10 (60.0%) | 9/10 (90.0%) | 9/10 (90.0%) | 0.733 | 2/5 (40.0%) | 4/5 (80.0%) |
| Hybrid RRF (searchBlobs) | 7/10 (70.0%) | 9/10 (90.0%) | 9/10 (90.0%) | 0.783 | 3/5 (60.0%) | 4/5 (80.0%) |
B. In-Corpus Architectural Ablation: Local Tooling Monorepo
Across the local repository stack (pg-git, krusch-context-mcp, krusch-cascade-router) indexing 190 content-addressed blobs, 14 frozen ablation queries were evaluated to test dense degradation on exact configuration flags, CVE identifiers, and architecture patterns:
| Retrieval Strategy | Recall@1 | Recall@5 | Recall@10 | MRR (Smoke Test) | Code Identifiers R@1 | Semantic Concepts R@1 |
|---|---|---|---|---|---|---|
| Lexical Cover-Density (Postgres) | 3/14 (21.4%) | 4/14 (28.6%) | 4/14 (28.6%) | 0.238 | 2/6 (33.3%) | 1/8 (12.5%) |
| Dense Cosine (bge-large 1024d) | 11/14 (78.6%) | 14/14 (100.0%) | 14/14 (100.0%) | 0.881 | 4/6 (66.7%) | 7/8 (87.5%) |
| Hybrid RRF (searchBlobs) | 13/14 (92.9%) | 14/14 (100.0%) | 14/14 (100.0%) | 0.964 | 6/6 (100.0%) | 7/8 (87.5%) |
- Dense Models Stumble on Exact Sub-Tokens: In the Express benchmark, querying
res.clearCookie optionscaused Dense Cosine to rank the generalres.cookie.jsahead ofres.clearCookie.js(Rank 2). The lexical scan matched the exact tokenclearCookie, enabling Hybrid RRF to pull the correct target to Rank 1. Similarly, queryingastChunker balanced bracerankedembedding.jsaboveast-chunker.jsunder pure Dense; Hybrid RRF immediately corrected it to Rank 1. - Lexical Fails on Architectural Paraphrase: For natural language architectural queries (e.g. "content negotiation accept header and mime type lookup"), lexical search yielded 0% recall because developers don't write conceptual essays in source comments. Dense Cosine achieved 80–87.5% Recall@1.
- The Hybrid RRF Invariant: Fusing dense and lexical rankings via RRF provides the highest MRR across both corpora (0.783 on Express, 0.964 in-corpus), preserving 100% of dense conceptual reasoning while guaranteeing exact identifier retrieval.
The reported 8.2ms P50 and 21.4ms P95 metrics measure PostgreSQL database query time (HNSW graph traversal + GIN inverted index scan + RRF CTE fusion) on a warm cache across 12,398 indexed symbols. In an end-to-end user query, generating the 1,024-dimensional query embedding via an external API (e.g. OpenRouter / BAAI) typically requires 40ms–120ms. In production RAG systems, embedding model inference—not the database vector lookup—dominates the total latency profile.
11. Conclusion & Open Source Reproducibility
High-reliability AI systems do not require complex, brittle multi-database topologies for code retrieval. By pairing single-store PostgreSQL 16 + pgvector with structural symbol extraction, SQL-native Reciprocal Rank Fusion, dependency graph expansion, and server-side token budget packing, developers achieve state-of-the-art code retrieval within an atomic, ACID-compliant database.
All benchmarks, frozen test fixtures, and source code are open-source and reproducible:
- Open-Source Repository: krusch-context-mcp on GitHub
- Evaluation Harness & Fixtures: docs/EVALS.md & scripts/eval_accuracy.js
- Companion Architecture: Postgres as the State Plane: Why Side Effects Require a Transaction Manager
- Cloud Elasticity Deep Dive: Local-First Core, Hybrid Cloud Elasticity: Decoupling Agent Memory