LlamaIndex query_engine.query() — empty response or wrong nodes retrieved
The query engine composes retrieval + response synthesis. When it returns "The provided context does not contain..." you have either a retrieval problem or a synthesis mode mismatch — they need different fixes.
Quick fix (TL;DR)
similarity_top_k, filters), postprocessors (rerank, filter), and response synthesizer (response_mode). Empty answers come from (a) top_k too small, (b) metadata filters excluding all matches, (c) response mode incompatible with the retrieved context size, or (d) similarity cutoff too tight. Fix by (a) increasing top_k and re-measuring, (b) checking filter results, and (c) picking the response_mode that fits your context size.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.
# response = query_engine.query("What is the refund policy?")
# print(response.response)
# "The provided context does not contain information about refund policies."
# ← retrieval returned nothing useful; check top_k and filters# retrieved = query_engine.retriever.retrieve("...")
# print(len(retrieved))
# 0
# ← metadata filter matched no documents, or embedding does not existopenai.BadRequestError: This model's maximum context length is 8192 tokens, however you requested 12481 tokens. # response_mode="compact" concatenated 30 large chunks; blew past LLM context
Reference
<code>response_mode</code> options in the response synthesizer
| Mode | How it works | When to use |
|---|---|---|
compact | Concatenates as much as fits in one LLM call | Default — fastest |
refine | One node at a time; refines answer | Long context; better quality on more nodes |
tree_summarize | Hierarchical summarize | Very large context sets |
simple_summarize | One-shot summarize (truncates) | Rare — usually worse than compact |
accumulate | Answer per node; accumulate | Comparison across documents |
compact_accumulate | Accumulate but compact each | Middle ground |
generation | Ignore context; pure LLM answer | Debug / no-retrieval baseline |
no_text | Return retrieved nodes only | Retrieval-only workflows |
Root causes, ranked by frequency
Based on developer reports across LlamaIndex forums, GitHub issues, and Discord community during 2025–2026.
- 26%similarity_top_k too small. Default 2 misses in most real corpora; retrieval quality demands 6-10.
- 18%Metadata filters exclude everything. Filter written expecting one schema; actual metadata has different keys.
- 14%Wrong response_mode for context size. Compact with too many big chunks; refine on tiny chunks.
- 10%Similarity cutoff too tight.
SimilarityPostprocessor(similarity_cutoff=0.85)— too high on cosine. - 8%Retriever using stale index. Index built with old embed_model; queries use new one; scores meaningless.
- 7%Response mode overflow on context window. Concatenated too many nodes for the LLM to handle.
- 7%Query embedding vs storage embedding mismatch. Different embedding models on ingest vs query.
- 10%No postprocessors. Cheap query returns 10 near-duplicate results; retrieval "worked" but answer is one-sided.
Fixes — copy-paste solutions
Isolate: is it retrieval or synthesis?
Split the query engine into retriever + synthesizer. Look at what retriever returns before the synthesizer touches it. This tells you whether to tune retrieval or synthesis.
from llama_index.core import VectorStoreIndex query_engine = index.as_query_engine(similarity_top_k=6) # --- Step 1: check what the retriever alone returns --- retriever = query_engine.retriever nodes = retriever.retrieve("What is the refund policy?") print(f"Retriever returned {len(nodes)} nodes") for i, node in enumerate(nodes): print(f" {i}: score={node.score:.3f}") print(f" source: {node.metadata.get('source', '?')}") print(f" text: {node.text[:100]}...") print() # --- If 0 nodes: retrieval is the problem --- # Check: is the index actually populated? print(f"Index size: {len(index.docstore.docs)} docs") # Check: does a very-broad query find anything? broad = retriever.retrieve("anything") print(f"Broad query returned {len(broad)} nodes") # Check: is embed_model the same for the query as the index? print(f"Index embed_model: {index._embed_model}") print(f"Retriever embed_model: {retriever._embed_model}") # --- Step 2: if retrieval works but response is bad, look at synthesizer --- from llama_index.core.response_synthesizers import get_response_synthesizer synth = get_response_synthesizer(response_mode="compact") response = synth.synthesize("What is the refund policy?", nodes) print(response) # Now you can vary response_mode without affecting retrieval for mode in ["compact", "refine", "tree_summarize"]: synth = get_response_synthesizer(response_mode=mode) r = synth.synthesize("...", nodes) print(f"[{mode}] {r.response[:150]}")
Tune similarity_top_k and add postprocessors
Start with similarity_top_k=10, apply a similarity cutoff or reranker as a postprocessor, then measure hit rate on a labeled test set. Iterate.
from llama_index.core.postprocessor import ( SimilarityPostprocessor, MetadataReplacementPostProcessor, ) from llama_index.core.vector_stores import ( MetadataFilters, MetadataFilter, FilterOperator, ) # --- Metadata filters push down to the vector store --- filters = MetadataFilters(filters=[ MetadataFilter(key="category", value="policies", operator=FilterOperator.EQ), MetadataFilter(key="year", value=2025, operator=FilterOperator.GTE), ]) query_engine = index.as_query_engine( similarity_top_k=10, # retrieve broadly filters=filters, node_postprocessors=[ # Drop weakly-scored nodes SimilarityPostprocessor(similarity_cutoff=0.3), # Cross-encoder rerank the top-10 to top-5 # (requires: pip install llama-index-postprocessor-cohere-rerank) # CohereRerank(top_n=5, model="rerank-english-v3.0", api_key="..."), ], response_mode="compact", ) response = query_engine.query("What is the refund policy for policies from 2025 onward?") print(response) # For introspection for node in response.source_nodes: print(f"score={node.score:.3f} source={node.metadata.get('source')}") # --- Measure hit rate on a labeled test set --- test_queries = [ ("What is the refund policy?", ["doc_refund", "doc_terms"]), ("Password reset process?", ["doc_password_kb", "doc_account_faq"]), # ... ] hits = 0 for query, expected_sources in test_queries: resp = query_engine.query(query) retrieved_sources = {n.metadata.get("source") for n in resp.source_nodes} if any(exp in retrieved_sources for exp in expected_sources): hits += 1 print(f"Hit rate: {hits}/{len(test_queries)} = {hits/len(test_queries):.1%}")
Pick the right response_mode for your context size
compact is fast but limited by LLM context. refine handles many nodes by processing them sequentially. tree_summarize is best for very large context that needs synthesis.
# --- compact: default; concatenates as much as fits --- # Best for: <=10 nodes of ~500 tokens each qe_compact = index.as_query_engine( similarity_top_k=6, response_mode="compact", ) # --- refine: process one node at a time, refining the answer --- # Best for: 15-50 nodes; longer LLM cost but higher-quality synthesis qe_refine = index.as_query_engine( similarity_top_k=20, response_mode="refine", ) # --- tree_summarize: hierarchical summarize for very large context --- # Best for: 50+ nodes; recursive summarization qe_tree = index.as_query_engine( similarity_top_k=50, response_mode="tree_summarize", use_async=True, # tree_summarize parallelizes well ) # --- accumulate: answer PER node, then combine (for comparative queries) --- # Best for: "compare X across all documents" qe_accum = index.as_query_engine( similarity_top_k=10, response_mode="accumulate", ) # --- Pick based on context and question type --- def choose_response_mode(context_size_estimate: int, question_type: str) -> str: if question_type == "compare": return "accumulate" if context_size_estimate < 4000: return "compact" if context_size_estimate < 20000: return "refine" return "tree_summarize" # --- Common trap: refine mode + streaming --- # refine mode does NOT stream naturally (produces refined answers per node) # use compact if you need streaming to end users qe_streaming = index.as_query_engine( similarity_top_k=6, response_mode="compact", streaming=True, # stream the final answer ) response = qe_streaming.query("...") for chunk in response.response_gen: print(chunk, end="", flush=True)
compact. Refine and tree_summarize produce multiple LLM calls internally — hard to stream naturally.Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Start with
similarity_top_k=10— the default of 2 is almost always wrong. - Always test the retriever alone before blaming the synthesizer.
- Add a cross-encoder reranker as a postprocessor for a 10-30% hit-rate boost.
- Push metadata filters into the vector store — do not filter after retrieval.
- Match embed_model exactly between ingest time and query time.
- Choose response_mode based on expected context size — compact for small, tree_summarize for huge.
- Build a labeled test set and measure hit rate; retune whenever the corpus or embedding changes.
Frequently asked questions
similarity_top_k is how many candidates the vector store returns. Top_n on a reranker is how many the reranker keeps after re-scoring. Common pattern: top_k=20 → rerank to top_n=5.as_retriever() instead of as_query_engine(). Returns nodes directly. Or use response_mode="no_text" which returns the nodes as source_nodes without LLM synthesis.llm= to as_query_engine() to override; keep the index embed_model consistent with ingestion.response.source_nodes to see what was retrieved.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.