Skip to main content

Improving Retrieval Quality

Naive RAG retrieves documents with a single step: embed the user's query, find the nearest neighbors in the vector store, done. This works, but it has predictable failure modes. A short or ambiguous query embeds to a vague vector that doesn't land near anything useful. A query phrased differently from the source documents (different vocabulary, different level of detail) can miss documents that are actually relevant, since embedding similarity captures meaning but isn't immune to wording mismatches. And a single similarity search has no way to combine information spread across several documents, or to catch an exact match on a code, an ID, or a rare term that a semantic embedding tends to blur.

None of this means naive RAG is wrong to start with. It's usually the right first implementation. These are the techniques worth reaching for once it stops being good enough.

Query transformation

Instead of embedding the raw user query as-is, transform it first.

Rewriting cleans up a query before retrieval, for example resolving pronouns and references from earlier conversation turns ("what about the other one?") into something retrievable on its own.

Expansion turns one query into several variants, phrased differently or focused on different aspects of the question, then retrieves for each and merges the results. This helps when a single phrasing might miss relevant documents that use different terminology.

Hypothetical document embeddings (HyDE) takes a different approach: rather than embedding the question, an LLM first drafts a plausible answer to it, and that hypothetical answer gets embedded and searched instead. The idea is that a hypothetical answer is closer, in embedding space, to a real answer than the original question is.

Spring AI's modular RAG support has built-in implementations of some of these ideas: RewriteQueryTransformer, CompressionQueryTransformer for conversation history, and MultiQueryExpander for query expansion. See Modular RAG for the API.

Vector similarity search is good at matching meaning and bad at matching exact tokens. A query for an error code, a product SKU, or an uncommon proper noun can retrieve poorly through pure embedding similarity, because the embedding model was never trained to treat that string as special. Hybrid search runs a traditional keyword search (often BM25 or full-text search) alongside the vector search and combines both result sets, so an exact lexical match isn't lost just because it lacked semantic weight. Most vector databases with hybrid search support let you tune how much weight goes to each side.

Reranking

The retrieval step usually needs to be fast, so it operates over the whole vector store and returns a wide net, maybe the top 50 candidates. That speed comes from using a bi-encoder: query and document are embedded independently, and similarity is a simple vector distance calculation. A bi-encoder never actually looks at the query and document together.

A reranker, typically a cross-encoder model, does look at them together. It takes the query and a candidate document as a pair and scores how relevant that specific pair is, which produces a more accurate ranking than embedding distance alone. Cross-encoders are too slow to run over an entire vector store, which is why reranking is a second stage: retrieve a wide candidate set cheaply with vector search, then rerank just that candidate set and keep only the top few, the ones actually worth sending to the LLM.

This fits naturally as a post-retrieval step. In Spring AI's modular RAG flow, this is what DocumentPostProcessor is for, run after DocumentRetriever and before the query gets augmented with the final document set.

Chunking still matters

All of the above happens after documents are already split into chunks and embedded, but chunk size and overlap shape what these techniques have to work with. Chunks that are too large dilute the embedding with irrelevant surrounding text, which hurts similarity search and gives a reranker more noise to sort through. Chunks that are too small lose the surrounding context needed to make sense of the match. There's no universal right size. It depends on the source material and is usually worth tuning based on evaluation results rather than guessing once and moving on.

Knowing whether any of this helped

Every technique above adds cost, latency, or both, so it's worth confirming each one actually improves results rather than adding it on faith. This is where the retrieval-specific half of evaluation comes in: build a small set of queries with known relevant documents, and check whether the right document shows up in the top-k results before and after a change. A reranker or query rewriter that doesn't move that number for your data isn't worth the extra latency it adds.