Separating retrieval from answering lets you test, tune and reuse each part on its own.
A retriever does only the search half: question in, ranked nodes out. Get one from an index:
retriever = index.as_retriever(similarity_top_k=5)
results = retriever.retrieve("refund for damaged items")Each result is a `NodeWithScore`: .node (text, metadata, id) and .score (how similar it is; higher is closer for most vector stores). Testing retrievers directly, without an LLM, is fast and cheap, and is the best way to tune chunking and similarity_top_k.
You can build a query engine from explicit parts with `RetrieverQueryEngine`:
query_engine = RetrieverQueryEngine.from_args(
retriever,
node_postprocessors=[SimilarityPostprocessor(similarity_cutoff=0.75)],
response_mode="compact",
)Node postprocessors run between retrieval and the LLM. They can drop, reorder or change nodes:
SimilarityPostprocessor(similarity_cutoff=...)drops weak matches.KeywordNodePostprocessorkeeps or removes nodes containing given words.- Rerankers re-score nodes with a stronger model (an Advanced lesson).
Score scales depend on the embedding model and vector store, so pick a cutoff by looking at real scores for good and bad matches, not by guessing.
Because retrievers share one interface, you can swap in other kinds (keyword search, hybrid, routing between indexes) without changing the rest of your pipeline. Agents can also use a retriever wrapped as a tool.
Example
from llama_index.core import Document, VectorStoreIndex
from llama_index.core.postprocessor import SimilarityPostprocessor
from llama_index.core.query_engine import RetrieverQueryEngine
index = VectorStoreIndex.from_documents([
Document(text="Damaged items are refunded in full."),
Document(text="Gift cards cannot be refunded."),
Document(text="Our office dog is called Biscuit."),
])
retriever = index.as_retriever(similarity_top_k=3)
for r in retriever.retrieve("refund for a broken product"):
print(f"{r.score:.2f} {r.node.text}")
# 0.84 Damaged items are refunded in full.
# 0.77 Gift cards cannot be refunded.
# 0.61 Our office dog is called Biscuit.
qe = RetrieverQueryEngine.from_args(
retriever, node_postprocessors=[SimilarityPostprocessor(similarity_cutoff=0.7)])
resp = qe.query("Can I get a refund for a broken product?")
print(len(resp.source_nodes)) # 2 (the dog was filtered out)Remember
as_retriever(similarity_top_k=k).retrieve(q)returns NodeWithScore listRetrieverQueryEngine.from_args(retriever, node_postprocessors=[...])- Tune retrieval on its own before touching prompts
Watch out for
- Choosing a similarity_cutoff without looking at actual score distributions, which silently filters out every result.