GenAI Course · Module 9 · Session 35

Retrievers & Re-Ranking

⏱ 14 min read🎯 Advanced🖼 3 diagrams🧪 1 live demo🐍 Python code

Basic vector search gets you started, but users ask vague, short or keyword-heavy questions. Advanced retrievers fix this. In this session, let us see six techniques that make retrieval much more reliable, and calculate hybrid search scores by hand.

In this session

  • Multi-query: ask the question several ways
  • Multi-vector and parent-document retrievers
  • Self-query: let the LLM build metadata filters
  • Contextual compression
  • Hybrid search with BM25 + dense, merged by RRF (real numbers)
  • Re-ranking with cross-encoders

01Multi-query retriever

A short question like “FD early exit?” may miss chunks that use different words. A multi-query retriever asks the LLM to rewrite the question in several ways, searches with each, and merges the results.

Multi-query retrieval“FD early exit?”original question“Penalty for breaking a fixed deposit”rewrite 1“Premature withdrawal of FD rules”rewrite 2“Can I close my FD before maturity?”rewrite 3
More ways of asking → more chances to hit the right chunk.
multi_query.py
from langchain.retrievers.multi_query import MultiQueryRetriever

mq = MultiQueryRetriever.from_llm(retriever=db.as_retriever(), llm=llm)   # db and llm from Session 31
docs = mq.invoke("FD early exit?")

02Multi-vector, parent-document and self-query

Demo · Self-query in action

03Contextual compression

Retrieved chunks often contain only one useful sentence. Contextual compression trims each chunk to the parts relevant to the question (using an LLM or an embedding filter) before sending it to the generator. Fewer tokens, less noise, lower cost.

Before compression• full chunk: 600 tokens• eligibility, rates, withdrawal…• only 1 sentence is relevantAfter compression• ~40 tokens• “Breaking an FD early reduces• the rate by 1%.”
Compression keeps only what answers the question.

04Hybrid search: BM25 + dense

Dense (vector) search understands meaning but can miss exact terms like product codes or section numbers. BM25 keyword search is great at exact terms but misses synonyms. Hybrid search runs both and merges the rankings, often with Reciprocal Rank Fusion (RRF):

RRF score(doc) = Σ 1 / (k + rank)   with k = 60 by convention
rrf.py
def rrf(rankings, k=60):
    scores = {}
    for ranking in rankings:
        for rank, doc in enumerate(ranking, start=1):
            scores[doc] = scores.get(doc, 0) + 1 / (k + rank)
    return sorted(scores.items(), key=lambda x: -x[1])

bm25  = ["FD_rules_p4", "Savings_p2", "FD_faq_p1"]          # keyword search ranking
dense = ["FD_faq_p1", "FD_rules_p4", "Loan_p9"]             # vector search ranking
for doc, s in rrf([bm25, dense]):
    print(f"{doc:12s} {s:.4f}")
FD_rules_p4 0.0325 FD_faq_p1 0.0323 Savings_p2 0.0161 Loan_p9 0.0159

FD_rules_p4 was 1st in BM25 and 2nd in dense, so it wins overall. Documents found by both methods rise to the top. In LangChain:

hybrid.py
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever

bm25 = BM25Retriever.from_documents(chunks); bm25.k = 5
dense = db.as_retriever(search_kwargs={"k": 5})
hybrid = EnsembleRetriever(retrievers=[bm25, dense], weights=[0.5, 0.5])   # weighted RRF
docs = hybrid.invoke("Section 3.2 premature withdrawal")
Version note: import paths change between LangChain releases. In LangChain 1.0 and later, several retrievers (including MultiQueryRetriever and EnsembleRetriever) moved to the langchain-classic package. Check the docs for your installed version.

05Re-ranking

Retrievers are fast but rough: they embed the question and the chunk separately. A cross-encoder re-ranker reads the question and each chunk together and gives a much more accurate relevance score. The usual pattern: retrieve 20–50 candidates quickly, re-rank, keep the top 3–5.

Retrievetop 30, fastRe-rankcross-encoderKeep top 4best contextLLManswer
Two-stage retrieval: fast and wide first, then slow and precise.
Re-rankerTypeNotes
Cohere RerankAPIstrong, multilingual, easy to add
BGE re-rankers (BAAI)open sourcebge-reranker-base / large / v2-m3 (multilingual)
ms-marco cross-encodersopen sourcesmall and fast English models
rerank.py
# pip install sentence-transformers
from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-base")
question = "Penalty for breaking an FD early?"
candidates = [d.page_content for d in hybrid.invoke(question)]
scores = reranker.predict([(question, c) for c in candidates])
top4 = [c for _, c in sorted(zip(scores, candidates), reverse=True)[:4]]

06Summary

07Quick quiz

1. Hybrid search combines…

2. In RRF, a document ranked 1st in both lists gets…

3. A cross-encoder re-ranker is more accurate because it…

4. Which retriever turns “2026 retail policies” into a metadata filter?

5. Typical two-stage setup?

08References

Created by Ajeetkumar