Query Transformation
By the Syncfusion DevTutorials team · Last updated
Improving questions before search: disambiguation and paraphrasing, conversational rewriting, query routing, sub-queries, HyDE, context-aware expansion, vague queries and multi-query fusion with RRF.
A good librarian does not take every question literally. Ask "do you have that book about the boy wizard?" and they understand which book you mean. Ask "the second one?" a minute later and they know you mean the sequel. Ask "where are the cookbooks and when do you close?" and they answer both parts separately. And when you say "something like the last one", they search in a few different ways to be sure.
Query transformation gives your RAG system those librarian skills: it improves the question before searching.
The main idea: users ask short, vague, multi-part and follow-up questions, in words that differ from your documents. Before searching, you can rewrite them (disambiguate, paraphrase, make follow-ups standalone), route them to the right index, expand them with synonyms or a hypothetical answer (HyDE), and search several variants at once, merging the results with RRF.
A Real-World Example
Brightgrid studied a week of real chats. Many searches failed, not because the answer was missing, but because of how people asked:
- "can I cancel it?", asked right after a question about Brightgrid Plus. The search for "cancel it" found the article about cancelling orders.
- "how much is Plus and how do I set up the Glow Strip?" One search returned only Plus results, so the setup half was never answered.
- "money back for a broken light". The help center says "refund", "faulty" and "warranty", none of which appear in the question.
- "hi!" This triggered a search, which returned random articles.
The team added a small query step before retrieval:
- Follow-ups are rewritten into standalone questions using the chat history.
- Multi-part questions are split into sub-queries.
- Short or vague questions get synonyms added.
- Greetings skip search entirely.
Failed searches dropped by half, without changing the index at all.
In simple words: fix the question before you search for the answer.
What You Will Learn
In this lesson, you will learn:
- Query transformation: disambiguation, paraphrasing and sub-query generation, plus conversational rewriting and query routing.
- Advanced query patterns: HyDE, context-aware expansion and vague-query resolution.
- Multi-query fusion: parallel searches, RRF across variants, deduplication and weighting.
The RAG Lab's Retrieval settings have Expand with synonyms and Multi-query. With multi-query on, the lab searches the original question, an expanded version and a keywords-only version, then merges them with RRF. The Trace tab lists each variant.
Query transformation happens before retrieval: rewrite follow-ups into standalone questions, route to the right index, split multi-part questions, expand with synonyms or a hypothetical answer, then search the variants and merge them with RRF.
Query Transformation
Query Disambiguation & Paraphrasing
Ambiguous questions can mean several things: "lamp setup" could be the Lumo Lamp or the Glow Strip, and "reset" could be a password or a device. Options:
- Ask the user when the difference matters: "Do you mean the Lumo Lamp or the Glow Strip?"
- Use context: the user's products, the page they're on, or earlier messages.
- Paraphrase into clearer wording: "my light won't hook up to the internet" becomes "lamp will not connect to Wi-Fi".
Paraphrasing is usually done by a language model with a short instruction. It's fast with a small model, and the original question is always kept too.
Conversational Query Rewriting
In a chat, the latest message often depends on earlier ones: "Can I cancel it?", "What about the EU?", "And the other one?". Searching for that message alone fails. Rewrite it into a standalone question first, using the chat history:
Turn a follow-up into a standalone question
// Conversational rewriting: turn a follow-up into a standalone question before searching.
const history = [
{ role: "user", text: "How much does Brightgrid Plus cost?" },
{ role: "assistant", text: "Plus costs $3.99 a month [S1]." },
];
const followUp = "Can I cancel it any time?";
// Find the last thing the user talked about (a very simple rule; real apps ask a model).
const topics = ["Brightgrid Plus", "Lumo Lamp", "Glow Strip", "Brightgrid Hub"];
const lastTopic = [...history]
.reverse()
.map((m) => topics.find((t) => m.text.includes(t)))
.find(Boolean);
const standalone = followUp.replace(/\b(it|that|this one)\b/i, lastTopic);
console.log("follow-up: ", followUp);
console.log("rewritten: ", standalone);
console.log("Search for the rewritten question, not the follow-up.");
Expected output:
follow-up: Can I cancel it any time?
rewritten: Can I cancel Brightgrid Plus any time?
Search for the rewritten question, not the follow-up.
Real systems ask a small language model: "Rewrite the last user message as a standalone question, using the conversation. Don't answer it." Keep the history short, like the last few turns, and search for the rewritten question.
Query Routing
Routing decides where to search, or whether to search at all:
- No search for greetings, thanks and small talk.
- Live systems for live data: "where is my order?" needs the orders database or an API, not the help center.
- A specific index or category when the topic is clear: billing questions go to billing articles.
- The default knowledge base otherwise.
Route each question
// Query routing: decide where (or whether) to search before searching.
const routes = [
{
name: "no search (small talk)",
test: (q) => /^(hi|hello|thanks|thank you|bye)\b/i.test(q),
},
{
name: "orders database (live data)",
test: (q) => /\border\b.*\b(where|status|track)|\b(BG|A)-\d+/i.test(q),
},
{
name: "billing articles",
test: (q) => /\b(price|cost|plus|invoice|refund|pay)/i.test(q),
},
{ name: "help center (default)", test: () => true },
];
for (const q of [
"Hi there!",
"Where is order A-1042?",
"How much does Plus cost?",
"Lamp keeps flickering",
]) {
console.log(`${q.padEnd(28)} -> ${routes.find((r) => r.test(q)).name}`);
}
Expected output:
Hi there! -> no search (small talk)
Where is order A-1042? -> orders database (live data)
How much does Plus cost? -> billing articles
Lamp keeps flickering -> help center (default)
Routing can use simple rules, like this cell, a small classifier, or a language model choosing from a list of routes. Start with rules for the obvious cases.
Sub-Query Generation for Multi-Part Questions
A question with several parts ("How much is Plus, and how do I set up the Glow Strip?") needs several searches. One combined search tends to favour one part and miss the other. Split it into sub-queries, retrieve for each, and give the model the chunks for every part.
Split a two-part question
// Sub-queries: split a question with several parts, search each part separately.
const question = "How much is Plus, and how do I set up the Glow Strip?";
const parts = question
.replace(/\?$/, "")
.split(/,?\s+and\s+|;\s*/i)
.map((p) => p.trim())
.filter((p) => p.split(" ").length >= 3)
.map((p) => (p.endsWith("?") ? p : p + "?"));
parts.forEach((p, i) => console.log(`sub-query ${i + 1}: ${p}`));
console.log(
"Each sub-query gets its own top-k. The results are merged before generation.",
);
Expected output:
sub-query 1: How much is Plus?
sub-query 2: how do I set up the Glow Strip?
Each sub-query gets its own top-k. The results are merged before generation.
Advanced Query Patterns
Hypothetical Document Embeddings (HyDE)
Questions and documents are written differently. A question is short and asks; a document is longer and states facts. HyDE bridges the gap:
- Ask a language model to write a short hypothetical answer, even if it might be wrong.
- Search with that answer instead of, or as well as, the question.
The fake answer is worded like your documents, so it often lands closer to the right chunk. It's used only for searching and is never shown to the user.
Search with a hypothetical answer
// HyDE (Hypothetical Document Embeddings): first write a fake answer, then search with it.
// A fake answer is worded like the documents, so it often matches better than the question.
const docs = {
"wifi-troubleshooting#2":
"move the lamp within 5 metres of the router, check that a 2.4 ghz network is on, and reset the lamp",
"hub-pairing#2":
"in the app, open the hub, press add light and hold the device's button for 5 seconds",
};
const words = (t) => new Set(t.toLowerCase().match(/[a-z0-9.]+/g));
const overlap = (a, b) => [...words(a)].filter((w) => words(b).has(w)).length;
const question = "why does my light keep dropping off the internet?";
// In a real app a language model writes this. It may get details wrong; that's fine,
// because it's only used for searching, never shown to the user.
const hypothetical =
"Move the lamp closer to the router and make sure the 2.4 GHz network is on.";
for (const [label, text] of [
["question", question],
["hypothetical answer", hypothetical],
]) {
const best = Object.keys(docs).sort(
(a, b) => overlap(text, docs[b]) - overlap(text, docs[a]),
)[0];
console.log(
`${label.padEnd(20)} -> ${best} (${overlap(text, docs[best])} shared words)`,
);
}
Expected output:
question -> hub-pairing#2 (2 shared words)
hypothetical answer -> wifi-troubleshooting#2 (9 shared words)
HyDE costs one extra model call before every search, which adds latency. It can also mislead when the model confidently "answers" with wrong terms, for example a feature your product doesn't have. Use it for vague, descriptive questions, measure it on your gold set, and keep the original question as one of the searches.
Context-Aware Query Expansion
Expansion adds words the documents might use: synonyms ("refund" for "money back"), your product names ("lamp" for "light"), acronyms ("BG" becomes "Brightgrid"), and context such as the user's product or region. Keep a domain synonym list, built from failed searches in your logs, and add the extra words to the query.
Without expansion
With synonym expansion
Vague Query Resolution
Very short questions like "price?", "not working" or "help" don't contain enough to search well. Options:
- Use context: the product page they're on, or their last order.
- Ask a clarifying question with a few choices.
- Search broadly, then show the top topics and let the user pick.
Multi-Query Fusion
Parallel Multi-Query Vector Search
Instead of betting on one version of the question, generate several: the original, a paraphrase, an expanded version, a keywords-only version. Search them in parallel, so total time stays about the same as one search.
Reciprocal Rank Fusion across Expanded Queries
Merge the result lists with RRF (Lesson 12). Chunks found by several variants rise to the top. A chunk that only one odd variant found stays low.
Deduplication of Multi-Query Results
Variants find many of the same chunks. RRF adds scores per chunk id, so each chunk appears only once in the merged list. Never pass the same chunk twice to the model.
Score Weighting Across Query Variants
Not all variants deserve equal trust. Give the original question the highest weight, and generated variants a little less, for example 0.8, so a strange paraphrase cannot take over.
Merge three variants with weighted RRF
// Multi-query fusion: search several versions of the question, then merge with RRF.
const resultsPerVariant = {
"money back for broken lamp": ["warranty#3", "returns-policy#3", "cancel-order#1"],
"refund for faulty lamp": ["returns-policy#3", "warranty#1", "warranty#3"],
"broken lamp warranty claim": ["warranty#3", "warranty#1", "safety#2"],
};
const weights = {
"money back for broken lamp": 1.0,
"refund for faulty lamp": 0.8,
"broken lamp warranty claim": 0.8,
};
const score = {};
for (const [variant, ids] of Object.entries(resultsPerVariant)) {
ids.forEach((id, rank) => {
score[id] = (score[id] ?? 0) + weights[variant] / (60 + rank + 1);
});
}
// Each chunk appears once in the output, however many variants found it.
Object.entries(score)
.sort((a, b) => b[1] - a[1])
.forEach(([id, s]) => console.log(`${id.padEnd(18)} ${s.toFixed(4)}`));
Expected output:
warranty#3 0.0422
returns-policy#3 0.0292
warranty#1 0.0258
cancel-order#1 0.0159
safety#2 0.0127
Multi-query: three variants merged with RRF
Every transformation can also hurt. A rewrite can change the meaning, expansion can add noise, and HyDE can invent terms. Measure each one on your gold set, and keep only the ones that raise the hit rate on your real questions.
Common mistakes
- Ignoring chat history, so follow-up questions fail.
- Rewrites that change the meaning, like dropping "not" or changing the product.
- Too many variants, which add cost and noise for little gain.
- HyDE for exact lookups like order numbers or codes, where it only adds noise.
- Duplicated chunks from several variants reaching the model.
Exercises
Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.
What to Remember
- Improve the question before searching: disambiguate, paraphrase, and rewrite follow-ups into standalone questions using the chat history.
- Route questions: no search for small talk, live systems for live data, the right index for clear topics.
- Split multi-part questions into sub-queries.
- Expand with domain synonyms, use context, or HyDE (search with a hypothetical answer). Clarify vague questions.
- Multi-query: search several variants in parallel, merge with RRF, deduplicate, and weight the original highest.
- Measure every transformation on your gold set.
Next: Chapter 5, Reranking, a second, more careful pass over the candidates.