← Back to list

The Document Was There. Search Never Found It.

Hybrid retrieval is not a trend. It is what happens when you stop pretending one index can read minds and match error codes at the same…

Sohail Rashid · 2026-06-18 20:44 · 4 claps · 7.7 min read
#search #ai #hybrid-search #bm25 #dense-vector
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval EVAL · Evaluation & Benchmarks AI · AI · General

The Document Was There. Search Never Found It.

Hybrid retrieval is not a trend. It is what happens when you stop pretending one index can read minds and match error codes at the same time.

The hybrid retrieval pipeline: BM25, dense vectors, and learned sparse streams fusing into a single ranked result.

The hybrid retrieval pipeline: BM25, dense vectors, and learned sparse streams fusing into a single ranked result.

QUERY >> "how to rotate JWT signing keys without downtime"

Your BM25 index tokenizes the string. It scores by term overlap. It returns a wall of generic authentication docs.

Nothing in the corpus literally contains rotate, JWT, signing keys, and without downtime in the same passage.

The right runbook exists. It talks about key rollover, zero-downtime cutover, and kid header rotation. Lexical retrieval never surfaces it.

This is not a frontend bug. It is a recall failure in the retrieval layer. The user typed a question. The system answered a different question.

The demo that lies to you

The fix is not “replace search with an LLM.”

Chat wrappers add latency, hallucination risk, and operational cost. They also hide the failure: a model can sound confident while citing the wrong runbook. Production systems do not win on confidence. They win on surfacing the correct document in the first three results, every time, inside a latency budget.

What actually ships is a retrieval pipeline:

  1. Multiple first-stage retrievers, each with a different blind spot
  2. A fusion step that merges incompatible score spaces without pretending they mean the same thing
  3. An optional second-stage reranker that optimizes top-k precision on a shortlist

This post is the architecture behind that pipeline. Documentation search, support tickets, enterprise knowledge bases, RAG context retrieval: if users type natural language against a large corpus, the same physics apply.

Two retrievers, two different brains

Think of retrieval as hiring two specialists who disagree on what “similar” means.

Specialist A: BM25 (lexical, sparse)

BM25 is the workhorse. Inverted index. Term frequency. Inverse document frequency. Fast, deterministic, debuggable. When an on-call engineer asks trace-id: a8f3c2, BM25 is the reason the answer comes back in milliseconds.

Strength: exact identifiers. Error codes. API names. RFC section numbers. UUIDs. Anything where query terms appear verbatim in the corpus.

Failure mode: vocabulary mismatch. Query "debug intermittent 503 errors" does not match a document titled "Handling gateway timeouts and upstream unavailability" because there are no shared tokens. Zero recall on the document the user needed.

Specialist B: Dense vectors (semantic, bi-encoder)

Embed queries and documents with a bi-encoder (text-embedding-3-large, bge-m3, e5-large-v2). Retrieve via approximate nearest neighbor search (HNSW, IVF, DiskANN). Cosine similarity captures meaning independent of wording.

Strength: paraphrase and conceptual intent. Exactly where BM25 breaks.

Failure mode: exact-match precision. Query "RFC 7519 section 4.1.3" may return any JWT claims doc because embeddings cluster by topic, not by citation. Ask for a specific trace ID and you get conceptually similar observability docs, not the log line.

The vocabulary gap and how SPLADE bridges it without any manual synonym rules.

The vocabulary gap and how SPLADE bridges it without any manual synonym rules.

The production conclusion

Neither specialist is sufficient alone. BEIR, MS MARCO, and production telemetry agree: hybrid retrieval (both in parallel, fused) beats either method in isolation on nDCG@10 and recall@k.

The era of AI did not obsolete search infrastructure. It exposed that search was always a layered system pretending to be a single algorithm.

The pipeline (index time vs query time)

A production stack indexes each document in multiple representations and queries them concurrently.

Each retriever returns top_k candidates (typically 50–100). Downstream consumers (search UI, RAG builder, recommendation API) never see individual retriever output. They see the fused, optionally reranked result set.

Parallel first-stage retrievers → RRF fusion → optional cross-encoder rerank → top-k results

Parallel first-stage retrievers → RRF fusion → optional cross-encoder rerank → top-k results

Index time

For each document d:

index_bm25(d)    → inverted index (Elasticsearch, OpenSearch, Tantivy)
index_dense(d)   → float[D] embedding in vector DB
index_sparse(d)  → sparse float[V] (optional, SPLADE output)

