AI Engineer Dojo Contents
AI Search / RAG Engineer · Chapter 11

Advanced Retrieval

The vanilla "embed, retrieve top-k, generate" pipeline handles most queries. The ones it can't — filtered searches, multi-hop questions, queries that need a decision about whether to retrieve — are where senior RAG engineering lives. This chapter is the toolbox for the hard 20%.

Each advanced pattern exists because a specific class of query defeats vanilla RAG. Learn them as matched pairs — the failure and the fix — so you deploy the right one instead of cargo-culting all of them.

Metadata filtering: retrieve within a constraint

A user asks "what's the refund policy?" but they're an enterprise customer, and the enterprise policy differs from consumer. Pure semantic search happily returns the consumer chunk — it's textually similar and has no idea about the constraint. Metadata filtering attaches structured fields (customer_tier, product, date, language) to each chunk and filters before or during retrieval, so you only search the eligible subset. It's the single highest-leverage precision tool for corpora with natural partitions, and it prevents a whole class of confidently-wrong-but-relevant answers.

Multi-hop & agentic RAG: retrieve, reason, retrieve again

"Which of our SOC-2-certified vendors also support SSO?" needs two retrievals: first the SOC-2 vendors, then, for those, SSO support. A single retrieval can't do it — the answer isn't in any one chunk. Agentic RAG lets the model drive retrieval: it decides what to search, reads results, and searches again based on what it learned, looping until it can answer. It's more powerful and more expensive/slower — each hop is an LLM call plus a retrieval — so you reserve it for genuinely multi-step questions rather than making every query pay for the capability.

Adaptive retrieval: know when NOT to retrieve

Not every query needs RAG. "Rewrite this paragraph to be shorter" needs no corpus; retrieving for it just injects noise and cost. Mature systems put a cheap router in front: does this query need retrieval, and if so, which index? Retrieving for a no-retrieval query is a real failure mode — it can drag irrelevant chunks into context and lower answer quality.

Match the pattern to the failure — don't stack blindly

Filtered/partitioned corpus → metadata filtering. Multi-step question → agentic/multi-hop. Mixed retrieval/no-retrieval traffic → a router. GraphRAG (retrieval over an entity graph) → questions about relationships across many docs. Every pattern adds latency and complexity; deploy the one your query distribution actually needs, proven with the eval harness from Ch. 8.

Try it · ~25 min

Metadata filtering — kill the confidently-wrong answer

You'll retrieve over a corpus where the semantically-best chunk is for the wrong customer tier, and show that adding a metadata filter flips a wrong-but-relevant answer into the right one — with precision measured before and after. Free and deterministic.

Setup: none — pure Python.

Step 1. Retrieve by topical score alone; observe the wrong-tier chunk winning.

Step 2. Filter candidates to the query's tier, then retrieve.

Step 3. Compare which chunk each returns and compute accuracy over the query set.

Your goal: unfiltered picks a relevant-but-wrong-tier chunk; filtered picks the correct one — accuracy up.

Starter code

CHUNKS = [
  {"id":"c1","tier":"consumer",  "topical":0.95, "text":"Consumer refunds within 14 days."},
  {"id":"c2","tier":"enterprise","topical":0.80, "text":"Enterprise refunds are governed by contract terms."},
  {"id":"c3","tier":"consumer",  "topical":0.90, "text":"Consumer plan cancellation policy."},
  {"id":"c4","tier":"enterprise","topical":0.78, "text":"Enterprise cancellation requires 30-day notice."},
]
# (query, asker's tier, correct chunk id)
QUERIES = [
  ("refund policy",       "enterprise", "c2"),
  ("cancellation policy", "enterprise", "c4"),
]

def best(cands):  # highest topical score
    return max(cands, key=lambda c: c["topical"])

# TODO unfiltered: best(all chunks) — does it match the correct id?
# TODO filtered:   best(chunks whose tier == asker tier)
# Print accuracy (fraction correct) for each.
Worked solution
def accuracy(use_filter):
    correct = 0
    for q, tier, gold in QUERIES:
        cands = [c for c in CHUNKS if (c["tier"] == tier)] if use_filter else CHUNKS
        correct += best(cands)["id"] == gold
    return correct / len(QUERIES)

print(f"unfiltered accuracy: {accuracy(False):.2f}")
print(f"filtered   accuracy: {accuracy(True):.2f}")

What you should see: unfiltered 0.00, filtered 1.00. Unfiltered retrieval picks the consumer chunks every time — they have the highest topical scores (0.95, 0.90) because they're textually cleaner matches — and hands an enterprise customer the wrong policy with total confidence. The metadata filter restricts candidates to the asker's tier before ranking, so semantic similarity only competes within the correct partition. The wrong answer wasn't a retrieval-quality problem you could fix with a better embedder — it was a constraint problem, and only a filter fixes it.

The senior insight: this is the failure semantic search cannot see, because the wrong chunk is genuinely relevant — just not eligible. Any corpus with tiers, dates, languages, permissions, or product lines has this hazard. Filtering is often a bigger accuracy win than any amount of retriever tuning, and it doubles as access control (never retrieve a chunk the user isn't allowed to see).

Going further: combine filter + hybrid + rerank and measure on a mixed query set where some queries have no tier constraint — you'll need the router idea (retrieve/no-filter vs. filter) to avoid over-restricting the unconstrained queries. That's the real production shape.

Case study

ResearchMate: retrieve small, read big

ResearchMate answers questions over scientific papers. They faced the chunking dilemma from both sides: small chunks embedded cleanly and retrieved precisely, but handed to the model in isolation they lacked the surrounding context needed to synthesize a real answer — completeness scores sat at 3.4/5. Bigger chunks carried context but muddied the embeddings and buried the precise fact.

The advanced pattern that resolved it: small-to-big (parent-document) retrieval. Index and match on tight paragraph-sized chunks for retrieval precision, but at synthesis time feed the model the parent section each match belongs to. Answer-completeness rose to 4.3/5 with no drop in retrieval precision — you get the accuracy of small-chunk matching and the context of large-chunk reading, instead of trading one for the other.

Running case · Acme Vault

Acme retrieves on tight paragraph chunks but hands the model the full section, so an answer about vault.export arrives with its surrounding auth prerequisites attached — no more technically-correct answers that omit the required setup step.

Quiz · Chapter 11 — reasoning, not recall

  1. An enterprise user asks for the refund policy and gets the consumer policy — which is textually very similar. Pure semantic search fails here because:
  2. "Which SOC-2 vendors also support SSO?" defeats single-shot retrieval because:
  3. Why not make every query use agentic multi-hop retrieval by default?
  4. Retrieving context for "rewrite this paragraph to be shorter" is a failure mode because:
  5. When is GraphRAG (retrieval over an entity graph) the right tool?

← Back Continue →

The AI Search / RAG Engineer · AI Engineer Dojo · aiengineerdojo.com