AI Techniques

Hybrid Search

Hybrid search runs lexical retrieval (typically BM25) and semantic retrieval (dense vector search) against the same query, then fuses the two ranked lists into one, combining exact-term precision with conceptual recall. It has become the default recommendation, not an advanced option, across nearly every major vector database as of 2025-2026.

Hybrid search runs two different retrieval methods, almost always BM25 keyword search and dense vector search, against the same query, and merges their two ranked lists into one before anything reaches a reranker or an LLM. Neither method alone is enough: keyword search misses paraphrase and synonymy, dense search blurs exact identifiers and rare tokens. By 2025-2026, running both and fusing the output isn’t a specialized technique reserved for teams with retrieval expertise; it’s the default, out-of-the-box configuration recommended by Qdrant, Weaviate, Elasticsearch, Pinecone, and Azure AI Search alike.

The Blind Spots It Closes

Sparse retrieval (BM25) matches shared vocabulary and is exact, cheap, and unbeatable on product codes, legal citations, or a person’s name, but it cannot connect “heart attack” to “myocardial infarction” because the two share no terms. Dense retrieval matches shared meaning via learned embeddings and generalizes across phrasing, but that same compression can lose an exact SKU or a rare acronym the encoder barely saw during training. These failure modes are near-perfect complements of each other, not overlapping weaknesses, which is what makes fusing the two so effective: a document that either method alone would miss is often caught by the other. See Dense Retrieval vs. Sparse Retrieval for the full mechanics of each side.

Anatomy of a Hybrid Query

graph TB
    classDef default fill:#ffffff,stroke:#4338CA,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
    classDef data    fill:#EEF0F7,stroke:#0D9488,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
    classDef process fill:#F7F8FC,stroke:#6366F1,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
    classDef output  fill:#4338CA,stroke:#4338CA,stroke-width:2px,color:#ffffff,rx:8px,ry:8px;

    Q([User Query]):::data --> SP[Sparse Leg:<br/>BM25 over inverted index]:::process
    Q --> DN[Dense Leg:<br/>Embedding + ANN search]:::process
    SP -->|Ranked list A| FU[Fusion]:::output
    DN -->|Ranked list B| FU
    FU -->|Single fused ranking| RR[Optional: Reranker]:::process
    RR --> OUT[Final Results]:::output

Both legs run against the same document collection and the same incoming query, independently, and only meet at the fusion step. This is what makes hybrid search additive rather than a trade-off: neither leg has to compromise its own scoring to accommodate the other.

Two Ways to Fuse

Score-based (weighted) fusionRank-based fusion (RRF)
InputRaw similarity/relevance scoresOnly rank position
Normalization neededYes: min-max, z-score, or similar, since BM25 and cosine similarity live on incompatible scalesNone
Tuning knobA weight (e.g. Weaviate’s alpha, 0 = pure BM25, 1 = pure dense)A single constant k (almost always 60)
Sensitivity to outliersHigher; one extreme score can skew the normalized rangeLower; an extreme score doesn’t distort rank position
Best whenScores are well-calibrated and a domain-specific weight has been tunedYou want something that works out of the box, no calibration step

Score-based fusion is easiest to reason about with a concrete pair of documents in front of you. The widget below fuses a fixed BM25 score and a fixed dense score for four candidates answering an internal engineering query, drag alpha and watch which document actually wins:

Interactive: dragging alpha reorders the hybrid ranking

Query: "CJ-1173 cache job keeps failing." One candidate names the job ID directly; another describes the same underlying problem without ever using it.

Score-based fusion, shown in the widget above and the code example in the frontmatter, is intuitive: normalize each leg’s scores to a common range, then take a weighted sum. Its weakness is that it depends on the normalization holding up across queries, which isn’t guaranteed when score distributions shift. Rank-based fusion sidesteps that entirely by throwing away raw scores and combining only rank position:

# Scenario: fuse a BM25 ranking and a dense ranking using only rank
# position, no score normalization needed
def reciprocal_rank_fusion(bm25_ranked_ids, dense_ranked_ids, k=60):
    scores = {}
    for rank, doc_id in enumerate(bm25_ranked_ids):
        scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
    for rank, doc_id in enumerate(dense_ranked_ids):
        scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
    return sorted(scores, key=scores.get, reverse=True)

bm25_ranked_ids = ["doc_7", "doc_2", "doc_9"]    # BM25's ranking, best first
dense_ranked_ids = ["doc_2", "doc_9", "doc_7"]   # dense search's ranking, best first

print(reciprocal_rank_fusion(bm25_ranked_ids, dense_ranked_ids))
# doc_2 and doc_9 rank well in both lists, so RRF pushes them above doc_7,
# which only one method rated first, with no score calibration required.

Reciprocal Rank Fusion (RRF) is the dominant version of this approach and is covered in full there, including the formula’s derivation, the k constant, and why it has become the default in most native hybrid APIs.

Reference Implementations Across Vector Databases

SystemFusion approach
QdrantUniversal Query API; RRF or Distribution-Based Score Fusion (DBSF) as a fusion parameter, combining dense and sparse (including SPLADE) vectors server-side
Weaviatealpha-weighted score fusion (rankedFusion or relativeScoreFusion), plus an RRF-style mode
Elasticsearch / OpenSearchNative rrf retriever combining a BM25-based retriever with a knn vector retriever in one query
PineconeSingle sparse-dense index; a record holds both a sparse and a dense vector, queried together and combined by weighting
Azure AI SearchRRF as the default and only fusion method for hybrid queries
Postgres / pgvector stacksNo native fusion primitive; RRF is typically implemented by hand in SQL, joining a pgvector ANN query with a tsvector full-text query

