Hybrid Search: Dense Vectors Meet Sparse Lexical BM25
Pure vector search is blind to exact alphanumeric identifiers — error codes, invoice numbers, variable names — failing silently on the queries engineers care most about. This article implements hybrid search in PostgreSQL by fusing dense pgvector similarity with sparse BM25 full-text search via Reciprocal Rank Fusion (RRF) SQL CTEs.
Full-Stack AI Architecture: From Streaming Foundations to Resilient Agentic Systems in Node.js & React
Hybrid Search: Dense Vectors Meet Sparse Lexical BM25
A user types "ERR_CONTEXT_OVERFLOW 0x4A2F" into the Workspace knowledge search. Dense vector search finds documents about "error handling" and "context management" — conceptually related, but none containing that specific error code. The exact 12-character string the user is debugging is invisible to semantic embeddings. This failure is silent: the system returns confident results that simply miss the answer.
This article implements hybrid search entirely inside PostgreSQL: dense pgvector ANN similarity fused with sparse BM25 full-text search via Reciprocal Rank Fusion (RRF) CTEs. No additional search infrastructure, no Elasticsearch, no synchronisation hazard — one database, one query, two retrieval signals merged deterministically.
Who this is for: Engineers who have deployed vector search and noticed it fails on exact identifiers, version strings, error codes, and domain-specific terminology. After this article, hybrid search is a single SQL query away.
1. The Two Retrieval Signals and Their Failure Modes
| Signal | Strength | Failure Mode |
|---|---|---|
| Dense (Vector) | Semantic intent, paraphrase, concept matching | Exact alphanumeric strings, version numbers, error codes |
| Sparse (BM25) | Exact keyword matching, domain-specific terms | Synonyms, paraphrasing, natural language intent |
2. Full-Text Search Setup in PostgreSQL
PostgreSQL's built-in tsvector / tsquery provides BM25-style ranking via ts_rank_cd. It indexes lexemes (word roots after stemming) and supports proximity queries.
The GENERATED ALWAYS AS ... STORED column means PostgreSQL automatically maintains the fts_vector on every INSERT/UPDATE — zero application-level maintenance.
3. Reciprocal Rank Fusion SQL
3.1 Why Rank Position, Not Raw Scores
Dense similarity scores (0.0–1.0) and BM25 scores (0.0–unbounded) live on completely different scales. You cannot simply add them. RRF normalises both by rank position:
Where k = 60 is a smoothing constant (prevents top-ranked documents from dominating) and rank(d, query_i) is the document's position in each retrieval list.
3.2 The Complete Hybrid Search Query
3.3 TypeScript Wrapper
4. Tuning RRF for the Workspace
4.1 The k Parameter
The smoothing constant k determines how aggressively high-ranked results dominate. With k = 0, a rank-1 document scores 1.0 and rank-2 scores 0.5 — a 2× gap. With k = 60, rank-1 scores 1/61 ≈ 0.0164 and rank-2 scores 1/62 ≈ 0.0161 — almost equal. Higher k flattens the fusion curve.
| k value | Character | Best for |
|---|---|---|
10 |
Aggressive — top ranks dominate | When one signal is clearly more reliable |
60 |
Balanced — standard RRF | General knowledge retrieval (Workspace default) |
100 |
Conservative — near-uniform fusion | When both signals are equally trustworthy |
4.2 Score Monitoring Query
Summary
| Concept | Rule |
|---|---|
| Dense-only failure | Pure vector search misses exact identifiers; always pair with BM25 in production |
| RRF over score addition | Rank-based fusion avoids cross-scale normalisation problems entirely |
| k = 60 | Standard smoothing constant; prevents any single top-ranked result from dominating |
| Single-DB architecture | FULL OUTER JOIN inside one PostgreSQL query — no synchronisation, no second system |
| Maintained fts_vector | GENERATED ALWAYS AS ... STORED column auto-updates with no application code |
What's Next
Part 11 adds a second retrieval stage: after 50 candidates from hybrid RRF, a Cross-Encoder reranker computes full attention between the query and each candidate, selecting the 5 most semantically precise results — without violating P99 latency budgets.
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.