All indexes point to the same document ID. Updates must be atomic. A document deleted from BM25 but still in the vector index creates ghost results in fusion. Those are brutal to debug in production.

Query time

query q:
  results_bm25   = bm25.search(q, k=50)
  results_dense  = ann.search(embed(q), k=50)
  results_sparse = splade.search(q, k=50)     # optional third path
  fused          = rrf(results_bm25, results_dense, results_sparse)
  final          = reranker(q, fused[:100])   # optional second stage
  return final[:10]

Latency budget for a typical stack: 15–40ms first-stage retrieval, 50–200ms reranking. Parallel retriever execution keeps first-stage latency bounded by the slowest path, not the sum.

RRF: fuse ranks, not scores

The hard problem in hybrid search is score incompatibility. BM25 scores are unbounded positive reals. Cosine similarity lives in [0, 1]. A naive weighted blend:

final_score = α · bm25_score + (1-α) · cosine_score

This fails in production because the scales are not commensurable. Tuning α per query type does not generalize.

Reciprocal Rank Fusion (RRF) operates on rank positions only:

RRF_score(d) = Σ  1 / (k + rank_i(d))

rank_i(d) is document d's rank in retriever i's list. k is a smoothing constant (default 60, from Cormack et al.).

Why RRF became the production default:

  • No score normalization. Ranks are unitless.
  • Minimal tuning. k=60 works across domains. Bias toward precision: 30–50. Bias toward recall: 70–100.
  • Rewards consensus. Documents that rank highly across multiple retrievers accumulate score. Single-list outliers still surface but do not dominate.

OpenSearch benchmarks show hybrid + RRF beating pure BM25 or pure neural retrieval on every evaluated dataset (NFCorpus, FIQA, Quora, SciDocs). Native RRF now ships in Elasticsearch 8.8+, OpenSearch 2.19+, Qdrant, Weaviate, and Azure DocumentDB: one round-trip, no client-side fusion code.

SPLADE: the bridge between brains

BM25 + dense vectors cover the two dominant failure modes. A third retriever addresses the gap: queries where vocabulary differs but the user still expects specific lexical signals in results.

SPLADE (Sparse Lexical and Expansion) passes text through a transformer with an MLM head, applies log-saturation and max-pooling, and outputs a sparse vector over the full vocabulary (~30k dimensions, ~200 non-zero values). Unlike BM25, it expands terms automatically:

query:  "k8s pod keeps OOMKilling"
SPLADE: {k8s: 2.1, pod: 1.8, OOM: 2.4, memory: 1.6, limit: 1.4, eviction: 1.2, ...}

No synonym dictionary. No query-rewrite rules. The model learned associations from training pairs.

Operationally:

  • Inverted-index compatible. Same retrieval infrastructure as BM25.
  • Debuggable. On-call can inspect which expanded terms triggered a match.
  • Measurable. Fine-tuned SPLADE achieves roughly 29% nDCG@10 improvement over BM25 on domain benchmarks. Faire reported recovering about 20% of queries that both BM25 and dense retrieval missed entirely.

Tradeoff: domain fine-tuning. Off-the-shelf SPLADE works for general text. Legal, medical, internal platform docs: fine-tune on query-document pairs from your search logs.

Second stage: the editor’s desk

First-stage retrieval optimizes recall@k: get the right document into the candidate set somewhere.

Reranking optimizes precision@k: put it at position 1.

After RRF produces ~50–100 candidates, a cross-encoder (bge-reranker-v2-m3, cohere-rerank-v3, mxbai-rerank-v2) scores each (query, document) pair with full cross-attention. Re-sort. Return top-n.

Measured impact: +15–40% nDCG@10 on complex queries, at +50–200ms latency. Batch inference and GPU acceleration bring reranking within SLO for most search APIs.

A query flowing through normalize → parallel retrieval → RRF → rerank → output

A query flowing through normalize → parallel retrieval → RRF → rerank → output

Skip reranking when:

  • Query distribution is dominated by exact identifier lookups
  • Latency SLO is sub-50ms with no GPU budget
  • Corpus is small (<10k docs) and first-stage recall is already near-perfect

Add reranking when:

  • Multi-constraint natural language queries are common
  • RAG downstream quality depends on top-3 context accuracy
  • Search is a primary product surface

