What is RAG, and What Problem Does It Solve?
An LLM's knowledge is frozen at its training cutoff. It knows nothing about your product catalogue, your order history, your internal documentation, or anything that changed after training. The naive fix is to include your data in the prompt โ but a context window has a token limit, and sending an entire product catalogue on every request is expensive, slow, and often impossible.
Retrieval-Augmented Generation (RAG) solves this by storing your data as vector embeddings in a database and retrieving only the relevant chunks at query time. The model gets exactly the context it needs โ not everything, just the right thing.
/*
* BEFORE โ stuffing the entire product catalogue into the prompt.
* Breaks at scale: 50,000 products ร 200 tokens each = 10 million tokens.
* gpt-4o has a 128k token context. This does not fit, costs a fortune,
* and degrades answer quality as the context grows.
*/
String allProducts = productRepository.findAll().stream()
.map(Product::toString)
.collect(Collectors.joining("\n"));
String answer = chatClient.prompt()
.user("Here are all our products:\n" + allProducts // token explosion
+ "\n\nCustomer asks: " + customerQuestion)
.call()
.content();
/*
* AFTER โ RAG. Retrieve the 5 most relevant product chunks, send only those.
* Works at 50,000 products, at 5 million products. Cost and latency are
* proportional to the retrieved context size, not the catalogue size.
*/
String answer = chatClient.prompt()
.advisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5)
.similarityThreshold(0.6)
.build())
.build())
.user(customerQuestion)
.call()
.content();
// Spring AI retrieves the 5 most semantically similar product chunks,
// injects them into the prompt automatically, then calls the model.
Fine-tuning re-trains the model on your data. It is expensive ($thousands for a large model), requires ML expertise, and the result is a static snapshot that goes stale when your data changes.
RAG retrieves fresh data at query time from a vector store you control. It is cheap, keeps knowledge current, and requires no ML expertise. For the vast majority of enterprise use cases โ product catalogues, documentation, order history, policy documents โ RAG is the right choice. Fine-tuning is for teaching the model a new communication style or domain-specific reasoning pattern, not for injecting factual data.
Embeddings โ Text as Vectors
An embedding is a dense numerical vector (typically 768โ3072 numbers) that represents the semantic meaning of a piece of text. Two texts that mean the same thing in different words have vectors that are close together in this high-dimensional space. Two texts with unrelated meanings are far apart.
This is the mechanism that makes semantic search possible: you embed the user's question and retrieve the stored texts whose embeddings are closest to it โ not by keyword match, but by meaning.
// EmbeddingModel is auto-configured from your Spring AI starter.
// You rarely call it directly โ VectorStore uses it internally.
@Service
public class ProductIngestionService {
private final VectorStore vectorStore;
public ProductIngestionService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
// Call this once at startup, or whenever products change.
// VectorStore embeds each document automatically using the configured EmbeddingModel.
public void ingestProducts(List<Product> products) {
List<Document> documents = products.stream()
.map(p -> {
// The text you embed determines what queries will match.
// Include all fields a customer might search for.
String content = String.format(
"Product: %s. Category: %s. Description: %s. Price: %s.",
p.name(), p.category(), p.description(), p.price());
return new Document(content, Map.of(
"productId", p.id(),
"category", p.category(),
"price", p.price().toString()
));
})
.toList();
vectorStore.add(documents);
// Each Document is sent to the EmbeddingModel, which returns a vector.
// The vector + original text + metadata are stored in the vector store.
}
}
Embedding quality determines retrieval quality. If you embed only the product name, a query for "waterproof shoes for rain" will not find "Trail Runner Pro" unless its name mentions waterproofing. Include every field a user might search for โ name, category, description, tags. Think of it as choosing what goes into a search index: garbage in, garbage out.
Vector Stores โ Where Embeddings Live
Spring AI's VectorStore interface abstracts over different vector
databases. You write code against the interface; the implementation is a
dependency choice.
| Implementation | When to use | Dependency |
|---|---|---|
SimpleVectorStore |
Development and tests. In-memory, no persistence. Disappears on restart | Built-in (no extra dependency) |
PgVectorStore |
Production with PostgreSQL already in the stack. The most common enterprise choice | spring-ai-starter-vector-store-pgvector |
RedisVectorStore |
Low-latency retrieval, Redis already in the stack | spring-ai-starter-vector-store-redis |
ChromaVectorStore |
Dedicated vector DB, standalone deployment | spring-ai-starter-vector-store-chroma |
PgVector โ production setup
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
# application.properties
spring.ai.vectorstore.pgvector.index-type=HNSW
spring.ai.vectorstore.pgvector.distance-type=COSINE_DISTANCE
spring.ai.vectorstore.pgvector.dimensions=1536 # must match the embedding model's output size
spring.ai.vectorstore.pgvector.initialize-schema=true # creates the vector_store table on startup
// VectorStore is auto-configured โ inject it directly.
@Service
public class ProductSearchService {
private final VectorStore vectorStore;
public ProductSearchService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public List<Document> findSimilarProducts(String query) {
return vectorStore.similaritySearch(
SearchRequest.builder()
.query(query)
.topK(5)
.similarityThreshold(0.6) // 0โ1; below this, the result is noise
.filterExpression("category == 'footwear'") // metadata filter
.build()
);
}
}
The Full RAG Pipeline
RAG has two phases that run at different times:
/*
* INGESTION PHASE (run once, or on data change)
*
* Raw data โ chunk โ embed โ store
*
* Product descriptions are split into chunks (if long), each chunk is sent
* to the embedding model, the resulting vector is stored in pgvector
* alongside the original text and metadata.
*
* RETRIEVAL PHASE (run on every user query)
*
* User query โ embed query โ find nearest neighbours โ inject into prompt โ generate
*
* The user's question is embedded with the SAME model used at ingestion.
* The nearest stored vectors are retrieved. Their text is injected into
* the prompt context. The model answers using that context.
*/
QuestionAnswerAdvisor โ the wiring in one line
@Service
public class ProductAssistantService {
private final ChatClient chatClient;
public ProductAssistantService(ChatClient.Builder builder,
VectorStore vectorStore) {
this.chatClient = builder
.defaultSystem("""
You are a product assistant for an e-commerce platform.
Answer questions ONLY using the product information provided in the context.
If the context does not contain relevant information, say so clearly โ
do not invent product details.
""")
.defaultAdvisors(
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5)
.similarityThreshold(0.6)
.build())
.build()
)
.build();
}
public String answer(String customerQuestion) {
return chatClient.prompt()
.user(customerQuestion)
.call()
.content();
// What happens inside .call():
// 1. QuestionAnswerAdvisor embeds the question
// 2. Retrieves the 5 most similar product documents from pgvector
// 3. Injects them into the prompt context
// 4. Calls the model with the augmented prompt
// 5. Returns the model's answer
}
}
Chunking โ when documents are long
// A product description of 50 words fits in one document.
// An order history PDF of 20 pages does not โ it needs chunking.
// Spring AI provides TokenTextSplitter for this.
@Component
public class DocumentIngestionService {
private final VectorStore vectorStore;
private final TokenTextSplitter splitter = new TokenTextSplitter();
public DocumentIngestionService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void ingestPolicyDocument(String documentText, String documentId) {
Document doc = new Document(documentText,
Map.of("source", documentId, "type", "policy"));
List<Document> chunks = splitter.apply(List.of(doc));
// Each chunk is ~512 tokens with ~100-token overlap between chunks.
// Overlap ensures context around a concept is not split across boundaries.
vectorStore.add(chunks);
}
}
Tuning and Common Pitfalls
| Problem | Symptom | Fix |
|---|---|---|
| Wrong embedding model at query time | Retrieval returns garbage โ completely unrelated documents | You must use the same embedding model for ingestion and retrieval. If you switch models, you must re-embed all stored documents |
| Similarity threshold too low | Retrieval returns many irrelevant documents; model hallucinates from noise | Raise similarityThreshold. Start at 0.6, measure
retrieval quality, adjust. Below 0.5, results are typically
noise |
| Chunk size too large | Retrieved chunks contain the right section plus a lot of irrelevant surrounding text, diluting the answer | Reduce chunk size and increase topK โ retrieve more, smaller, focused chunks |
| Metadata not filtered | A customer question about shoes retrieves electronics documentation | Store category/type in document metadata and apply a
filterExpression in the search request |
| Ingestion not triggered on data change | The model answers with stale product information | Trigger re-ingestion from an application event or a Kafka consumer when products are updated. Track ingested documents by ID and delete/re-add on change |
Best Practices
โ Do use the same embedding model for ingestion and retrieval. Different models produce incompatible vector spaces โ retrievals will be meaningless.
โ
Do include a similarityThreshold. Without it,
the vector store returns the top-K documents regardless of how dissimilar
they are โ including completely unrelated content that poisons the prompt.
โ Do store metadata on every document (source ID, category, timestamp) and filter on it. Metadata filters are evaluated before vector similarity โ they are free, and they dramatically improve retrieval precision.
โ Do instruct the model explicitly to answer only from the provided context and to say so when the context does not contain the answer. Without this instruction, the model will blend retrieved context with its training knowledge โ producing confident answers that mix real data with hallucinated data.
โ Do measure retrieval quality separately from generation quality. Log which documents were retrieved for each query. If retrieval is bad, better prompts do not help โ fix the embedding and chunking strategy first.
โ Don't embed entire documents without chunking when documents exceed ~500 tokens. A single embedding for a long document averages out the meaning of the whole โ it retrieves the document when any part is relevant, not when the most relevant part is closest to the query.
โ Don't skip re-ingestion when source data changes. Stale embeddings produce stale answers. Build ingestion into your data pipeline, not as a one-off startup job.
โ Don't use RAG as a substitute for a proper search index. RAG retrieves by semantic similarity. Exact-match requirements (order ID lookup, SKU search) are better served by a structured query. RAG and traditional search are complementary, not alternatives.
Interview Questions
Q: What is RAG and why is it used instead of just putting all
your data in the prompt?
RAG (Retrieval-Augmented Generation) retrieves only the relevant chunks
of data at query time and injects them into the prompt, rather than
including everything. Context windows have token limits โ you cannot fit
a large product catalogue into a single prompt. RAG also reduces cost
(you pay per token), improves answer quality (focused context beats
noisy context), and keeps knowledge current because you control the
vector store.
Q: What is an embedding?
An embedding is a numerical vector โ typically hundreds to thousands of
floating-point numbers โ that represents the semantic meaning of a piece
of text. Text with similar meaning produces vectors that are close in
vector space. This closeness is how semantic search works: embed the
query, find the stored vectors nearest to it, retrieve those documents.
Q: Why must you use the same embedding model for ingestion and
retrieval?
Different embedding models produce vectors in different spaces โ the
dimensions do not correspond. A vector from model A compared to a vector
from model B produces a meaningless similarity score. If you switch
embedding models, you must discard all stored embeddings and re-embed
every document with the new model.
Q: What is the difference between RAG and fine-tuning, and when
is each appropriate?
Fine-tuning modifies the model's weights by training on your data. It is
expensive, requires ML expertise, and produces a static snapshot that goes
stale when data changes. RAG retrieves fresh data at inference time from
a vector store you control. It is cheap, keeps knowledge current, and
requires no ML expertise. Fine-tuning is appropriate for teaching the
model a communication style, a domain-specific reasoning pattern, or
a new task format. RAG is appropriate for factual knowledge โ product
catalogues, policy documents, order history. For most enterprise
applications, RAG is the right tool.
Q: Your RAG system is returning relevant documents but the model
is still giving wrong answers. What do you investigate?
Retrieval and generation are separate failure modes. If retrieval is
confirmed good, investigate: (1) chunk size โ the retrieved chunk may
contain the right section surrounded by distracting context; (2) topK โ
too many chunks can dilute the relevant signal; (3) system prompt โ without
an explicit instruction to answer only from context, the model blends
retrieved data with training knowledge; (4) similarity threshold โ a
threshold that is too low retrieves noise that poisons the prompt; (5)
document quality โ if the ingested text is poorly structured, even correct
retrieval produces hard-to-use context.
Q: How do you handle data freshness in a RAG system โ products
change, prices update, policies are revised?
Ingestion must be event-driven, not a one-off startup job. The cleanest
pattern: an application event (or a Kafka consumer) triggers re-ingestion
whenever a product, document, or policy is updated. Documents should be
stored with a source ID in their metadata so you can delete the old
embedding and add the new one atomically. For high-frequency updates, a
scheduled full re-ingestion may be simpler than tracking individual
changes โ the vector store is rebuilt nightly, and stale data is bounded
to hours rather than days.