LlamaIndex QueryFusionRetriever — fusion errors and BM25/vector imbalance
Hybrid retrieval combines multiple retrievers (typically BM25 for exact-match + vector for semantic) via fusion. When the fusion is wrong, one retriever dominates and hybrid loses to plain vector.
Quick fix (TL;DR)
QueryFusionRetriever combines results from multiple retrievers. Fusion modes: reciprocal_rerank (RRF — rank-based, robust); relative_score (score-based, sensitive to scale); simple (concat + dedupe). Fix by (a) using RRF as the default, (b) normalizing scores when mixing retrievers with different scoring scales, and (c) generating multiple queries via num_queries for query expansion.Real error messages you'll see
These are the exact strings returned by the LlamaIndex framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.
# BM25 scores: 40-80 (Okapi BM25 raw scores) # Vector cosine scores: 0.3-0.9 # fusion_mode="relative_score" gives BM25 all top spots
# BM25 top-2 both make top-5 fused # Vector top-1 does not # Cause: k parameter in RRF too low; small rank diffs dominate
# Just running one query through multiple retrievers, # no query expansion benefit
Reference
Fusion modes
| Mode | How it fuses | Best for |
|---|---|---|
reciprocal_rerank | RRF: sum of 1/(k+rank) across retrievers | Recommended default — robust to score scales |
relative_score | Normalized scores added with weights | When scores are comparable across retrievers |
dist_based_score | Distance-based scoring | For cosine-only retrievers |
simple | Just concat + dedupe | Debug / baseline |
Root causes, ranked by frequency
Based on developer reports across LlamaIndex forums, GitHub issues, and Discord community during 2025–2026.
- 25%Score scales incompatible for relative_score mode. BM25 raw scores vs cosine similarity in 0-1 range.
- 18%Weights not set for retrievers. Default equal weights suboptimal when one retriever is stronger.
- 14%k parameter in RRF too low. Small k emphasizes top-1 differences; large k smooths.
- 10%num_queries=1 loses query expansion benefit. Fusion of one query per retriever is just merging.
- 8%BM25 not tokenized for the corpus language. English tokenizer on non-English text; BM25 useless.
- 7%Vector retriever using wrong embedding. Stale index; drift in cosine scores.
- 10%Retriever similarity_top_k too small. BM25 top-3 + vector top-3 = 6 nodes to fuse; not enough diversity.
- 8%Dedup not fusing scores. Duplicate nodes retain first-seen score; better one thrown away.
Fixes — copy-paste solutions
Use RRF as the default fusion mode
Reciprocal Rank Fusion is the standard for hybrid IR. Rank-based, not score-based, so scale differences between BM25 and cosine do not matter.
from llama_index.core.retrievers import QueryFusionRetriever from llama_index.retrievers.bm25 import BM25Retriever from llama_index.core import VectorStoreIndex # ... build vector_index and get nodes ready ... # --- Vector retriever --- vector_retriever = vector_index.as_retriever(similarity_top_k=20) # --- BM25 retriever --- bm25_retriever = BM25Retriever.from_defaults( nodes=all_nodes, similarity_top_k=20, ) # --- Fusion retriever --- fusion_retriever = QueryFusionRetriever( retrievers=[vector_retriever, bm25_retriever], similarity_top_k=10, # final top-k after fusion num_queries=1, # no query expansion (see next fix) mode="reciprocal_rerank", # RRF — robust to score scale differences use_async=True, verbose=True, ) # Use it in a query engine from llama_index.core.query_engine import RetrieverQueryEngine query_engine = RetrieverQueryEngine.from_args(fusion_retriever, llm=llm) response = query_engine.query("What is the return window for damaged items?") print(response) # --- Inspect fusion behaviour --- retrieved = fusion_retriever.retrieve("What is the return window?") for i, node in enumerate(retrieved): print(f"{i}: score={node.score:.3f} source={node.metadata.get('source')}") # RRF scores are always in the [0,1] range because they are computed as # sum over retrievers of 1 / (k + rank) # with k typically 60. Small values but comparable across queries.
k parameter in RRF (default 60) controls how much rank-1 vs rank-2 differ. Higher k smooths; lower k gives top-1 more weight. Rarely need to change from default.Use num_queries > 1 for query expansion + hybrid benefit
Set num_queries=4 to have the LLM generate paraphrases, then run each through each retriever, and fuse everything. Substantial recall lift.
from llama_index.core.retrievers import QueryFusionRetriever from llama_index.llms.openai import OpenAI llm = OpenAI(model="gpt-4o-mini") # --- Query expansion + hybrid fusion --- fusion_retriever = QueryFusionRetriever( retrievers=[vector_retriever, bm25_retriever], similarity_top_k=10, num_queries=4, # LLM generates 4 query variants mode="reciprocal_rerank", llm=llm, # for query generation verbose=True, # print generated queries query_gen_prompt=( # Optional custom prompt — default is decent "You are helpful. Given the user's query, generate {num_queries} " "paraphrased versions that capture different phrasing of the same intent.\n" "Original query: {query}\n\n" "Generate {num_queries} paraphrases (one per line):" ), use_async=True, # crucial — otherwise VERY slow ) # Execution: 4 queries × 2 retrievers = 8 parallel retrieval calls, # fused via RRF into top-10 response = query_engine.query("How can I return a damaged product?") # LLM generates variants like: # "What is the procedure for returning a damaged item?" # "How do I initiate a return for a broken product?" # "Return process for defective goods?" # Each variant is retrieved separately, boosting recall on paraphrased queries
When scores must be used, normalize per-retriever
If you must use score-based fusion (e.g. for weighting), normalize each retriever's scores to the same range and apply per-retriever weights.
from llama_index.core.retrievers import QueryFusionRetriever from llama_index.core.schema import NodeWithScore from typing import List class NormalizingRetriever: """Wraps a retriever and normalizes its scores to [0, 1].""" def __init__(self, retriever, weight: float = 1.0): self.retriever = retriever self.weight = weight async def aretrieve(self, query: str) -> List[NodeWithScore]: results = await self.retriever.aretrieve(query) if not results: return results max_score = max(n.score or 0 for n in results) min_score = min(n.score or 0 for n in results) rng = max_score - min_score if rng > 0: for n in results: # Normalize to [0,1], then apply weight normalized = (n.score - min_score) / rng n.score = normalized * self.weight return results # --- Weighted fusion favouring vector over BM25 --- weighted_vector = NormalizingRetriever(vector_retriever, weight=0.7) weighted_bm25 = NormalizingRetriever(bm25_retriever, weight=0.3) fusion = QueryFusionRetriever( retrievers=[weighted_vector, weighted_bm25], similarity_top_k=10, mode="relative_score", # score-based since we've normalized use_async=True, ) # --- Simpler alternative: post-hoc reranking with a cross-encoder --- # Often better than any fusion tuning — rerank fused top-30 to top-5 from llama_index.postprocessor.cohere_rerank import CohereRerank fusion_broad = QueryFusionRetriever( retrievers=[vector_retriever, bm25_retriever], similarity_top_k=30, # broad candidate pool mode="reciprocal_rerank", use_async=True, ) reranker = CohereRerank(top_n=5, model="rerank-english-v3.0", api_key="...") from llama_index.core.query_engine import RetrieverQueryEngine query_engine = RetrieverQueryEngine.from_args( fusion_broad, node_postprocessors=[reranker], llm=llm, )
Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Default to
reciprocal_rerank(RRF) — robust to score-scale differences. - Set
num_queries=3-5for query expansion; substantial recall lift. - Set
use_async=True— sequential fusion is very slow. - When mixing BM25 and vector, ensure BM25 uses the correct tokenizer for your corpus language.
- Set retriever
similarity_top_kgenerously (20-30) for diverse candidates before fusion. - For weighted scoring, normalize per-retriever first — raw score comparison is meaningless.
- Combine fusion with a cross-encoder reranker at the end; more quality than fusion tuning alone.
Frequently asked questions
num_queries modest (2-4).Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.