Overview
RAG Fusion addresses a fundamental limitation of single-query retrieval: a user's phrasing of a question may not match the terminology used in relevant documents. By generating multiple reworded and expanded queries from the original question, then merging the results with Reciprocal Rank Fusion (RRF), RAG Fusion achieves significantly higher recall than any single query can deliver.
Pattern #3 in the 37 RAG Patterns Collection
flowchart TD Q[Query] --> GQ["LLM: generate 4 query variations covering different angles"] GQ --> Q1[Variation 1] & Q2[Variation 2] & Q3[Variation 3] & Q4[Variation 4] Q1 --> R1[Search top-10] Q2 --> R2[Search top-10] Q3 --> R3[Search top-10] Q4 --> R4[Search top-10] R1 & R2 & R3 & R4 --> RRF["RRF k=60 — up to 40 candidates fused"] RRF --> TOP[Top-K diverse results] TOP --> LLM[Generate]
The Problem
A single query is anchored to the user's phrasing. Documents that use synonyms, related terms, or different framings are systematically under-retrieved:
Query: "How do I reduce model inference latency?"
Retrieved (top-3):
1. "Optimizing transformer throughput" — relevant but uses different terms
2. "GPU memory management" — partially relevant
3. "Batch size tuning guide" — ranked lower than ideal
Missed entirely:
- "Speeding up LLM serving with quantization" (uses "speeding up", not "reduce latency")
- "TensorRT optimization for production" (uses a product name, not the concept)
- "Low-latency inference with dynamic batching" (different framing)
This vocabulary mismatch means Recall@10 often plateaus even with better embeddings — you need more query diversity to reach more of the relevant space.
Solution
Generate N query variants, retrieve independently for each, then fuse with Reciprocal Rank Fusion:
Original: "How do I reduce model inference latency?"
↓
LLM generates N reworded queries:
Q1: "How do I reduce model inference latency?" (original)
Q2: "Techniques for speeding up LLM inference"
Q3: "Optimizing transformer model serving performance"
Q4: "Low-latency deployment strategies for large language models"
↓
Independent retrieval for each query → 4 ranked lists
↓
RRF merge: score(d) = Σ 1 / (60 + rank_i(d)) for each list where d appears
↓
Final top-K fused documents → Generator
The RRF formula rewards documents that appear in multiple lists and rank highly in each. The constant 60 dampens the influence of very-high-ranked documents from a single query, making the fusion robust to any one list dominating.
Implementation
from typing import List, Dict
def generate_queries(original_query: str, llm, n: int = 4) -> List[str]:
"""Use an LLM to generate N reworded/expanded variants of the original query."""
prompt = f"""You are a query expansion assistant. Given a user question, generate {n - 1}
different ways to ask the same question. Vary the vocabulary, perspective, and framing
to maximize document recall. Return one query per line, no numbering.
Original question: {original_query}
Alternative queries:"""
response = llm.invoke(prompt)
variants = [q.strip() for q in response.strip().split('\n') if q.strip()]
# Always include the original first
return [original_query] + variants[: n - 1]
def retrieve_for_query(query: str, retriever, top_k: int = 10) -> List[Dict]:
"""Retrieve documents for a single query and tag each result with its rank."""
results = retriever.search(query, top_k=top_k)
return [{'doc': doc, 'rank': i + 1, 'query': query} for i, doc in enumerate(results)]
def rrf_merge(ranked_lists: List[List[Dict]], k: int = 60, top_n: int = 10) -> List[Dict]:
"""
Reciprocal Rank Fusion across multiple ranked lists.
score(d) = sum of 1 / (k + rank_i(d)) for every list i where d appears.
k=60 is the standard default from the original RRF paper (Cormack 2009).
Documents appearing in more lists, and ranked higher within each list, score highest.
"""
scores: Dict[str, float] = {}
doc_map: Dict[str, Dict] = {}
for ranked_list in ranked_lists:
for item in ranked_list:
doc_id = item['doc']['id']
rank = item['rank']
rrf_score = 1.0 / (k + rank)
if doc_id not in scores:
scores[doc_id] = 0.0
doc_map[doc_id] = item['doc']
scores[doc_id] += rrf_score
sorted_ids = sorted(scores, key=lambda x: scores[x], reverse=True)
return [
{'doc': doc_map[doc_id], 'rrf_score': scores[doc_id]}
for doc_id in sorted_ids[:top_n]
]
def rag_fusion(query: str, retriever, llm, n_queries: int = 4, top_k: int = 10) -> Dict:
"""Full RAG Fusion pipeline: generate → retrieve → fuse → generate."""
# Step 1: Generate query variants
queries = generate_queries(query, llm, n=n_queries)
# Step 2: Retrieve for each query independently
all_ranked_lists = []
for q in queries:
ranked = retrieve_for_query(q, retriever, top_k=top_k)
all_ranked_lists.append(ranked)
# Step 3: Fuse ranked lists with RRF
fused_docs = rrf_merge(all_ranked_lists, k=60, top_n=top_k)
# Step 4: Generate answer from fused context
context = "\n\n".join([item['doc']['content'] for item in fused_docs])
answer = llm.invoke(f"Context:\n{context}\n\nQuestion: {query}\n\nAnswer:")
return {
'answer': answer,
'queries_used': queries,
'fused_docs': fused_docs,
'total_candidates': len(queries) * top_k,
}Complete Notebook
Run the full implementation:
Includes:
- ▸Query generation prompts and tuning
- ▸Parallel retrieval across query variants
- ▸RRF implementation with parameter sweep (k=20, 60, 100)
- ▸Recall@10 comparison against single-query baseline on a real benchmark
Performance
| Metric | Single Query | RAG Fusion |
|---|---|---|
| Recall@10 | 65% | 82% |
| MRR | 0.61 | 0.76 |
| Latency | ~200ms | ~600ms |
| Cost/query | $0.04 | $0.12 |
RAG Fusion improves Recall@10 by +17 points at roughly 3x the latency of single-query retrieval — the tradeoff is well worth it for recall-sensitive workloads.
When to Use
Best For
- ▸Ambiguous questions where the user's phrasing may not match document vocabulary
- ▸Knowledge bases with varied terminology — technical docs mixing formal and informal terms, or multilingual corpora
- ▸High-recall requirements — when missing relevant documents is more costly than extra latency
- ▸Multi-domain corpora where the same concept appears under different names across sources
Combine With
- ▸Cross-Encoder Reranking (#4) — apply a reranker to the fused list to improve precision after the recall gains
- ▸Adaptive RAG (#8) — route only ambiguous or broad queries through multi-query fusion; skip it for specific lookups
- ▸Contextual Compression (#6) — compress the larger fused context before generation to control token costs
Related Papers
- ▸RAG-Fusion: Shi et al., 2024 — multi-query generation + RRF for RAG
- ▸HyDE: Gao et al., 2022 — hypothetical document embeddings, a complementary query expansion technique
- ▸RRF original: Cormack, Clarke & Buettcher (SIGIR 2009) — the foundational rank fusion paper
Next Steps
- ▸Try it: Run the notebook
- ▸Tune: Experiment with N queries (3–6) and the RRF k parameter; k=60 is a strong default
- ▸Improve precision: Layer Cross-Encoder Reranking (#4) on top of the fused list
Part of: 37 RAG Patterns Collection