The question is not the query
Query rewriting means changing a question before you search with it, so the search finds more. It matters because people type short, vague or two-part questions, and an index only knows the words it holds.
"How do I get my money back" shares no word with the page that says "refund". "And how long?" shares no useful word with any page.
By the end of this lesson
Note
I can rewrite a question so it finds more, and check against known sources that the rewrite helped.
Key words
| Word | What it means |
|---|---|
| Query | the text a search actually receives |
| Expansion | adding related words to a query |
| Decomposition | splitting a question into smaller ones |
| Follow-up | a question that only makes sense with the chat so far |
| Synonym | a different word for the same idea |
| Known source | the page a test question should find |
The cells students fill in are left blank in this preview.
Expanding adds the words the page uses
A page about refunds says "refund". A user says "money back". Expansion adds related words to the query: "money back" becomes "money back refund return". A table of synonyms does it. A model can suggest them too.
Keep the original words and add to them. An expansion that replaces the user's words can lose the one exact word that mattered.
A two-part question is two searches
Take "Are you closed on Sunday and can I return a parcel". One search returns the page that fits one half best. The other half is lost.
Decomposition splits the question where it says "and" and searches each part. Then you merge the lists, for example with reciprocal rank fusion.
A follow-up needs the chat so far
"And how long do I have?" means nothing alone. Join it to the earlier question, "can I return a parcel", or have a model write one question that stands alone.
Never send the whole chat. Long queries drift. Add only the last question, and only when the new one is short.
A common mistake
Warning
Many people think a rewrite always helps. A synonym can match the wrong page and a long history can pull the query off course. Keep a set of test questions with known sources and count the hits before and after every change.
Write the model helper, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// The token counts of a reply, or null when the reply carries none.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the model helper, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' const data = await res.json();' ' const text = data.choices[0].message.content;' ' return { text, inTokens: count(data, \"prompt_tokens\"), outTokens: count(data, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the rewriter, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const docs = {' ' hours: \"the shop opens at nine on saturday and closes at one\",' ' sunday: \"the shop is closed on sunday\",' ' returns: \"you can return a parcel within thirty days for a refund\",' ' delivery: \"delivery takes three days and costs four pounds\",' '};' 'const synonyms = { money: [\"refund\", \"return\"], back: [\"return\"] };' > rewrite1.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the rewriter, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function stem(w) { return w.length > 3 && w.endsWith(\"s\") ? w.slice(0, -1) : w; }' 'function words(t) { return t.toLowerCase().replace(/[?,]/g, \"\").split(\" \").map(stem); }' 'function best(q) {' ' const w = words(q);' ' let id = \"nothing\", top = 0;' ' Object.keys(docs).forEach(function (k) {' ' const n = words(docs[k]).filter(function (x) { return w.includes(x); }).length;' ' if (n > top) { top = n; id = k; }' ' });' ' return id;' '}' 'function expand(q) {' ' const extra = [];' ' q.toLowerCase().split(\" \").forEach(function (w) {' ' (synonyms[w] || []).forEach(function (s) { extra.push(s); });' ' });' ' return q + \" \" + extra.join(\" \");' '}' >> rewrite1.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the rewriter, part 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const cases = [' ' { q: \"when do you open on saturday\", want: \"hours\" },' ' { q: \"how do I get my money back\", want: \"returns\" },' ' { q: \"how long does delivery take\", want: \"delivery\" },' '];' 'let plain = 0, wider = 0;' 'cases.forEach(function (c) {' ' const a = best(c.q), b = best(expand(c.q));' ' if (a === c.want) plain++;' ' if (b === c.want) wider++;' ' console.log(c.q + \" | plain: \" + a + \" | expanded: \" + b);' '});' 'console.log(\"hits plain:\", plain, \"expanded:\", wider, \"of\", cases.length);' >> rewrite1.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the rewriter, part 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const two = \"are you closed on sunday and can I return a parcel\";' 'console.log(\"whole question finds:\", best(two));' 'console.log(\"split finds:\", two.split(\" and \").map(best).join(\" + \"));' 'async function main() {' ' const text = \"Earlier: can I return a parcel. Now: how long do I have. Rewrite the last question so it stands alone. Reply with the question only.\";' ' const reply = await ask([{ role: \"user\", content: text }], \"How long do I have to return a parcel?\", 40);' ' console.log(\"rewritten:\", reply.text.trim());' ' console.log(\"finds:\", best(reply.text));' '}' 'main();' >> rewrite1.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run and count the hits
docker exec -w /home/node/project agent node rewrite1.jsShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
This is a stand-in
The finder counts shared words in four pages. A real system searches a real index. The one model call rewrites a follow-up, and the loop around it uses a recorded rewrite when no model answers.
The method is the same at any size: change the query, then count hits on known sources.
What the Run prints
| Question | Plain finds | Expanded finds |
|---|---|---|
| when do you open on saturday | hours | hours |
| how do I get my money back | nothing | returns |
| how long does delivery take | delivery | delivery |
The cells students fill in are left blank in this preview.