Hybrid Search
From the course Semantic Search Implementation
Combining Keyword and Semantic Search
Why Hybrid?
Pure semantic search misses exact matches (product IDs, names). Pure keyword search misses conceptual similarity. Hybrid combines both.
Architecture
User Query
├── Keyword Search (BM25) → Results A
└── Semantic Search (Vector) → Results B
↓
Reciprocal Rank Fusion (RRF)
↓
Combined Results
Reciprocal Rank Fusion
def rrf(results_lists, k=60):
scores = {}
for results in results_lists:
for rank, doc in enumerate(results):
if doc.id not in scores:
scores[doc.id] = 0
scores[doc.id] += 1 / (k + rank + 1)
return sorted(scores.items(), key=lambda x: -x[1])
Implementation Options
- Elasticsearch + Vector Plugin: BM25 + kNN in one system
- PostgreSQL + pgvector: Full-text search + vector similarity
- Separate systems: Dedicated keyword + vector DBs, merge results
- Weaviate/Qdrant: Built-in hybrid search
Tuning Hybrid Weight
Balance between keyword and semantic results:
- alpha=0: Pure keyword search
- alpha=0.5: Equal weight
- alpha=1: Pure semantic search
- Test different values on your evaluation dataset
Common Pitfall: Implementing Hybrid Search Without Normalizing Scores
BM25 scores and cosine similarity scores are on completely different scales. BM25 produces scores that can range from 0 to 20+, while cosine similarity is bounded between 0 and 1. Merging them without normalization gives BM25 an unfair weight advantage. Normalize both to [0, 1] before applying weights:
def normalize_scores(results: list) -> list:
if not results:
return results
max_score = max(r["score"] for r in results)
min_score = min(r["score"] for r in results)
span = max_score - min_score or 1.0
for r in results:
r["score"] = (r["score"] - min_score) / span
return results
keyword_results = normalize_scores(bm25_results)
semantic_results = normalize_scores(vector_results)
# Now safe to merge with equal weights
Best Practice: Use Weaviate or Qdrant for Built-In Hybrid Search
If you are building a new search system, choose a vector database with native hybrid search rather than maintaining two separate systems. Weaviate provides BM25 + vector search with a single query. Qdrant supports sparse + dense vector hybrid search. Both eliminate the need to run and sync a separate keyword search engine like Elasticsearch.
Named Product Reference: pgvector for PostgreSQL Hybrid Search
For teams already running PostgreSQL, the pgvector extension adds vector similarity search directly to the database. Combined with PostgreSQL's built-in full-text search (tsvector/tsquery), you get hybrid search in a single database without any additional infrastructure. This is the lowest-complexity hybrid search solution for teams with existing Postgres infrastructure.
Key Takeaways
- •Hybrid search combines keyword (BM25) and semantic (vector) search
- •Reciprocal Rank Fusion merges results from multiple search methods
- •Tune the keyword/semantic balance based on your data and queries
- •PostgreSQL + pgvector offers hybrid search in a single database
Progress is kept in this browser only.