← learning log
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