Skip to main content

Query Enhancement

TL;DR
  • The query you send the retriever decides how good the context is — so fix the query first.
  • Expansion adds synonyms/related terms; decomposition splits a complex question; HyDE embeds a draft answer instead of the question.
  • All three run before retrieval — the LLM only ever sees better context, not your raw phrasing.

A short or vague query retrieves weak chunks, and weak chunks mean a weak answer. Query enhancement reformulates the question into something the retriever handles better. One submodule per technique, ending with a cheat sheet.

Query enhancement: a raw query fans out to expansion, decomposition, and HyDE, all feeding a better retrieval

Reading the diagram: the raw user query is short and one-phrased. Each technique reshapes it into something that matches the stored documents better, and the improved query — not the original — is what hits the retriever.

Query Expansion (synonyms & context)

The simplest enhancement: ask an LLM to rewrite the query with synonyms, technical terms, and useful context, so it overlaps with more of the relevant chunks.

from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

query_expansion_prompt = PromptTemplate.from_template("""
You are a helpful assistant. Expand the following query to improve document
retrieval by adding relevant synonyms, technical terms, and useful context.

Original query: "{query}"

Expanded query:
""")

query_expansion_chain = query_expansion_prompt | llm | StrOutputParser()
query_expansion_chain.invoke({"query": "Langchain memory"})
# → "LangChain memory, conversation buffer, chat history,
# ConversationBufferMemory, persistent memory in LLM apps…"

Then you wire that expanded query into a normal RAG pipeline — expand, then retrieve:

from langchain_core.runnables import RunnableMap
from langchain.chains.combine_documents import create_stuff_documents_chain

retriever = vectorstore.as_retriever(search_type="mmr", search_kwargs={"k": 5})
document_chain = create_stuff_documents_chain(llm=llm, prompt=answer_prompt)

rag_pipeline = (
RunnableMap({
"input": lambda x: x["input"],
# expand the question, then retrieve with the expanded text
"context": lambda x: retriever.invoke(
query_expansion_chain.invoke({"query": x["input"]})
),
})
| document_chain
)

rag_pipeline.invoke({"input": "What types of memory does LangChain support?"})

The expanded query catches chunks that the original wording would have missed (e.g. a doc that says "conversation buffer" when the user typed "memory").

Query Decomposition (split complex questions)

A single complex question often needs facts from several different chunks. Decomposition asks the LLM to break it into simpler sub-questions, retrieve for each, then combine.

decompose_prompt = PromptTemplate.from_template("""
Break the following question into 2-4 simpler, standalone sub-questions
that together cover everything needed to answer it.

Question: "{query}"
Sub-questions (one per line):
""")

decompose_chain = decompose_prompt | llm | StrOutputParser()

# "Compare LangChain and CrewAI memory" →
# 1. How does LangChain handle memory?
# 2. How does CrewAI handle memory?
sub_qs = decompose_chain.invoke({"query": "Compare LangChain and CrewAI memory"}).split("\n")

# retrieve for each sub-question, then merge the contexts
context = []
for q in sub_qs:
context += retriever.invoke(q)

This is the fix for "the answer is split across the corpus" — each sub-question pulls its own focused chunks instead of one blurry query trying to cover everything.

HyDE (Hypothetical Document Embeddings)

A question and a document don't look alike, so a question's embedding can sit far from the chunks that answer it. HyDE flips this: have the LLM draft a hypothetical answer, embed that, and retrieve with it — answer-like text matches real documents far better.

HyDE: question to a drafted answer to its embedding to retrieving the real docs

hyde_prompt = PromptTemplate.from_template("""
Write a short, plausible paragraph that answers the question below.
It does not need to be correct — it's only used to improve search.

Question: "{query}"
Hypothetical answer:
""")

hyde_chain = hyde_prompt | llm | StrOutputParser()

def hyde_retrieve(question):
draft = hyde_chain.invoke({"query": question}) # throwaway fake answer
return retriever.invoke(draft) # retrieve with the draft's vector

docs = hyde_retrieve("What is LCEL in LangChain?") # then answer from the REAL docs

The draft is never shown to the user — it exists only to produce a better search vector. The cost is one extra LLM call per query, so reach for HyDE when plain retrieval keeps missing the right chunks.

Cheat sheet

TechniqueWhat it doesCode shape
Expansionadd synonyms/contextexpansion_prompt | llm | StrOutputParser() then retrieve
Decompositionsplit into sub-questionsretrieve per sub-question, merge contexts
HyDEembed a drafted answerretriever.invoke(hyde_chain.invoke({...}))
Wire into RAGexpand → retrieve → answerRunnableMap({...}) | create_stuff_documents_chain(...)
⚠ Common mistakes
  • Adding an LLM call you don't need — enhancement costs latency; only use it when plain retrieval is actually weak.
  • Letting expansion drift off-topic — a too-greedy prompt can add unrelated terms and hurt precision. Keep it tight.
  • Forgetting HyDE's draft can be wrong — that's fine, it only shapes the search vector; the final answer must still come from the real retrieved docs.
  • Decomposing simple questions — overhead with no benefit; save it for genuinely multi-part queries.

Quick self-check

Why does enhancing the query improve the final answer?

Retrieval quality sets a ceiling on answer quality — a better query retrieves more relevant chunks, so the LLM has better context to answer from.

What problem does HyDE specifically solve?

A question's embedding sits far from the documents that answer it. A drafted answer is written like a document, so its embedding lands closer to the real source chunks.

When is query decomposition the right tool?

When one question needs facts spread across multiple chunks — split it into sub-questions, retrieve for each, then combine.

Is the HyDE draft shown to the user?

No. It's throwaway — used only to produce a better search vector. The final answer comes from the real retrieved documents.

Related: Hybrid Search · Embeddings · Glossary

Next: Multi-Modal RAG →