TAI BUI
Back to curriculum
CapstonePython

Query Rewriting: HyDE, Multi-Query, and Decomposition

Query rewriting turns what the user typed into something closer to what the answer looks like, before the retriever ever runs. The index can only match the text it is handed.

TIME: ~90 minutesPREREQUISITES: Phase 11 lessons 04 (embeddings), 06 (RAG); Phase 19 Track B foundations (lessons 20-29); Phase 19 lessons 64 and 65

Learning Objectives

  • Implement Hypothetical Document Embeddings (HyDE): generate a fake answer, embed it, retrieve against that vector instead of the query vector.
  • Implement multi-query expansion: rewrite one query into N paraphrases, retrieve with each, merge the union by reciprocal rank fusion.
  • Implement query decomposition: split a complex question into sub-questions, retrieve per sub-question, merge.
  • Compare the three rewriters head to head on a fixture and explain when each strategy wins.
  • Wire a mock LLM that produces deterministic, on-fixture outputs so the rewriter loop runs offline.

The Problem

Query rewriting is the step that transforms a user's query before it reaches the retriever, so the search sees text closer to how the corpus is written. Without it, the retriever has to bridge the vocabulary gap on its own.

Take a user who types "what do we do when a transfer breaks halfway". The corpus contains a doc that says "AbortMultipartOnFail terminates an S3 multipart transfer and decrements the per-bucket retry quota on persistent failure". The query and the document share one word, "transfer". The user says "breaks halfway" where the doc says "persistent failure", so BM25 has little to match on, and the query vector lands nearer to generic transfer and quota docs. In this lesson's demo the hybrid retriever ranks the answer third, behind the transfer manager doc and the quota cooldown doc. The two-stage rerank from lesson 66 can salvage the answer if it sits in the top-N, but a document that never reaches the top-N is invisible to the reranker.

The fix is to rewrite the query before it touches the retriever. The 2023 paper "Precise Zero-Shot Dense Retrieval without Relevance Labels" (Gao et al.) introduced HyDE: ask an LLM to write the document that would answer the query, embed that hypothetical document, and use its embedding as the retrieval vector. The hypothetical document sits in the right region of the embedding space because it is written in the corpus's voice, which the query was not.

