Skip to main content
Back to Blog
Architecture·6 min read·July 5, 2025

Hybrid Search: Why Pure Vector Search Isn't Enough

Vector search is powerful but misses exact matches. BM25 is precise but misses intent. Here's how to combine them for enterprise-grade retrieval.

S
Sanjay Sebastian
Founder, Chervik

When teams first deploy a RAG system, they often start with pure vector search. It feels modern, it handles natural language beautifully, and the demos look impressive. Then someone searches for a specific contract number, a product code, or an employee ID — and the system returns completely irrelevant results. This is the vector search blind spot.

How Vector Search Works (and Where it Fails)

Vector search converts text into high-dimensional numerical vectors and finds documents whose vectors are closest to the query vector. It excels at semantic similarity — 'how do I request annual leave' will find documents about 'holiday booking process' even if those exact words don't appear.

But vector search struggles with exact matches. If you search for 'Contract REF-2024-0847', the model has no special understanding of that identifier. It will return documents that are semantically similar to the concept of contracts — which may not include the specific contract you need.

How BM25 Works (and Where it Fails)

BM25 is a keyword-based ranking algorithm — the foundation of traditional search engines. It's excellent at exact and partial matches. Search for 'REF-2024-0847' and BM25 will find every document containing that string, ranked by term frequency.

But BM25 has no semantic understanding. It can't handle synonyms, paraphrasing, or intent. 'Show me the leave policy' won't find a document titled 'Annual Holiday Entitlement Guidelines' unless those exact words overlap.

In enterprise knowledge bases, you need both. Users ask natural language questions AND search for specific identifiers, names, and codes. Neither approach alone is sufficient.

Hybrid Search: Combining Both

Hybrid search runs both vector and BM25 retrieval in parallel, then merges the results. The standard merging algorithm is Reciprocal Rank Fusion (RRF) — a simple formula that combines the rank positions from both result sets without requiring score normalisation.

python
# Reciprocal Rank Fusion
def rrf_score(rank: int, k: int = 60) -> float:
    return 1 / (k + rank)

def fuse_results(vector_results, bm25_results):
    scores = {}
    for rank, doc_id in enumerate(vector_results):
        scores[doc_id] = scores.get(doc_id, 0) + rrf_score(rank)
    for rank, doc_id in enumerate(bm25_results):
        scores[doc_id] = scores.get(doc_id, 0) + rrf_score(rank)
    return sorted(scores, key=scores.get, reverse=True)

Re-ranking: The Final Step

After hybrid retrieval, a re-ranker model scores each candidate document against the original query for relevance. Re-rankers are slower than retrieval but more accurate — they read the full document chunk rather than comparing vectors. Running a cross-encoder re-ranker on the top 20 hybrid results before passing the top 5 to the LLM significantly improves answer quality.

Implementation in Practice

  • Use pgvector for vector storage and PostgreSQL full-text search for BM25 — one database, two search modes
  • Or use Weaviate/Qdrant which have hybrid search built in
  • Set your retrieval to return top-20 from hybrid, then re-rank to top-5 before the LLM
  • Tune the vector/BM25 weight ratio based on your query patterns — keyword-heavy domains benefit from higher BM25 weight
  • Always evaluate retrieval quality separately from generation quality — they fail in different ways