LangChain Hands-on · Module 2 · Lab 10

Retrievers

⏱ 20 min + hands-on🎯 Beginner🖼 1 diagram🧪 2 live demos🐍 Python code

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.

EnsembleRetrieverhybrid, weights 0.5 / 0.5BM25Retrieverexact words: U30, PAN, IFSCVector retrievermeaning: stolen ≈ lost
Hybrid search = keyword + semantic, merged with Reciprocal Rank Fusion.
Found while building this lab: the notebook text says "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

RetrieverHowUse when
similarity as_retriever(search_kwargs={"k": 2})top-k nearest vectorsdefault
MMR search_type="mmr"relevant + diverseresults repeat each other
score thresholdonly chunks above a scoreavoid unrelated answers (“pizza recipe”)
BM25Retrieverkeyword rankingexact words: IDs, error codes, names
EnsembleRetrieverBM25 + vector, mergedproduction RAG: best of both
MultiQueryRetrieverLLM rewrites the question 3 waysshort or vague questions
ContextualCompressionRetrieverLLM keeps only the useful sentenceslong, noisy chunks
ParentDocumentRetrieversearch small chunks, return the big parentprecise search + full context
Demo · MultiQueryRetriever step by step

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.

lab10_retrievers.py
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)")
similarity 'my card is stolen' -> [2, 0] BM25 default 'U30' -> [7, 6] <- misses: the text has 'UPI-U30' as one word BM25 + regex 'U30' -> [5, 7] <- found doc 5 hybrid 'what does U30 error mean for my UPI payment?' -> [5, 3] multi-query 'how much can I send to someone new?' -> [3, 1, 5, 6] parent-doc 'annual fee' -> {'section': 'cards'} (171 chars: the whole section)
Runs here without downloads. A tiny keyword embedding (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

Colab setup (same in every notebook): click 🔑 Secrets in the Colab sidebar, add groq_key with your free key from console.groq.com, switch on Notebook access, then run the cells from the top.
Demo · Notebook player · Lab 10

05Practice tasks

  1. Build a hybrid retriever on a PDF and compare it with pure vector search on 5 questions.
  2. Print the questions MultiQueryRetriever generates for “cheap phone with good camera”.
  3. Try different weights in 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

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"?

08References

Created by Ajeetkumar