24 Aug 2026 · 10 min read
Implementing Production Hybrid Search: BM25, Embeddings, and Reciprocal Rank Fusion
Why semantic search misses exact matches, keyword search misses intent, and how to combine them mathematically with RRF.
When developers build Retrieval-Augmented Generation (RAG) or search interfaces, they frequently choose between two extremes: traditional full-text keyword indexing (like BM25 or Postgres `tsvector`) or dense vector search (using cosine similarity on OpenAI or Cohere embeddings).
Both approaches have distinct structural blind spots. Pure vector search struggles with acronyms, specific part numbers, legal clause citations, and misspelled entity names. Pure keyword search fails when users describe a concept without using the exact matching vocabulary. Hybrid search bridges this divide by querying both systems in parallel and merging the results.
Comparing the search paradigms
- Sparse Search (BM25)
- Token-frequency matching with term frequency-inverse document frequency weighting. Exceptional on exact phrases, codes, and identifiers.
- Dense Search (Vectors)
- High-dimensional geometric distance in semantic embedding space. Exceptional on conceptual similarity, synonyms, and multilingual intent.
- Hybrid + RRF
- Executes both queries simultaneously and blends rank scores using reciprocal rank algorithms without score scale mismatches.
The mathematics of Reciprocal Rank Fusion (RRF)
The primary challenge in merging dense and sparse results is that raw score magnitudes are not directly comparable: BM25 outputs unbounded positive floats, while cosine similarity yields values between -1 and 1. Normalizing these raw scores is fragile and sensitive to document length outliers.
Reciprocal Rank Fusion solves this by ignoring raw score values entirely and operating purely on the rank position $r(d)$ of each document within each result set using a constant smoothing parameter $k$ (typically 60):
- 01Fetch top-50 results from BM25 sparse index and rank them from 1 to 50.
- 02Fetch top-50 results from dense vector index and rank them from 1 to 50.
- 03Compute the unified score for every document: RRF_Score = SUM(1 / (k + rank_i)).
- 04Sort the merged candidate pool by RRF score and send the top 20 documents to a cross-encoder re-ranker.
RRF eliminates the need to calibrate arbitrary weight parameters between keyword and semantic search scores.
Production implementation recommendations
- Store both vector embeddings and full-text search tsvectors in the same database row (e.g. PostgreSQL with `pgvector` and `pg_trgm`).
- Apply metadata filtering (tenant ID, creation date, access permissions) before executing the vector distance calculation.
- Add a lightweight cross-encoder re-ranking step over the merged RRF top-k candidates to ensure strict context relevance before context injection.
- Log zero-result and low-confidence queries into an analytics queue to identify content gaps in your documentation index.
Written by
OneScript Studio
Software, AI & Digital Solutions for Businesses We publish what we learn building software for businesses.