Course navigation
Embeddings, Vector Stores & RAGLesson 5 of 11

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:

query: "href attribute URL"
BM25Retriever
Chroma retriever
↓ EnsembleRetriever
print merged results
Same three HTML strings from Vector Databases. For PostgreSQL, see the separate PostgreSQL Hybrid Search lesson.

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.

MethodPicks
Vector searchLines with similar meaning
BM25Lines with matching words
HybridBoth lists combined
BM25 runs on the text list only. Vector search still calls OpenAI through Chroma.

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

Keep OPENAI_API_KEY in .env for the Chroma half.
hybrid_search_demo.py
"""hybrid_search_demo.py"""
from langchain.retrievers import EnsembleRetriever
# BM25 + Chroma → print both result lists
hybrid_retriever = EnsembleRetriever(…)
python hybrid_search_demo.py
PowerShell — (.venv) active
(.venv) PS C:\projects\langchain-course> python hybrid_search_demo.py
Query: href attribute URL
=== Vector-only ===
[0] The <title> tag sets the browser tab title.
[1] The <h1> tag marks the main heading on a page.
=== Hybrid (BM25 + vector) ===
[0] The <a> tag creates a hyperlink. Set the href attribute…
[1] The <title> tag sets the browser tab title.
On this query the <a> line ranks first only after BM25 is added.

If it fails

  • ModuleNotFoundError: rank_bm25 — run pip install rank_bm25.
  • AuthenticationError — check OPENAI_API_KEY in .env.
  • Both printed lists match — change QUERY to a string with a rare word from one chunk, like href.

Docs: LangChain ensemble retriever.

What's Next

BM25 and Chroma are wired up. Next: the same pattern with PostgreSQL.