RAG courseLesson 3 of 6
RAG course · Lesson 3 of 6
Hybrid Search and Reranking for RAG, Explained with Code
Why RAG systems combine keyword search (BM25) with semantic search, how reciprocal rank fusion merges them, and what rerankers add, with runnable Python.
On this page
Retrieval decides what the model gets to read, so it decides most of a RAG system’s quality. There are two main ways to search text, and they fail in opposite ways. Keyword search finds exact words but misses different wording. Semantic search finds meaning but can miss exact terms such as error codes, product numbers and names. Hybrid search runs both and merges the results, and a reranker then puts the best few at the top.
Keyword search with BM25
BM25 is the standard keyword ranking method used by most search engines. For each query word found in a chunk, it adds a score that is higher when:
- the word is rare across all chunks (an error code matters more than “the”),
- the word appears more often in the chunk, with diminishing returns,
- the chunk is shorter than average, so a match is more focused.
It is fast, needs no model, and is excellent for exact terms.
Semantic search with embeddings
Semantic search compares embeddings, so “money back” can match “refunds” even with no shared words. It handles paraphrases and natural questions well, but codes and rare names carry little meaning for an embedding model, so they are often missed.
Combining them: reciprocal rank fusion
The two methods produce scores on different scales, so you cannot simply add them. Reciprocal rank fusion (RRF) combines positions instead: each result gets 1 / (k + rank) from each list it appears in, with k usually 60, and the totals decide the final order. Results ranked high by either method rise to the top.
This example runs BM25, a stand-in for semantic scores (hand-set numbers, since no embedding model is downloaded here), and RRF on two questions:
import math, re
from collections import Counter
CHUNKS = [
"Error PAY-317 means the card was declined by the bank.",
"If your payment keeps failing, try another card or contact your bank.",
"Refunds are paid within 14 days of the item reaching our warehouse.",
"Express delivery takes 1 working day and costs 9 dollars.",
"To reset your password, click Forgot password on the sign-in page.",
]
tok = lambda t: re.findall(r"[a-z0-9-]+", t.lower())
docs = [tok(c) for c in CHUNKS]
avg_len = sum(map(len, docs)) / len(docs)
df = Counter(w for d in docs for w in set(d))
def bm25(query, k1=1.5, b=0.75):
"""Keyword score: rewards rare query words that appear in a chunk, adjusted for chunk length."""
scores = []
for d in docs:
tf, s = Counter(d), 0.0
for w in tok(query):
if w in tf:
idf = math.log(1 + (len(docs) - df[w] + 0.5) / (df[w] + 0.5))
s += idf * tf[w] * (k1 + 1) / (tf[w] + k1 * (1 - b + b * len(d) / avg_len))
scores.append(s)
return scores
# Stand-in for embedding similarity: hand-made meaning scores for one query (a real system computes these).
SEMANTIC = {
"what does PAY-317 mean": [0.30, 0.34, 0.05, 0.04, 0.32], # codes carry little meaning for embeddings
"how do I get my money back after a return": [0.10, 0.22, 0.83, 0.12, 0.05],
}
def rank(scores):
"""Positions of chunks from best to worst, leaving out chunks with no match at all."""
return [i for i in sorted(range(len(scores)), key=lambda i: -scores[i]) if scores[i] > 0]
def rrf(*rankings, k=60):
"""Reciprocal rank fusion: combine rankings without comparing their raw scores."""
fused = Counter()
for r in rankings:
for position, i in enumerate(r, 1):
fused[i] += 1 / (k + position)
return [i for i, _ in fused.most_common()]
for q in SEMANTIC:
kw, sem = rank(bm25(q)), rank(SEMANTIC[q])
print(f"Q: {q}")
for name, order in [("keyword", kw), ("semantic", sem), ("hybrid", rrf(kw, sem))]:
print(f" {name:8} top hit: {CHUNKS[order[0]] if order else '(no match)'}")
Q: what does PAY-317 mean
keyword top hit: Error PAY-317 means the card was declined by the bank.
semantic top hit: If your payment keeps failing, try another card or contact your bank.
hybrid top hit: Error PAY-317 means the card was declined by the bank.
Q: how do I get my money back after a return
keyword top hit: (no match)
semantic top hit: Refunds are paid within 14 days of the item reaching our warehouse.
hybrid top hit: Refunds are paid within 14 days of the item reaching our warehouse.
Keyword search alone fails the second question (no shared words), and semantic search alone fails the first (the code carries little meaning). Hybrid search gets both right. This pattern, exact terms plus natural wording, is very common in real support and documentation search.
Reranking
The first search is fast but rough. A reranker is a slower, more accurate model, usually a cross-encoder, that reads the question and each candidate passage together and scores how well the passage answers it. A typical setup:
- Hybrid search returns the top 20 to 50 candidates quickly.
- The reranker scores each candidate against the question.
- Only the top 3 to 5 go into the prompt.
Reranking usually improves answer quality noticeably, at the cost of some extra time per question. Measure that trade-off on your own test set.
Other ways to improve retrieval
- Metadata filters: restrict by product, language, date or user permissions before ranking.
- Query rewriting: turn a vague or multi-part question into clearer search queries.
- Minimum scores: drop weak matches instead of always sending the top k.
- Better chunks: often the biggest win. See chunking strategies.
Common mistakes
- Using only vector search for content full of codes, IDs and names.
- Adding raw scores from different methods instead of fusing ranks.
- Sending too many passages to the model instead of reranking to a few good ones.
- Tuning without a test set, so you cannot tell whether a change helped.
Key takeaway
Keyword search (BM25) finds exact terms; semantic search finds meaning. Hybrid search runs both and merges them with reciprocal rank fusion, and a reranker picks the best few passages for the prompt. Next: measuring all of this with evaluating RAG.
Progress is saved in this browser only. No account needed.

