Retrievers
A retriever takes a question and returns relevant documents. It is the “R” in RAG, and the quality of your whole app depends on it: if the right chunk is not retrieved, the best LLM cannot answer. In this lab you compare seven retrievers on NovaBank documents, and discover why an error code like U30 needs keyword search.
In this session
- Vector store → retriever with as_retriever()
- similarity, MMR and score threshold
- BM25 keyword search for codes and IDs
- Hybrid search with EnsembleRetriever
- MultiQuery, contextual compression, parent documents
- Which retriever when
01The big idea: two kinds of librarian
Think of two helpers in a library. One understands meaning: ask for “my card got stolen” and she brings the lost-card policy. The other matches exact words: ask for “U30” and he finds the page with that code. Each misses what the other finds. Hybrid search asks both and merges their answers.
"Error code UPI-U30 …". BM25 splits on spaces by default, so the word is "upi-u30" and a search for "U30" finds nothing. Fix: BM25Retriever.from_documents(docs, preprocess_func=lambda t: re.findall(r"\w+", t.lower())). The hands-on run below shows both.02Seven retrievers, one interface
| Retriever | How | Use when |
|---|---|---|
similarity as_retriever(search_kwargs={"k": 2}) | top-k nearest vectors | default |
MMR search_type="mmr" | relevant + diverse | results repeat each other |
| score threshold | only chunks above a score | avoid unrelated answers (“pizza recipe”) |
BM25Retriever | keyword ranking | exact words: IDs, error codes, names |
EnsembleRetriever | BM25 + vector, merged | production RAG: best of both |
MultiQueryRetriever | LLM rewrites the question 3 ways | short or vague questions |
ContextualCompressionRetriever | LLM keeps only the useful sentences | long, noisy chunks |
ParentDocumentRetriever | search small chunks, return the big parent | precise search + full context |
03Hands-on: run it here
Similarity, BM25 (default and fixed), hybrid, multi-query and parent-document retrievers on the notebook’s eight bank texts. The numbers are document ids.
import warnings; warnings.filterwarnings("ignore")
from standins import KeywordEmbeddings, RuleModel
from langchain_core.documents import Document
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_community.vectorstores import FAISS
from langchain_community.retrievers import BM25Retriever
from langchain_classic.retrievers import EnsembleRetriever, MultiQueryRetriever, ParentDocumentRetriever
from langchain_classic.storage import InMemoryStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
embeddings = KeywordEmbeddings()
texts = ["Report a lost card within 3 days for zero liability.",
"Card-not-present payments above Rs 5,000 need OTP.",
"Credit card annual fee is Rs 500, waived on spends above Rs 1 lakh.",
"UPI limit for a new payee is Rs 25,000 in the first 24 hours.",
"Never share your UPI PIN with anyone.",
"Error code UPI-U30 means the debit failed at the bank.",
"Video KYC is available from 9 AM to 9 PM.",
"Home loan interest starts at 8.5% per year."]
docs = [Document(page_content=t, metadata={"id": i}) for i, t in enumerate(texts)]
show = lambda ds: [d.metadata["id"] for d in ds]
retriever = FAISS.from_documents(docs, embeddings).as_retriever(search_kwargs={"k": 2})
print("similarity 'my card is stolen' ->", show(retriever.invoke("my card is stolen")))
bm25 = BM25Retriever.from_documents(docs, k=2) # default: splits on spaces only
print("BM25 default 'U30' ->", show(bm25.invoke("U30")), " <- misses: the text has 'UPI-U30' as one word")
import re
bm25 = BM25Retriever.from_documents(docs, k=2, preprocess_func=lambda t: re.findall(r"\w+", t.lower()))
print("BM25 + regex 'U30' ->", show(bm25.invoke("U30")), " <- found doc 5")
hybrid = EnsembleRetriever(retrievers=[bm25, retriever], weights=[0.5, 0.5])
print("hybrid 'what does U30 error mean for my UPI payment?' ->", show(hybrid.invoke("what does U30 error mean for my UPI payment?")))
llm = RuleModel(default="How much money can I transfer to a new payee?\nWhat is the UPI limit for a new beneficiary?\nIs there a limit on the first payment to a new person?")
multi = MultiQueryRetriever.from_llm(retriever=retriever, llm=llm)
print("multi-query 'how much can I send to someone new?' ->", show(multi.invoke("how much can I send to someone new?")))
parent = ParentDocumentRetriever(vectorstore=InMemoryVectorStore(embeddings), docstore=InMemoryStore(),
child_splitter=RecursiveCharacterTextSplitter(chunk_size=60, chunk_overlap=0))
parent.add_documents([Document(" ".join(texts[:3]), metadata={"section": "cards"}), Document(" ".join(texts[3:6]), metadata={"section": "upi"})])
r = parent.invoke("annual fee")
print("parent-doc 'annual fee' ->", r[0].metadata, f"({len(r[0].page_content)} chars: the whole section)")KeywordEmbeddings in standins.py) stands in for MiniLM, so every output below is real, but the scores are smaller than MiniLM’s. In Colab use HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2").04Hands-on: Ajeetkumar’s Colab notebook
groq_key with your free key from console.groq.com, switch on Notebook access, then run the cells from the top.05Practice tasks
- Build a hybrid retriever on a PDF and compare it with pure vector search on 5 questions.
- Print the questions MultiQueryRetriever generates for “cheap phone with good camera”.
- Try different
weightsin EnsembleRetriever.
Try these in the same Colab notebook after you finish the lab. Change one thing at a time and look at the output again.
06Summary
- A retriever: question in, Documents out. Any vector store →
.as_retriever(). - MMR for diversity, score threshold to drop unrelated chunks.
- BM25 for exact words; check its tokenizer for codes like UPI-U30.
- EnsembleRetriever (hybrid) is the production default.
- MultiQuery, compression and parent-document retrievers fix vague questions, noisy chunks and missing context.
07Quick quiz
1. Best retriever for an exact error code like U30?
2. What does EnsembleRetriever combine?
3. Users ask short, vague questions. Which helps?
4. ParentDocumentRetriever returns…
5. Why did default BM25 miss "U30"?