Hybrid Search
Vector Databases matched lines by meaning. Hybrid search keeps that retriever and adds BM25Retriever on the same text list. LangChain merges both result lists with EnsembleRetriever. This lesson uses local Chroma — the PostgreSQL Hybrid Search lesson covers the same pattern with PGVector.
Before you run
Activate the venv from Project Setup. Install BM25 support:
pip install rank_bm25
Keep OPENAI_API_KEY in .env from OpenAI Account Setup. Chroma still calls OpenAI when it builds vectors.
Demo flow:
Vector search and BM25
The demo uses the same three HTML tag lines as the last lesson. Query href attribute URL contains the word href, which only appears in the <a> line. Vector search alone can still return the wrong lines first.
| Method | Picks |
|---|---|
| Vector search | Lines with similar meaning |
| BM25 | Lines with matching words |
| Hybrid | Both lists combined |
Build the hybrid retriever
BM25Retriever.from_texts needs no API key. Wrap it and the Chroma retriever in EnsembleRetriever.
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
texts = [
"The <a> tag creates a hyperlink. Set the href attribute to the URL.",
"The <title> tag sets the browser tab title.",
"The <h1> tag marks the main heading on a page.",
]
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_texts(texts=texts, embedding=embeddings)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
bm25_retriever = BM25Retriever.from_texts(texts)
bm25_retriever.k = 2
hybrid_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.5, 0.5],
)
docs = hybrid_retriever.invoke("href attribute URL")
for doc in docs:
print(doc.page_content)weights=[0.5, 0.5] splits the vote evenly. Bump the first number if exact word matches matter more.
Use in RAG
In Retrieval-Augmented Generation (RAG), swap vectorstore.as_retriever() for hybrid_retriever. The chain code stays the same.
Run the demo
Download the script, unzip if needed, then run:
hybrid_search_demo.py
Prints vector-only and hybrid results for one query
OPENAI_API_KEY in .env for the Chroma half.python hybrid_search_demo.py
If it fails
- ModuleNotFoundError: rank_bm25 — run
pip install rank_bm25. - AuthenticationError — check
OPENAI_API_KEYin.env. - Both printed lists match — change
QUERYto a string with a rare word from one chunk, likehref.
Docs: LangChain ensemble retriever.
What's Next
BM25 and Chroma are wired up. Next: the same pattern with PostgreSQL.