Each layer covers the systematic failure mode of the layers below it

Each layer covers the systematic failure mode of the layers below it

The reranker is a precision layer on top, not a replacement for good first-stage recall.

What is shipping next (2025–2026)

The hybrid + RRF + rerank stack is the baseline. These are additive upgrades.

LESER (LLM-driven query expansion) fine-tunes an LLM with search-engine feedback as reinforcement signal. Expansions align with corpus content and platform constraints, unlike static synonym files. Online A/B tests report measurable CTR and relevance gains at scale.

Hint-augmented reranking decomposes comparative queries (“best,” “most reliable,” “recommended approach for”) into structured attribute hints via LLM, then transfers hints to a lightweight reranker. ACL 2025: +10.9 MAP, +5.9 MRR over baselines without full LLM inference per query.

ColBERT / late interaction computes token-level similarity between query and document embeddings. Middle ground between bi-encoders and cross-encoders on accuracy and latency. Useful as a rescorer on the RRF shortlist.

Multimodal indexes add image or code embeddings as additional RRF paths. Relevant for design asset search, diagram retrieval, code+doc hybrid corpora.

Field manual: order of operations

  1. Instrument failure modes first. Log zero-result queries, low-CTR queries, RAG context misses. Categorize: vocabulary mismatch vs identifier lookup vs multi-constraint intent. Your data picks the retrievers.
  2. Start with BM25 + dense vectors + RRF. Dual-index the corpus. Fuse with k=60. Native on every major platform. Typically cuts zero-result rate by 30–50% with no model training.
  3. Do not go vector-only. Corpora with identifiers, codes, version numbers, or internal naming conventions will regress. On-call will notice immediately.
  4. Add learned sparse when vocabulary mismatch dominates failure logs. SPLADE closes the synonym gap without manual query-rewrite rules.
  5. Add cross-encoder reranking when top-3 precision drives downstream quality. Measure nDCG@10 and MRR on a held-out query set before and after.
  6. Keep indexes synchronized. Stale embeddings beside updated BM25 indexes produce fusion artifacts. Silent quality regression. Easy to miss.

Closing

Teams that treat retrieval as a single algorithm (keywords or vectors) keep shipping systems that miss the right document.

Teams that treat it as layered infrastructure (retrieve broadly, fuse by rank, rerank precisely) ship search that works for natural-language queries.

Your LLM chatbot might win the demo.

Production wins when the runbook is rank 1, not rank 47, and the user never had to ask twice.

Cross-encoder reranking: the correct runbook promoted from rank 2 to rank 1.

Cross-encoder reranking: the correct runbook promoted from rank 2 to rank 1.

References

  1. Microsoft Azure DocumentDB — Hybrid Search: Combining BM25 and Vector Retrieval (2026)
  2. Redis — Hybrid Search Benefits: Why RAG Systems Need Both Methods
  3. Digital Applied — Hybrid Search: BM25, Vector & Reranking Reference 2026
  4. Jatin Bansal — Hybrid Search: BM25 Meets Dense Vectors
  5. OpenSearch — Introducing Reciprocal Rank Fusion for Hybrid Search
  6. Salfati Group — Semantic Search for Enterprise: The 2025 Implementation Guide
  7. OneUptime — Re-Ranking in Production RAG Systems (2026)
  8. Particula Tech — RAG Reranking: When It Actually Improves Retrieval
  9. Qdrant — Fine-Tuning Sparse Embeddings for E-Commerce Search
  10. Faire Engineering — Beyond BM25 and Dense Embeddings: SPLADE at Faire
  11. ACL Anthology — Hint-Augmented Re-ranking: LLM-Based Query Decomposition (2025)
  12. arXiv — LESER: Learning to Expand via Search Engine-feedback Reinforcement (2025)
  13. Aaron Tay — Can Semantic Search Be More Interpretable? ColBERT and SPLADE
  14. Towards AI — Sparse Vectors for Hybrid Retrieval with Qdrant

메타데이터
post_id
a06b05754f5a
slug
the-document-was-there-search-never-found-it-a06b05754f5a
url
https://medium.com/@sohail.ra5/the-document-was-there-search-never-found-it-a06b05754f5a
canonical_url
https://medium.com/@sohail.ra5/the-document-was-there-search-never-found-it-a06b05754f5a
author_url
https://medium.com/@sohail.ra5
status
ok
fetched_at
2026-07-25 21:11:06