Hybrid Search and Reranking: Better RAG Retrieval

Photo by xmodulo on flickr
Hybrid search runs a lexical retriever, usually BM25, and a dense vector retriever against the same document store, then merges the two ranked lists into one. It exists because vector search alone misses exact matches like product codes and rare acronyms, while BM25 alone misses paraphrased or conceptual queries, so combining both recovers the strengths of each.
BM25 scores and vector cosine similarities live on completely different scales and distributions, so averaging them directly tends to favor whichever retriever happens to output larger numbers. Reciprocal rank fusion looks only at each document's rank position in every list, which sidesteps score normalization entirely and requires no tuning.
Production reports and benchmark studies commonly show NDCG at 10 gains of five to fifteen points after adding a cross-encoder reranker, with larger gains on lexically hard datasets. The typical pattern is retrieving fifty to one hundred candidates with hybrid search, then reranking down to the top eight to fifteen passages before generation.
You can, and it will usually beat vector search alone, but hybrid search still ranks documents using relatively shallow signals such as term overlap and embedding distance. A reranker adds a joint query-passage relevance score that catches cases hybrid search misses, at the cost of extra latency on the narrowed candidate set.
Build a small labeled set of queries with the correct source passage marked for each one, then measure recall at k and NDCG before touching the generation prompt. If the correct passage never appears in the top k retrieved results, no amount of prompt engineering will fix a wrong answer, because the retriever never surfaced the right context.

Photo by xmodulo on flickr
Most RAG systems ship with a single retriever: a vector database that turns the query into an embedding and finds the nearest chunks by cosine similarity. It works fine for paraphrased, conceptual questions, and it falls apart the moment a user types an exact product code, an error message, or a rare acronym that never appeared anywhere near similar text during embedding training.
This post walks through the two changes that consistently move retrieval quality the most in production RAG: fusing lexical search with vector search using reciprocal rank fusion, and adding a cross-encoder reranking pass before the final context ever reaches the language model.
Dense embeddings are trained to capture meaning, which is exactly why they miss exact matches. A vector model may place the string of a specific part number close to unrelated part numbers, because it has learned that identifiers in general are similar to each other, not that this identifier is unique.
Lexical search, in the form of BM25, handles exactly these cases because it scores documents on literal term overlap, weighted by how rare each term is across the whole collection. Running both retrievers side by side and combining their outputs recovers the strengths of each without inheriting the weaknesses of either.
Keep BM25 and vector search as two entirely independent retrieval calls against the same document store. Do not try to average their raw scores. They live on different scales, and naive averaging quietly favors whichever retriever happens to produce larger numbers.
Reciprocal rank fusion, or RRF, is the standard answer to the score-incompatibility problem. Instead of combining raw relevance scores, RRF only looks at each document's rank position in every list it appears in. A document ranked first in the BM25 list and third in the vector list gets a combined score built purely from those two rank positions.
The formula sums one divided by a constant plus the rank, across every list a document appears in. The constant, usually set to sixty, dampens the influence of documents that only barely made it into a list. Documents that show up near the top of both lists rise to the top of the fused ranking, which is exactly the behavior you want from a retriever feeding an LLM.
| Signal | Strength | Weakness |
|---|---|---|
| BM25 (lexical) | Exact terms, codes, rare acronyms | No understanding of meaning or paraphrase |
| Vector search (dense) | Paraphrase, synonyms, conceptual queries | Loses exact-match precision on identifiers |
| RRF fusion | Captures both signals without score tuning | Still needs a reranker for final precision |
The implementation is short enough to write by hand even if your vector database or search engine already offers a built-in hybrid query. Run both retrievers against the same query, collect their ranked document lists, then merge with the RRF formula below.
function reciprocalRankFusion(bm25Ranked, vectorRanked, k = 60) {
const scores = new Map();
const addRanks = (rankedList) => {
rankedList.forEach((doc, index) => {
const rank = index + 1;
const contribution = 1 / (k + rank);
scores.set(doc.id, (scores.get(doc.id) || 0) + contribution);
});
};
addRanks(bm25Ranked);
addRanks(vectorRanked);
return Array.from(scores.entries())
.sort((a, b) => b[1] - a[1])
.map(([id, score]) => ({ id, score }));
}Notice that the fusion function never looks at the raw BM25 score or the raw cosine similarity, only at list position. That is the entire point: rank-based fusion sidesteps the need to normalize two incomparable scoring systems onto the same scale.
RRF assumes both input lists are reasonably sized, typically the top fifty to one hundred results per retriever. If you fuse only the top five from each list, ties and near-ties dominate the output and the fusion adds little value over either retriever alone.
Fusion gets you a better candidate set, but it still ranks documents using two relatively shallow signals: term overlap and vector distance. A cross-encoder reranker closes that gap by jointly encoding the query and each candidate passage through a single transformer pass, producing one relevance score per pair instead of comparing two independently computed representations.
Because the reranker attends over the query and passage together, it can catch relationships that neither BM25 nor a vector similarity score can see, such as a passage that mentions all the right keywords but answers a completely different question. That precision comes at a real latency cost, which is why reranking always runs on a narrowed candidate set, never on the full corpus.
Open-source cross-encoder rerankers such as BGE-reranker and Jina Reranker run comfortably on a single GPU and are a reasonable default if you want to avoid a per-query API cost. Hosted options like Cohere Rerank trade a small fee for zero infrastructure to maintain.
Teams frequently jump straight to tweaking prompts when a RAG answer is wrong, without first checking whether the retriever ever surfaced the right passage. Separating retrieval evaluation from generation evaluation is the single most useful debugging habit in RAG work.
If recall at k is low, no amount of reranking or prompt engineering fixes the problem, because the right passage was never in the candidate set to begin with. If recall is high but NDCG is low, that is precisely the situation a reranker solves.
A pragmatic hybrid search and reranking pipeline looks like this: run BM25 and vector search in parallel against the same chunk store, fuse the two ranked lists with reciprocal rank fusion, take the top candidates from the fused list, rerank those with a cross-encoder, and pass only the final handful of passages to the generation model.
Each stage narrows the candidate set while raising precision, which keeps the expensive cross-encoder pass fast and keeps the context window handed to the LLM small and relevant rather than padded with marginal matches. This layered design also makes the pipeline easier to debug in isolation, since a regression in final answer quality can usually be traced back to one specific stage rather than to the system as a whole.
Instrument recall at k and NDCG as part of your CI pipeline whenever the embedding model, chunking strategy, or reranker changes. Retrieval quality regressions are silent until a user notices a wrong answer, so treat retrieval metrics the same way you treat test coverage.