Collapsing Two Models Into One: BGE-M3 and Unified Encoders

Running hybrid search traditionally means operating two separate models: a BM25 index and a dense embedding model. BGE-M3 and similar unified encoders collapse this into a single forward pass, producing dense, sparse, and multi-vector (ColBERT-style) representations from one model call, so a hybrid pipeline no longer requires hosting and maintaining two entirely different systems. This has made hybrid search meaningfully cheaper to operate, not just more effective, which is part of why adoption accelerated through 2025.

Hybrid Search Plus Reranking: The Three-Stage Pipeline

Fusion produces a single candidate list, but it’s still built from two coarse first-stage rankings. Production RAG and enterprise search systems increasingly treat that fused list as input to a third stage:

StagePurpose
RetrievalGenerate a sparse ranking (BM25) and a dense ranking (embeddings) independently
FusionCombine both into one candidate list, cheaply, with no model inference
RerankingPrecisely reorder the fused shortlist with a cross-encoder

Fusion is essentially free, so it’s applied to a generous candidate pool (top 50-100 from each leg) before the more expensive reranking step narrows that down to the final top-k. “Retrieve, fuse, rerank” is now the standard shape of a high-quality RAG retrieval pipeline:

# Scenario: the full three-stage pipeline, end to end
def hybrid_search_with_rerank(query, bm25_index, dense_index, reranker, doc_store, top_k=5):
    bm25_hits = bm25_index.search(query, top_k=100)      # stage 1a: sparse leg
    dense_hits = dense_index.search(query, top_k=100)    # stage 1b: dense leg

    fused_ids = reciprocal_rank_fusion(
        [hit.id for hit in bm25_hits], [hit.id for hit in dense_hits]
    )                                                      # stage 2: fusion, no model calls

    shortlist = [doc_store[doc_id] for doc_id in fused_ids[:50]]
    pairs = [(query, doc.text) for doc in shortlist]
    scores = reranker.predict(pairs)                      # stage 3: precise reordering
    return sorted(zip(shortlist, scores), key=lambda x: x[1], reverse=True)[:top_k]

What’s New (2025-2026)

  • Hybrid as the default, not the advanced option. Qdrant’s Universal Query API, Weaviate’s hybrid mode, and Elasticsearch’s RRF retriever all reflect the same shift: vector database vendors now recommend hybrid search out of the box rather than treating it as a manual pipeline power users assemble themselves.
  • Learned sparse retrieval as the sparse leg. SPLADE and similar learned sparse models moved into production through 2025-2026 as an alternative to raw BM25 on the sparse side, keeping the inverted-index infrastructure while narrowing the vocabulary-mismatch gap that plain BM25 can’t close.
  • Multi-vector as a third leg. Some systems now fuse three signals instead of two: dense, sparse, and ColBERT-style multi-vector similarity, all combinable in a single query API call.
  • Unified embedding models going mainstream. BGE-M3-style models that emit dense, sparse, and multi-vector representations from one encoder have become the practical default for teams that don’t want to operate a separate BM25 stack alongside a separate embedding service.

Tuning and Practical Guidance

ScenarioRecommendation
Legal, medical, or product-catalog search with exact codes or IDsLean sparse: low alpha (weighted fusion) or trust BM25’s contribution heavily
Conversational, paraphrased natural-language queriesLean dense: high alpha, or rely on RRF’s balanced treatment of both legs
General-purpose enterprise search or RAGStart with balanced fusion (alpha ≈ 0.5, or RRF with k = 60) and adjust from evaluation data
No labeled data to tune a weight againstRRF; it needs no score calibration and works reasonably well without tuning
Multilingual or cross-lingual searchDense-leaning hybrid with a multilingual encoder, or a BGE-M3-style unified model
Tight latency budgetWeighted fusion with cached/precomputed scores, or a single unified encoder to avoid running two model calls

Hybrid search isn’t a compromise between lexical and semantic retrieval, it’s an acknowledgment that they fail in different, non-overlapping ways. The dominant pattern in 2025-2026 production systems is to stop choosing between BM25 and embeddings altogether, run both, fuse the results, and let a downstream reranker sort out which evidence actually answers the query.

How to Use: Weighted (alpha) score fusion for hybrid search

python
import numpy as np

def min_max_normalize(scores: np.ndarray) -> np.ndarray:
    lo, hi = scores.min(), scores.max()
    if hi == lo:
        return np.zeros_like(scores)
    return (scores - lo) / (hi - lo)

# Raw scores from two different retrieval systems, on incompatible scales
bm25_scores = np.array([12.4, 3.1, 9.8, 0.0])       # unbounded, corpus-dependent
dense_scores = np.array([0.71, 0.85, 0.62, 0.44])   # cosine similarity, bounded [0, 1]

# Normalize each to [0, 1] before combining, otherwise BM25's larger
# numeric range would dominate the fused score regardless of relevance
bm25_norm = min_max_normalize(bm25_scores)
dense_norm = min_max_normalize(dense_scores)

def hybrid_score(alpha: float) -> np.ndarray:
    # alpha=1.0 -> pure dense, alpha=0.0 -> pure BM25 (Weaviate's convention)
    return alpha * dense_norm + (1 - alpha) * bm25_norm

for alpha in (0.0, 0.5, 1.0):
    fused = hybrid_score(alpha)
    print(f"alpha={alpha}: {np.round(fused, 3)}")

# See /glossary/reciprocal-rank-fusion-rrf for the rank-based alternative,
# which skips normalization entirely by using rank position instead of raw scores.

Ready to build?

Leverage AI technologies to build your product stack

Superteams can help you build, deploy and launch AI application stacks using open source technologies — from architecture through to production.

Talk to Superteams