AI Model Wiki

    Skip to main content
    40 min
    Lesson 2 of 5

    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
    0 of 5 lessons marked complete

    Progress is kept in this browser only.