Two related techniques pair with HyDE. Multi-query expansion (the term Microsoft's GraphRAG used) generates N paraphrases of the query, retrieves with each, and merges. Decomposition (popularized as "subquery decomposition" in the 2024 Stanford DSPy work) splits "what happens when an upload fails and the retry budget is exhausted" into two questions: "how is an in-flight multipart upload aborted" and "what happens when the retry quota reaches zero". That gives two retrievals and one merged result, with both pieces of the answer within reach.

You will implement all three here and run them against the same fixture corpus.

Warm-up0/3
1. A user's question uses different words than the document that answers it. How does a keyword retriever like BM25 typically handle that?Warm-up
2. In dense retrieval, what decides which documents come back for a query?Warm-up
3. Reciprocal rank fusion merges several ranked lists. What does it use from each list?Warm-up
Check: The Problem0/2
1. What is query rewriting in a retrieval pipeline?Define
2. Why can a reranker not rescue a query whose answer never appears among the retrieved candidates?Explain

The Concept

Mermaid diagram

HyDE in detail

HyDE (Hypothetical Document Embeddings) replaces the user's query vector with the vector of an LLM-written hypothetical document. The prompt is short:

You are a domain expert. Write a one-paragraph passage that answers the question
below. Use the same vocabulary and phrasing the documentation in this domain would
use. Do not refuse. Do not say you do not know.

Question: {user_query}

Passage:

As a factual answer, the LLM's passage is probably wrong, because the model has not seen your corpus. That is fine, since the retriever cares about token distribution and not correctness. The hypothetical passage contains the words "abort", "multipart", "bucket", "budget", because that is what a documentation passage on this topic would say. Embed it and the vector lands near the real passage. In this lesson's code the hypothetical's embedding feeds only the dense side: BM25 still runs on the original query text, and a plain retrieval on the original query joins the fusion as well.

In production, cap the hypothetical at two or three sentences. Longer ones collect noise, and shorter ones lose the lexical signal HyDE needs.

Multi-query expansion in detail

Multi-query expansion generates N paraphrases of the user's query, retrieves top-k for each (and for the original query, so N+1 retrievals), and merges the ranked lists with RRF (the same algorithm from lesson 65). The simplest prompt:

Rewrite the following question in {N} different ways. Each rewrite must preserve
the original intent. Number them 1 to {N}. Do not add explanations.

It is cheap, parallel, and deterministic. It wins when the user's phrasing is one of many equally valid ways to ask the question and some rewrite would have asked it better. It loses when the original was bad in a way that every rewrite inherits.

Decomposition in detail

Decomposition asks the LLM to split a question into sub-questions and retrieves per sub-question, because a single retrieval cannot satisfy a multi-faceted question. The prompt:

The following question may require information from multiple distinct topics.
Decompose it into a list of sub-questions. Each sub-question must be answerable
independently. If the question is already atomic, return it unchanged.

Question: {user_query}

Retrieve per sub-question, then merge. Decomposition fits questions with conjunctions, multi-clause comparisons, or two unrelated topics. It is the wrong tool for atomic questions, where the decomposer should hand back the single question and not invent fake sub-questions.

Why all three exist

They cover different gaps. HyDE bridges the query-corpus token gap, multi-query covers paraphrase variance, and decomposition covers multi-topic queries. A production system can run all three and pick a strategy per query (lesson 69's end-to-end system shows the selector).

A fourth technique, step-back prompting, runs the other way from decomposition. The LLM turns a narrow question into a more general one, you retrieve on the general question, and then you narrow back to the specifics. It helps when the query is too specific for any document to be phrased that way. This lesson does not implement it; exercise 2 adds it.

Check: The Concept0/10
1. What is the core mechanism of HyDE (Hypothetical Document Embeddings)?Define
2. What is multi-query expansion?Define
3. What is query decomposition?Define
4. What makes a query atomic?Define
5. What is step-back prompting?Define
6. Why is it acceptable for the LLM-generated hypothetical document in HyDE to be factually wrong?Explain
7. What is the difference between multi-query expansion and query decomposition?Explain
8. Why does the lesson recommend capping a HyDE hypothetical document to two or three sentences in production?Explain
9. Which pairing of rewriting strategy and the gap it covers is correct?Explain
10. A user asks "how is authorization handled and how do policies get evaluated", and a different doc covers each half. Which strategy gives each half its own retrieval?Apply

The Mock LLM

The mock LLM is a small set of lookup tables keyed on the lowercased user query with any trailing question mark removed, plus a fallback for queries it has not seen. That keeps everything offline. The tables cover:

  • HyDE and multi-query: the same three queries ("what do we do when a transfer breaks halfway", "how is access control handled across user types", "how does the search service merge two retrievers"). Each has a written hypothetical passage and three paraphrases.
  • Decomposition: two different queries ("what happens when an upload fails and the retry budget is exhausted", "how is authorization handled and how do policies get evaluated"), each with two sub-questions.

The demo's third query appears only in the decomposition table, so on that query HyDE and multi-query run on the fallbacks. For an unknown query the fallbacks are deterministic:

  • HyDE writes every token of the query followed by its synonyms from a small synonym map.
  • Multi-query produces three paraphrases by cyclic synonym swaps, replacing alternate tokens with a different synonym on each pass.
  • Decomposition splits on " and ", or returns the query unchanged if it has none.

The shape of the mock matters more than its data. In production you swap the mock for a real model call and the retriever stays as it is.

Check: The Mock LLM0/5
1. What is the mock LLM in this lesson?Define
2. How does this lesson's MockLLM handle a query that is not in its lookup tables?Explain
3. The mock LLM receives the unseen query "retry the upload" in generate_hypothetical. What does it return?Apply
4. The mock LLM's decompose() receives the unseen query "rotate keys and revoke tokens". What does it return?Apply
5. The mock's fallback paraphrase("cancel the upload", n=3) runs for an unseen query. What does it return?Apply

Build It

code/main.py implements:

  • MockLLM - the deterministic stand-in described above.
  • HyDERewriter - calls the LLM to write the hypothetical document and returns a RewriteResult holding the hypothetical text and the query the retriever should use.
  • MultiQueryRewriter - calls the LLM for N paraphrases and returns a list of queries.
  • DecomposeRewriter - calls the LLM to decompose and returns sub-questions.
  • retrieve_with_rewriter - takes a rewriter and a retriever, runs the rewrites, and fuses the results. The multi-query rewriter keeps the original query in its list, so it runs N+1 retrievals. HyDE runs two: one on the hypothetical's vector (with BM25 on the original text) and one plain retrieval on the original query. Decomposition runs one per sub-question and drops the original.
  • A demo that runs the three rewriters plus a no-rewrite baseline on a three-query fixture and prints, for each, whether the gold document ranked first and where it landed.

The retriever shape comes from lesson 65 (hybrid BM25 + dense), with the same RRF fusion. The rewriter interface is the one new piece, and it is small.

Run it:

bash
python3 code/main.py

The output is one row per strategy per query, with a gold@1 flag, the gold document's rank, and a marker on the strategy each query was designed to favor. Gold first, in the order no-rewrite, HyDE, multi-query, decompose:

  • "what do we do when a transfer breaks halfway": no, no, yes, no. Multi-query wins. HyDE moves the gold doc from third to second but not to first, and decomposition finds nothing to split, so it matches the baseline.
  • "how does the search service merge two retrievers": no, yes, no, no. HyDE wins, and the baseline buries the gold doc at rank 5.
  • "what happens when an upload fails and the retry budget is exhausted": yes, no, yes, yes. Decomposition wins on paper, but plain retrieval and multi-query already rank the gold doc first, and HyDE drops it to second.

Each query's designed winner does hit gold first. The no-rewrite baseline loses on the first two queries and ties on the third.

Check: Build It0/11
1. Why does the multi-query rewriter keep the original query in its list alongside the paraphrases?Explain
2. With n=3, how many retrievals does the demo's MultiQueryRewriter run for one query?Apply
3. rrf() with k=60 fuses list A = [d1, d2] and list B = [d2, d3]. Which document comes first?Apply
4. DecomposeRewriter.rewrite("where is the policy engine cached") runs on the mock LLM. How many rewrites come back?Apply
5. How many retrievals does retrieve_with_rewriter run for one query when the rewriter is HyDERewriter?Apply
6. In the demo run on "how does the search service merge two retrievers", which strategy ranks the gold doc first?Apply
7. MultiQueryRewriter(n=3).rewrite(q) runs on a tabled query. What does rewrites[0] hold?Apply
8. In the demo, plain retrieval already ranks the gold doc first on the third query. What is the right takeaway?Analyze
9. A team runs the demo, sees multi-query win on the first query, and concludes multi-query beats HyDE on their own corpus. What is wrong with that conclusion?Analyze
10. Your corpus says "rank fusion" but users ask "how do we merge two retrievers", and plain retrieval buries the right doc. Which rewriter do you try first?Design
11. You are shipping a rewriter whose LLM is sometimes weak. Which design best limits the damage of a bad rewrite?Design

Failure modes the demo will hide

HyDE gets corpus-specific identifiers wrong. The model invents a function name. If BM25 runs on the hypothetical text, its score on the right doc collapses, because the invented name is now a high-weight token that does not appear in the index. The demo avoids this by running BM25 on the original query, so only the dense side sees the invention. Either way, cap the hypothetical's length.

Multi-query rewrites converge. A weak model produces three near-identical paraphrases, the N retrievals return the same top-k, and the RRF merge is no better than a single retrieval. Add an explicit diversity instruction to the rewrite prompt and detect duplicates by Jaccard.

Decomposition over-splits. The decomposer turns an atomic question into a list. The retrievals all return the same document at a reduced rank, and the merge ends up worse than the original. Add a "are these sub-questions distinct enough" pass before fan-out.

Latency multiplies. HyDE costs one LLM call, then two retrievals. Multi-query costs one LLM call to generate N rewrites, then N+1 retrievals. Decomposition costs one LLM call to decompose, then M retrievals. The retrievals can run in parallel, so the LLM call sets the floor.

Check: Failure modes the demo will hide0/5
1. When does query decomposition over-split and hurt retrieval?Explain
2. The lesson says a rewriter's retrievals can run in parallel after its single LLM call. According to the lesson, what sets the latency floor?Explain
3. BM25 runs on the hypothetical text, and the model invents a function name that is not in the corpus. What happens?Analyze
4. A weak model's multi-query paraphrases all converge on nearly the same wording. What is the effect?Analyze
5. A decomposer splits atomic questions into lists. The retrievals all return the same document at a reduced rank, and the merge ends up worse than the original query. Which fix does the lesson suggest?Analyze

Use It

Production patterns:

  • Pick a strategy per query by shape: short atomic queries get multi-query, complex multi-clause queries get decomposition, and jargon-heavy queries get HyDE.
  • Cache the rewriter output by query hash, since many queries repeat.
  • Run all three in parallel and fuse the three result sets with RRF. That costs three LLM calls and one fusion, and the quality is the union of all three strategies' coverage.
Check: Use It0/3
1. According to the lesson's production guidance, which rewriting strategy should a jargon-heavy query be routed to?Explain
2. Why does the lesson recommend caching rewriter output by query hash in production?Explain
3. Your budget allows one rewriter LLM call per query, but you want rewriting. Which design follows the lesson?Design

Ship It

Lesson 69 wires this rewriter stage in front of the retriever from lesson 65 and the reranker from lesson 66. Lesson 68 evaluates how much recall the rewriter adds.

Exercises

  1. Implement RAG-Fusion (a 2024 variant of multi-query) where the rewriter's paraphrases are intentionally diverse, then the rerank step (lesson 66) picks the final list.
  2. Add a fourth strategy: step-back prompting (ask the LLM for the more general question, retrieve on that, then narrow). Compare on the fixture.
  3. Train the decomposer to recognize atomic queries by adding a "is the question atomic" head. Measure the over-split rate before and after.
  4. Replace the mock LLM with a real model call. Measure the latency-per-strategy on your stack.
  5. Add a confidence score per rewrite. Drop rewrites below the threshold. Measure the impact on recall.

Key Terms

TermWhat people sayWhat it actually means
HyDE"Fake-document retrieval"LLM writes the answer; embed and retrieve on that instead of the query
Multi-query"Paraphrase expansion"N rewrites of the query; retrieve N times, merge by RRF
Decomposition"Subquery split"Multi-topic queries split into sub-questions, retrieved separately
Atomic query"Single-topic"Cannot be decomposed without inventing fake sub-questions
Step-back"Abstract the query"Ask the more general question, retrieve, then narrow
Author's Note

My bet is that the mock LLM proves the plumbing and says little about the strategies. Its tables are keyed on exact query strings, so a win in the demo says the rewriter loop and the fusion run correctly, not that HyDE or decomposition would win on your queries. I'd swap in a real model (exercise 4) and run it on questions from your own corpus before trusting any strategy ranking.

I'd keep the original query in the fused set, the way the multi-query rewriter here does, and HyDE keeps BM25 on the original text for the same reason. A weak rewrite then has to compete with the query the user actually typed instead of replacing it.

On the demo's third query, plain retrieval already ranks the gold doc first. A rewriter costs an LLM call, so I'd measure that baseline per query type before paying for rewriting everywhere.

Further Reading

  • Gao, Ma, Lin, Callan, "Precise Zero-Shot Dense Retrieval without Relevance Labels" (HyDE), 2023
  • Microsoft Research, "Multi-Query Expansion for Retrieval"
  • Stanford DSPy, "Subquery Decomposition for Multi-Hop QA"
  • LlamaIndex query transformations documentation
  • Phase 11 lesson 07 - advanced RAG patterns
  • Phase 19 lesson 65 - the retriever this rewriter feeds
  • Phase 19 lesson 68 - the eval that measures the rewriter lift

Code