Retrievers and custom query engines

LlamaIndex · Practical · 12 min

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.
  • KeywordNodePostprocessor keeps 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 list
  • RetrieverQueryEngine.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.

Check yourself

  1. Why test a retriever on its own, without the LLM?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developers.llamaindex.ai/python/framework/module_guides/querying/retriever/