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

Query Understanding & Transformation

So far you've improved the index and the retriever. But half of retrieval failures are caused by the query — it's too short, too vague, or worded nothing like the document that answers it. Rewriting the query before you search is often the cheapest large recall win available.

Retrieval quietly assumes the user's words resemble the document's words (lexical) or its meaning (dense). Real queries break that assumption constantly: "it broke after the update" (which product? which update?), "cheapest plan" (versus a doc that says "pricing tiers"), or a multi-part question whose two halves live in different documents. The fix is a small LLM pass that transforms the query into something retrievable. Four techniques, in rising order of power and cost:

TechniqueWhat it doesBest for
Rewrite / normalizeExpand pronouns, add implied context, fix jargonShort, vague, conversational queries
Multi-queryGenerate 3–4 paraphrases, retrieve for each, union the resultsRecall — different wordings catch different docs
HyDEHave the LLM write a hypothetical answer, embed that, retrieve by itWhen queries look nothing like answer text
DecompositionSplit a multi-part question into sub-queries, retrieve eachMulti-hop questions spanning documents

Why multi-query works: coverage through variation

A single query is a single point in retrieval space — it catches the docs near that phrasing and misses docs phrased differently. Generate three paraphrases of "how do I stop the app crashing on launch" — "resolve startup failures," "fix boot crash," "application won't start" — retrieve for each, and union the hits. Each phrasing lands near different documents, so the union recovers answers no single phrasing would. It's the same complementary-errors logic as hybrid (Ch. 5), applied to the query side instead of the retriever side.

HyDE: search with the answer, not the question

Hypothetical Document Embeddings exploits a subtle asymmetry: a question and its answer often share few words, but two answers to the same question look very alike. So instead of embedding the terse question, you ask the LLM to draft a plausible answer (it doesn't need to be correct — just shaped like the real document), embed that, and retrieve. The hypothetical answer sits much closer in embedding space to the real answer document than the question ever did. It costs one extra LLM call and can lift recall notably on corpora where questions and answers are worded very differently.

The trade you're making

Every transformation adds an LLM call (latency + cost) before retrieval even starts. The discipline: apply the cheapest transform that fixes your failure class. Vague queries → rewrite. Recall ceiling → multi-query. Question/answer vocabulary mismatch → HyDE. Multi-hop → decomposition. Don't stack all four by default; measure which one moves your recall and pay only for that.

Try it · ~30 min

Measure the multi-query recall lift

You'll retrieve for a set of hard, tersely-worded queries with (a) the raw query and (b) the union of three paraphrases, and measure recall@3 for each. You'll watch the union recover documents the single phrasing missed. Free: paraphrases are provided (in production an LLM generates them; the harness is identical).

Setup: pip install rank-bm25.

Step 1. Retrieve top-3 for the raw query.

Step 2. Retrieve top-3 for each of 3 paraphrases; union the doc ids.

Step 3. Compute recall@3 for single vs. multi-query. Print both.

Your goal: multi-query recall ≥ single, and the ability to point at one query the paraphrases saved.

Starter code

from rank_bm25 import BM25Okapi
import re
def toks(s): return re.findall(r"[a-z0-9]+", s.lower())

CORPUS = [
  "Resolving application startup failures and boot crashes",   # 0
  "Reducing monthly subscription costs on the Basic plan",     # 1
  "How to reset a forgotten account password",                 # 2
  "Troubleshooting slow query performance in the database",    # 3
  "Enabling two-factor authentication for your login",         # 4
]
# (raw query, [paraphrases the LLM would generate], gold doc id)
CASES = [
  ("app won't launch", ["application startup failure","boot crash fix","program won't start"], 0),
  ("make it cheaper",  ["reduce subscription cost","lower monthly price","cheapest plan"],      1),
  ("can't log in, forgot my code", ["reset forgotten password","account recovery","login help"], 2),
]
bm25 = BM25Okapi([toks(d) for d in CORPUS])
def ranks(q):
    s = bm25.get_scores(toks(q)); return sorted(range(len(CORPUS)), key=lambda i:s[i], reverse=True)

# TODO single: recall@3 using ranks(raw)[:3]
# TODO multi:  union of ranks(p)[:3] for each paraphrase p; hit if gold in the union
#      (union recall@3 = fraction of CASES whose gold is in the unioned set)
Worked solution
def single_hit(raw, paras, gold): return gold in ranks(raw)[:3]

def multi_hit(raw, paras, gold):
    hits = set()
    for p in paras:
        hits.update(ranks(p)[:3])
    return gold in hits

sr = sum(single_hit(*c) for c in CASES) / len(CASES)
mr = sum(multi_hit(*c)  for c in CASES) / len(CASES)
print(f"single-query recall@3: {sr:.2f}")
print(f"multi-query  recall@3: {mr:.2f}")

What you should see: single lands around 0.33–0.67, multi around 1.00. The query "make it cheaper" shares no content words with "Reducing subscription costs" — BM25 whiffs — but the paraphrase "reduce subscription cost" matches directly, so the union recovers it. Same mechanism as before: variation covers phrasings a single query can't.

The cost note: multi-query multiplied your retrieval calls by 3 and added an LLM call to generate the paraphrases. On a recall-bound system that's a bargain; on an already-high-recall system it's pure latency for no gain. Measure before adopting — the lift is real only when single-query recall is your bottleneck.

Going further: implement real HyDE — prompt claude-opus-4-8 for a one-paragraph hypothetical answer, embed it (Ch. 4's embed()), and retrieve by that vector. Compare its recall to multi-query on the same cases; you'll find HyDE wins when question and answer vocabularies diverge most.

Case study

TravelDesk: one question, two facts

TravelDesk's policy assistant fielded questions like "Can I expense a business-class flight to Tokyo?" — and got them wrong more often than not, at 0.58 answer accuracy. The problem wasn't retrieval quality; it was that the question needed two separate facts — the cabin-class rule (business class is allowed only over 8 hours) and the destination tier (is Tokyo long-haul?) — which lived in different documents. A single retrieval pulled chunks about one and starved the other.

They added a query-decomposition step: the model breaks a compound question into sub-questions ("What's the business-class flight-time threshold?" and "What's the flight time to Tokyo?"), retrieves for each, and synthesizes. Answer accuracy on multi-fact queries rose to 0.83. They also normalized vague queries against the conversation ("what about Osaka?" becomes a full standalone question) before retrieving, since a retriever can't act on a pronoun.

Running case · Acme Vault

Acme rewrites terse queries using recent context ("it's broken" → the endpoint just discussed) and expands vault export to its synonyms ("dump", "backup"). Those rewrites recover about 15% of previously-missed retrievals.

Quiz · Chapter 7 — reasoning, not recall

  1. Why does multi-query retrieval raise recall?
  2. HyDE embeds a hypothetical answer instead of the question because:
  3. Your system already has high single-query recall. Adding multi-query gives ~no recall gain but triples retrieval latency. The right call is:
  4. A user asks "which plan is cheapest and does it include 2FA?" — a multi-hop question. The best transform is:
  5. Query transformation is attractive because it improves retrieval:

← Back Continue →

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