noteSep 2026·1 min read
Hybrid search in practice: BM25 + dense + reranking
Why dense-only retrieval fails on identifiers and jargon, and how fusing sparse and dense results before a cross-encoder fixes it.
Dense embeddings are great at paraphrase, weak at exact tokens — product codes, CVE IDs, brand names. BM25 is the opposite.
Reciprocal Rank Fusion
A simple, score-free way to merge ranked lists:
def rrf(rankings, k=60):
scores = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
return sorted(scores, key=scores.get, reverse=True)
Then a cross-encoder reranks the fused top-k — slower per pair, but it reads query and passage together.
This portfolio's own "Ask AI" chatbot uses exactly this: pgvector cosine search + Postgres full-text, fused with RRF.
#rag#retrieval#reranking