Large Language Models are powerful.
But there is one fundamental limitation:
An LLM doesn't automatically know your application's private data.
Your company policies, product documentation, internal knowledge base, customer records, PDFs, technical documentation, or database content are not necessarily part of the model's training data.
This is where RAG β Retrieval-Augmented Generation comes in.
Instead of asking an LLM to answer directly, we first retrieve relevant information from our own data and provide that information as context to the model.
In this article, we'll build the foundation of a RAG application using Spring AI.
RAG stands for:
Retrieval-Augmented Generation
The idea is simple:
User Question
β
Retrieve Relevant Information
β
Add Retrieved Context to Prompt
β
LLM
β
Generated Answer
For example, imagine we have a company's internal documentation.
A user asks:
What is our refund policy for annual subscriptions?
Instead of expecting the LLM to magically know the answer, our application:
This is the core idea behind RAG.
A normal LLM application looks like this:
User
β
Application
β
LLM
β
Answer
The problem?
The LLM only has access to the information available to it.
With RAG, the architecture becomes:
ββββββββββββββββ
β Documents β
ββββββββ¬ββββββββ
β
Chunking
β
Embeddings
β
Vector Database
β
β
User β Query β Similarity Search
β
Relevant Context
β
LLM
β
Answer
Now the model can work with information from our own knowledge base.
A production RAG pipeline typically contains these stages:
Documents
β
Document
β
Chunking
β
Embeddings
β
Vector Database
β
Similarity Search
β
Relevant Context
β
Prompt
β
LLM
β
Answer
Let's understand each step.
First, we need to get our data into the application.
The source could be:
Spring AI provides abstractions for working with documents.
A document can be represented using Spring AI's Document
abstraction.
Conceptually:
Document document = new Document(
"Spring Boot is a framework for building Java applications..."
);
We now have content that can be processed by our RAG pipeline.
We shouldn't usually store an entire document as a single vector.
Imagine a 100-page PDF.
A user asks:
How do I configure authentication?
We don't want to retrieve the entire PDF.
Instead, we split the document into smaller pieces called chunks.
For example:
Document
β
Chunk 1
Chunk 2
Chunk 3
Chunk 4
...
Chunk 100
Each chunk represents a smaller piece of knowledge.
A simple example:
Chunk 1:
Introduction to Spring Security
Chunk 2:
Configuring authentication
Chunk 3:
Creating users
Chunk 4:
JWT authentication
Chunk 5:
Role-based authorization
Now when the user asks about JWT authentication, we can retrieve the relevant chunk instead of the entire document.
This is where things get interesting.
A computer doesn't understand the semantic meaning of text in the same way humans do.
We need a way to represent text numerically.
That's where embeddings come in.
An embedding model converts text into a vector.
For example:
"How do I configure JWT authentication?"
might become something conceptually like:
[0.12, -0.42, 0.87, 0.31, ...]
The actual vector contains many dimensions.
The important part is:
Semantically similar text produces vectors that are relatively close together in vector space.
For example:
"How can I configure JWT?"
β
[0.12, 0.81, 0.42, ...]
"JWT authentication configuration"
β
[0.15, 0.78, 0.45, ...]
These vectors should have high similarity.
Now we need somewhere to store these embeddings.
That's where a vector database comes in.
Popular choices include:
For a Spring Boot application, PostgreSQL with pgvector
is an especially interesting option because you can keep your relational data and vector data within the same ecosystem.
Conceptually:
Document Chunk
β
Embedding Model
β
Vector
β
Vector Database
Our database might contain something conceptually like:
ID | Content | Embedding
---|----------------------------|----------------
1 | JWT configuration... | [0.12,...]
2 | OAuth2 configuration... | [0.42,...]
3 | Database configuration... | [0.71,...]
Now suppose the user asks:
How do I configure JWT authentication in Spring Boot?
We first generate an embedding for the question.
User Query
β
Embedding Model
β
Query Vector
Then we search the vector database for similar vectors.
Query Vector
β
Vector Database
β
Similarity Search
β
Top K Relevant Chunks
For example:
Result 1 β JWT configuration
Result 2 β Spring Security authentication
Result 3 β SecurityFilterChain configuration
These results become our context.
Now we combine:
For example:
Context:
Spring Security can be configured using SecurityFilterChain.
JWT authentication can be implemented using a custom
authentication filter...
Question:
How do I configure JWT authentication in Spring Boot?
The LLM receives this information and generates the answer.
This is the Augmented part of Retrieval-Augmented Generation.
The final flow looks like:
User Question
β
Embedding
β
Vector Search
β
Relevant Documents
β
Prompt + Context
β
LLM
β
Answer
The LLM isn't searching the database itself.
Our application retrieves the information first and provides it to the model.
Now let's look at how Spring AI simplifies this architecture.
A typical Spring AI RAG application contains:
Spring Boot
β
βββ Document Reader
β
βββ Text Splitter
β
βββ Embedding Model
β
βββ Vector Store
β
βββ Chat Model
The exact model and vector store can be swapped without rewriting the entire application.
That's one of the strengths of Spring AI's abstraction-based approach.
Let's create a Spring Boot project.
We'll need Spring AI dependencies for:
For example, with Maven:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-bedrock-converse</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
You should use versions compatible with the Spring AI version used by your project.
If we're using AWS Bedrock, our application needs AWS credentials and model configuration.
Conceptually:
spring:
ai:
bedrock:
aws:
region: us-east-1
The exact configuration depends on the Spring AI version and Bedrock model you're using.
For production environments, don't hard-code AWS credentials inside application.yml
.
Use:
Environment Variables
β
IAM Roles
β
AWS Credentials Provider Chain
Whenever possible, prefer IAM roles over static credentials.
For PostgreSQL + pgvector, the architecture looks like:
Spring Boot
β
Spring AI VectorStore
β
PostgreSQL
β
pgvector
The vector store becomes the bridge between our application and the vector database.
Conceptually:
VectorStore vectorStore;
We can then add documents:
vectorStore.add(documents);
And later search:
List<Document> results =
vectorStore.similaritySearch(
SearchRequest.builder()
.query(question)
.topK(5)
.build()
);
The important abstraction here is:
VectorStore
Our application doesn't need to manually implement vector similarity calculations.
Let's say we have a PDF containing product documentation.
Spring AI provides document readers that can load different document formats.
Conceptually:
var documents = reader.get();
We then split the documents into chunks.
For example:
TokenTextSplitter splitter = new TokenTextSplitter();
List<Document> chunks =
splitter.apply(documents);
The exact splitter and configuration should be chosen based on your document structure and model context window.
The embedding model converts every chunk into a vector.
Conceptually:
Document Chunk
β
Embedding Model
β
Vector
Spring AI handles this through the embedding/vector-store integration.
Then we can store the documents:
vectorStore.add(chunks);
Our ingestion pipeline is now:
PDF
β
Documents
β
Chunks
β
Embeddings
β
Vector Store
Now let's handle the user's question.
Suppose the user asks:
How does authentication work in our application?
We search the vector store:
List<Document> results =
vectorStore.similaritySearch(
SearchRequest.builder()
.query(question)
.topK(5)
.build()
);
We now have the most relevant pieces of information.
For example:
Result 1
JWT authentication uses...
Result 2
The authentication filter...
Result 3
Security configuration...
We can now construct a prompt using the retrieved documents.
For example:
String context = results.stream()
.map(Document::getText)
.collect(Collectors.joining("\n\n"));
String prompt = """
Answer the question using only the provided context.
Context:
%s
Question:
%s
""".formatted(context, question);
Then send it to the chat model.
Conceptually:
ChatResponse response =
chatModel.call(
new Prompt(prompt)
);
And we return:
response.getResult()
.getOutput()
.getText();
That's a basic RAG implementation.
Putting everything together:
INGESTION PIPELINE
βββββββββββββββββ
β Documents β
βββββββββ¬ββββββββ
β
Text Splitting
β
Embeddings
β
Vector Store
β
β
β
βΌ
RETRIEVAL
β²
β
User Question
β
Embedding
β
Similarity Search
β
Top K Chunks
β
Context
β
Prompt
β
LLM
β
Answer
This is the basic architecture behind many modern knowledge-based AI applications.
One of the most underestimated parts of RAG is chunking.
Bad chunking can produce bad retrieval.
Imagine this document:
Spring Security
Authentication allows the application to verify
the identity of a user.
Authorization determines whether the authenticated
user has permission to access a resource.
If we split this badly:
Chunk 1:
Authentication allows...
Chunk 2:
the identity of a user. Authorization determines...
Chunk 3:
whether the authenticated user...
we may destroy important semantic relationships.
A better strategy is to preserve meaningful boundaries where possible.
Depending on the data, you may experiment with:
Chunk Size
Overlap
Sentence boundaries
Paragraph boundaries
Markdown headings
Semantic sections
There is no universal chunk size that works for every RAG application.
When performing similarity search, we usually retrieve the top K results.
For example:
.topK(5)
means:
Return the 5 most relevant chunks.
But bigger isn't always better.
If we retrieve too little:
K = 1
we may miss important context.
If we retrieve too much:
K = 50
we may introduce irrelevant information and increase token usage.
A common approach is to start with a small value and evaluate retrieval quality.
For example:
K = 3
K = 5
K = 10
Then measure which configuration works best for your dataset.
A common question is:
Why not fine-tune the model instead?
RAG and fine-tuning solve different problems.
Best when:
Useful when you want to change:
A useful mental model is:
RAG
β Give the model the right information.
Fine-tuning
β Change how the model behaves.
In many production systems, they can also be used together.
A basic RAG pipeline is only the beginning.
Production RAG systems often introduce additional stages:
Query
β
Query Transformation
β
Hybrid Retrieval
β
Metadata Filtering
β
Vector Search
β
Reranking
β
Context Compression
β
Prompt Construction
β
LLM
You might eventually introduce techniques such as:
This is where RAG becomes an engineering discipline rather than simply "put documents into a vector database."
If you're building RAG for production, don't stop at:
PDF β Vector DB β LLM
You also need to think about:
How frequently are documents updated?
New document
β
Process
β
Chunk
β
Embed
β
Update Vector Store
Store useful metadata alongside chunks:
document_id
source
page_number
tenant_id
created_at
updated_at
document_type
This becomes extremely useful for filtering.
For example:
tenant_id = "company-123"
can ensure that users only retrieve documents belonging to their tenant.
This is critical.
A RAG system must not retrieve documents that the current user isn't authorized to access.
Your retrieval layer should respect application permissions.
User
β
Authentication
β
Authorization
β
Metadata Filters
β
Retrieval
β
LLM
Never assume that because the LLM can't "see" a document directly, the document is secure.
Track things such as:
Retrieval latency
Embedding latency
LLM latency
Token usage
Retrieved chunks
Similarity scores
Failure rate
Answer quality
Without observability, debugging RAG becomes extremely difficult.
If you're new to RAG, remember this:
"Convert meaning into numbers"
"Store and search those meanings"
"Find relevant information"
"Use that information to generate an answer"
Together:
Retrieve β Augment β Generate
That's RAG.
Spring AI gives Java developers abstractions around many of these building blocks.
Instead of manually wiring every AI provider and vector database integration, we can work with abstractions such as:
ChatModel
EmbeddingModel
VectorStore
Document
DocumentReader
This allows us to focus more on the application architecture rather than provider-specific implementation details.
And that's particularly useful when building enterprise Java applications where we may want to change:
AWS Bedrock
β
Another Model Provider
without completely rewriting our application.
A production-oriented Spring AI RAG application can eventually look like this:
βββββββββββββββββββββ
β User / App β
βββββββββββ¬ββββββββββ
β
Spring Boot API
β
Query Processing
β
Retrieval Layer
β
βββββββββββββββββββββ
β Vector Store β
β PostgreSQL β
β + pgvector β
βββββββββββ¬ββββββββββ
β
Relevant Context
β
Prompt Assembly
β
βββββββββββββββββββββ
β Spring AI β
βββββββββββ¬ββββββββββ
β
βββββββββββββββββββββ
β AWS Bedrock β
βββββββββββ¬ββββββββββ
β
Answer
And separately:
Documents
β
Document Reader
β
Chunking
β
Embedding Model
β
PostgreSQL + pgvector
We started this series by exploring how Spring AI can connect Java applications with modern AI models.
With AWS Bedrock, we can access powerful foundation models without managing the underlying model infrastructure.
Now, with RAG, we can take another major step:
We can connect those models to our own data.
The journey now looks like:
LLM
β
Spring AI
β
AWS Bedrock
β
RAG
β
Vector Database
β
Our Own Data
But there is another important capability missing.
What if we don't just want the model to answer questions?
What if we want the model to take actions?
For example:
User
β
AI Agent
β
Decide what to do
β
Call a Tool
β
Execute Action
β
Return Result
That's where tool calling and AI agents come in.
Next up: Building AI Agents with Spring AI β Tool Calling, Memory, and Autonomous Workflows.
If you're building AI applications with Java and Spring Boot, RAG is one of the most important patterns to understand.