Integrating with Spring AI
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Examples on this page target Spring Boot 4.1 together with the Spring AI 2.0.x BOM (Spring AI 2.0.1 as
verified for this section), against the shared DocsAssistant scenario: PostgreSQL 17 with pgvector 0.8.6,
Ollama nomic-embed-text (768 dimensions) and llama3.1:8b. Every page in this section that shows a Spring
AI snippet links back to one of the anchors below, so the class names, property keys and builder calls here
are the single source of truth for the whole section — if a page elsewhere and this page ever disagree, this
page is the one to trust. This page collects the vector-store, retrieval, RAG-advisor and agent-memory surface
only; the rest of Spring AI — chat models, prompts, structured output, tools, chat memory, MCP, image and
audio models, and the wider observability and evaluation APIs — is covered by the dedicated
Spring AI sub-section of the AI guide, whose
Retrieval & RAG Bridge page links back to the anchors below.
Embeddings: EmbeddingModel
Spring AI represents every embedding provider behind one interface, EmbeddingModel, with embed(String) and
embed(Document) overloads that return a float[] regardless of which provider is configured underneath.
Adding the Ollama starter and pointing it at nomic-embed-text is enough to obtain one:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
spring:
ai:
ollama:
base-url: http://localhost:11434
embedding:
model: nomic-embed-text
package com.example.docsassistant;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.stereotype.Service;
@Service
public class DocsEmbeddingService {
private final EmbeddingModel embeddingModel;
public DocsEmbeddingService(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
public float[] embed(String text) {
return this.embeddingModel.embed(text);
}
}
embed(Document) is the overload the VectorStore implementation below calls internally when a Document is
added, so application code rarely needs to call embed directly outside of standalone tooling or tests. See
Spring AI: Embeddings API for the full
EmbeddingModel/EmbeddingResponse surface and the other supported providers (OpenAI, Azure OpenAI, Vertex
AI, Bedrock, Hugging Face, and more).
The VectorStore API
VectorStore is Spring AI’s single abstraction over every supported store: PgVectorStore, RedisVectorStore,
QdrantVectorStore, ElasticsearchVectorStore, MongoDBAtlasVectorStore, Neo4jVectorStore and others all
implement the same add/delete/similaritySearch contract, auto-configured from a Spring Boot starter and a
spring.ai.vectorstore. property block. This page’s *DocsAssistant examples use the pgvector starter:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
spring:
ai:
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 768
initialize-schema: true
table-name: docs_vector_store
ollama:
embedding:
model: nomic-embed-text
initialize-schema: true makes PgVectorStore create and own docs_vector_store itself — a table separate
from, and unaware of, the hand-written chunk_embeddings schema used by the plain-SQL examples elsewhere in
this section (see PostgreSQL & pgvector). dimensions: 768 matches the
nomic-embed-text model configured above; changing the embedding model without updating this value, or
without re-creating the table, is the same re-embedding-migration hazard covered on
Generating Embeddings in Practice.
Application code talks to VectorStore only, never to the store-specific class:
package com.example.docsassistant;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class DocsVectorStoreService {
private final VectorStore vectorStore;
public DocsVectorStoreService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void ingest(String sourcePath, String section, String component, String content) {
Document document = new Document(content, Map.of(
"source_path", sourcePath,
"section", section,
"component", component));
this.vectorStore.add(List.of(document));
}
public List<Document> search(String query, String component, int topK) {
SearchRequest.Builder request = SearchRequest.builder().query(query).topK(topK);
if (component != null) {
request.filterExpression("component == '" + component + "'");
}
return this.vectorStore.similaritySearch(request.build());
}
public void delete(List<String> documentIds) {
this.vectorStore.delete(documentIds);
}
}
add embeds and upserts a Document, keyed by its (optionally caller-supplied) id; delete takes the same
ids; similaritySearch takes a SearchRequest built with .query(…), .topK(…), an optional
.similarityThreshold(…) and an optional .filterExpression(…) — there is no separate "filter query", the
VectorStore combines the vector search and the metadata filter in one call. See
Spring AI: Vector Databases for the full
VectorStore interface and the property reference for every supported store starter.
Filter expressions
SearchRequest.filterExpression(…) accepts a small boolean expression language over document metadata,
either as a string or built programmatically with FilterExpressionBuilder so values are never string-concatenated
into the expression:
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.ai.vectorstore.filter.Filter.Expression;
FilterExpressionBuilder b = new FilterExpressionBuilder();
Expression filter = b.and(
b.eq("component", "vector-rag"),
b.in("title", "HNSW indexes", "pgvector basics")
).build();
List<Document> hits = vectorStore.similaritySearch(
SearchRequest.builder()
.query("What does ef_search control?")
.topK(5)
.filterExpression(filter)
.build());
The same ==, !=, >, >=, <, ⇐, IN, NIN, AND, OR and NOT operators are supported whether
the expression is written as text ("component == 'vector-rag' AND title IN ('HNSW indexes', 'pgvector
basics')") or built with FilterExpressionBuilder; each VectorStore implementation translates the same
expression into its own native filter (a SQL WHERE clause for PgVectorStore, a Redis query, a Qdrant
filter, and so on), which is what lets Metadata Filtering treat filtering as
one concept across engines. Enforce access-control and multi-tenancy filters here, server-side, never as
instructions in the prompt. See
Spring AI: Metadata filters
for the full expression grammar and per-store translation notes.
ETL: DocumentReader, DocumentTransformer, DocumentWriter
Spring AI models ingestion explicitly as an Extract-Transform-Load pipeline: a DocumentReader extracts
Document instances from a source, one or more DocumentTransformer stages split and enrich them (a
TokenTextSplitter, then a metadata enricher), and a DocumentWriter — typically the VectorStore itself — loads the result:
package com.example.docsassistant;
import org.springframework.ai.reader.markdown.MarkdownDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.core.io.FileSystemResource;
import java.util.List;
import java.util.Map;
public class DocsIngestionPipeline {
private final VectorStore vectorStore;
public DocsIngestionPipeline(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void ingest(String path, String component) {
MarkdownDocumentReader reader = new MarkdownDocumentReader(new FileSystemResource(path));
List<Document> pages = reader.get();
TokenTextSplitter splitter = new TokenTextSplitter();
List<Document> chunks = splitter.apply(pages);
chunks.forEach(chunk -> chunk.getMetadata().putAll(Map.of("source_path", path, "component", component)));
this.vectorStore.write(chunks);
}
}
Running this over the shared corpus (docs/vector-rag/hnsw.adoc, docs/vector-rag/pgvector.adoc,
docs/spring-ai/rag.adoc) with component set from the path is the Spring AI equivalent of the hand-written
ingest.py used by the plain-SQL and LangChain examples elsewhere in this section — Document Ingestion & Chunking covers chunking strategy, per-chunk metadata
and idempotent re-ingestion in depth; this page only fixes the ETL class names and wiring. See
Spring AI: ETL pipeline for the full set of
readers, transformers and writers.
RAG advisors: QuestionAnswerAdvisor and RetrievalAugmentationAdvisor
QuestionAnswerAdvisor is Spring AI’s naive-RAG advisor: registered as a default advisor on a ChatClient, it
runs the embed-search-augment steps before every call and hands the model a prompt that already contains the
retrieved context, with no change to the calling code. In Spring AI 2.0 it ships in the
spring-ai-vector-store-advisor artifact (renamed from spring-ai-advisors-vector-store in 1.x), in the
org.springframework.ai.chat.client.advisor.vectorstore package:
package com.example.docsassistant;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
public class DocsAssistantRag {
private final ChatClient chatClient;
public DocsAssistantRag(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
this.chatClient = chatClientBuilder
.defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().topK(5).build())
.build())
.build();
}
public String answer(String question) {
return this.chatClient.prompt().user(question).call().content();
}
}
RetrievalAugmentationAdvisor, from the separate spring-ai-rag artifact, is the modular counterpart of
QuestionAnswerAdvisor: it exposes each stage — one or more queryTransformers, an optional queryExpander, a documentRetriever and a
documentJoiner — as separately configurable, composable pieces rather than one fixed sequence:
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter;
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.35)
.topK(5)
.build())
.queryAugmenter(ContextualQueryAugmenter.builder()
.allowEmptyContext(false)
.build())
.build();
String answer = chatClient.prompt()
.advisors(retrievalAugmentationAdvisor)
.user("What does ef_search control in an HNSW index?")
.call()
.content();
See RAG Fundamentals for the naive-RAG walkthrough QuestionAnswerAdvisor
implements and Prompt Augmentation & Generation for ContextualQueryAugmenter
and prompt-template detail, plus
Spring AI: Retrieval
Augmented Generation for both advisors' full builder options.
Modular RAG: query transformers, expanders, retrievers and joiners
The pieces RetrievalAugmentationAdvisor composes above are independently reusable, which is what "modular
RAG" means in Spring AI: each stage is its own class, testable and swappable on its own.
import org.springframework.ai.rag.preretrieval.query.transformation.RewriteQueryTransformer;
import org.springframework.ai.rag.preretrieval.query.transformation.CompressionQueryTransformer;
import org.springframework.ai.rag.preretrieval.query.transformation.TranslationQueryTransformer;
import org.springframework.ai.rag.preretrieval.query.expansion.MultiQueryExpander;
import org.springframework.ai.rag.retrieval.join.DocumentJoiner;
import org.springframework.ai.rag.retrieval.join.ConcatenationDocumentJoiner;
RewriteQueryTransformer rewriteQueryTransformer = RewriteQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.build();
CompressionQueryTransformer compressionQueryTransformer = CompressionQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.build();
TranslationQueryTransformer translationQueryTransformer = TranslationQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.targetLanguage("english")
.build();
MultiQueryExpander multiQueryExpander = MultiQueryExpander.builder()
.chatClientBuilder(chatClientBuilder)
.numberOfQueries(3)
.build();
DocumentJoiner documentJoiner = new ConcatenationDocumentJoiner();
Advisor advisor = RetrievalAugmentationAdvisor.builder()
.queryTransformers(rewriteQueryTransformer, compressionQueryTransformer, translationQueryTransformer)
.queryExpander(multiQueryExpander)
.documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(vectorStore).build())
.documentJoiner(documentJoiner)
.build();
Spring AI does not ship a built-in query router as of 2.0.1; the common pattern is a custom
DocumentRetriever that inspects the (possibly transformed) query and delegates to one of several
VectorStoreDocumentRetriever instances, one per store or per component value, mirroring the query-routing
pattern covered on Retrieval Strategies. See
Spring AI: Retrieval
Augmented Generation for the full modular-RAG building-block reference.
Chat memory: MessageChatMemoryAdvisor and VectorStoreChatMemoryAdvisor
MessageChatMemoryAdvisor replays a bounded recent window of a conversation, keyed by a conversation id, using
a ChatMemory — MessageWindowChatMemory, which keeps the last maxMessages messages — backed by a
ChatMemoryRepository (JDBC, Cassandra, Neo4j or in-memory). The JDBC repository comes from the
spring-ai-starter-model-chat-memory-repository-jdbc starter and stores messages in its own
framework-managed SPRING_AI_CHAT_MEMORY table:
package com.example.docsassistant;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
public class DocsAssistantChatService {
private final ChatClient chatClient;
public DocsAssistantChatService(ChatClient.Builder chatClientBuilder,
JdbcChatMemoryRepository chatMemoryRepository) {
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.maxMessages(20)
.build();
this.chatClient = chatClientBuilder
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
public String ask(String conversationId, String question) {
return this.chatClient.prompt()
.user(question)
.advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
}
}
VectorStoreChatMemoryAdvisor is the long-term counterpart: instead of replaying a bounded window, it retrieves
the messages most similar to the current question from a dedicated VectorStore:
import org.springframework.ai.chat.client.advisor.vectorstore.VectorStoreChatMemoryAdvisor;
import org.springframework.ai.vectorstore.VectorStore;
public ChatClient chatClientWithLongTermMemory(ChatClient.Builder chatClientBuilder, VectorStore messageMemoryStore) {
return chatClientBuilder
.defaultAdvisors(VectorStoreChatMemoryAdvisor.builder(messageMemoryStore).build())
.build();
}
Registering both advisors on the same ChatClient combines short-term and long-term memory; messageMemoryStore
should be a VectorStore dedicated to conversation memory, separate from the DocsAssistant documentation
store configured above. See Agent Memory & Semantic Cache for the schema and
LangGraph-checkpointer comparison this advisor pair maps onto, and
Spring AI: Chat Memory for the full
ChatMemory/ChatMemoryRepository reference.
Tool calling: @Tool searchDocs
A plain Java method annotated @Tool becomes a callable the model can invoke through ChatClient’s normal
tool-calling loop, without a separate `Advisor — this is Spring AI’s retriever-as-tool pattern for agentic
RAG:
package com.example.docsassistant;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.document.Document;
import java.util.List;
public class DocsTools {
private final VectorStore vectorStore;
public DocsTools(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
@Tool(description = "Search the DocsAssistant documentation for chunks relevant to a query")
public List<DocHit> searchDocs(String query, String component) {
SearchRequest.Builder request = SearchRequest.builder().query(query).topK(5);
if (component != null) {
request.filterExpression("component == '" + component + "'");
}
List<Document> hits = this.vectorStore.similaritySearch(request.build());
return hits.stream()
.map(d -> new DocHit(
(String) d.getMetadata().get("source_path"),
(String) d.getMetadata().get("section"),
d.getText(),
d.getScore() != null ? d.getScore() : 0.0))
.toList();
}
}
record DocHit(String sourcePath, String section, String content, double score) {}
ChatClient chatClient = chatClientBuilder.defaultTools(new DocsTools(vectorStore)).build();
String answer = chatClient.prompt()
.user("What does ef_search control? Only search the vector-rag docs.")
.call()
.content();
ChatClient sends searchDocs’s schema to the model alongside the conversation; when the model requests the
tool, Spring AI invokes the Java method, feeds the returned `DocHit list back as a tool result, and re-invokes
the model, iterating until a final answer with no further tool calls is produced. See
Agentic RAG for the full agentic-RAG walkthrough this tool is drawn from and
Spring AI: Tool calling for ToolCallback
alternatives and defaultTools/tools(…) registration.
Testing: Testcontainers and RAG evaluators
Spring Boot’s @ServiceConnection lets a test spin up real pgvector and Ollama containers and wires the
resulting VectorStore and chat model automatically, so integration tests run against the same engines as
production rather than mocks. PostgreSQL’s connection details come from Spring Boot’s own
spring-boot-testcontainers module; Ollama’s come from Spring AI’s spring-ai-spring-boot-testcontainers
module, which matches a Testcontainers OllamaContainer:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-ollama</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
package com.example.docsassistant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.ollama.OllamaContainer;
import org.testcontainers.utility.DockerImageName;
@SpringBootTest
@Testcontainers
class DocsAssistantRagIntegrationTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
DockerImageName.parse("pgvector/pgvector:pg17").asCompatibleSubstituteFor("postgres"));
@Container
@ServiceConnection
static OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.34.4");
@BeforeAll
static void pullModels() throws Exception {
ollama.execInContainer("ollama", "pull", "nomic-embed-text");
ollama.execInContainer("ollama", "pull", "llama3.1:8b");
}
@Test
void searchDocsReturnsRelevantChunks() {
// ingest the shared corpus, then assert the top hit's source_path
}
}
asCompatibleSubstituteFor("postgres") is required because PostgreSQLContainer only accepts images it
recognises as PostgreSQL; the pgvector/pgvector image is a drop-in PostgreSQL image with the extension
preinstalled, and declaring it a compatible substitute lets both the container and Spring Boot’s service
connection treat it as one. The Testcontainers artifact names above are the 2.x ones managed by the Spring
Boot 4 BOM (1.x used postgresql, ollama and junit-jupiter). See
Spring AI: Testcontainers and
Spring
Boot: Testcontainers service connections for the full list of supported containers.
Beyond wiring, Spring AI ships two evaluators purpose-built for RAG regression tests: RelevancyEvaluator
checks whether the retrieved context (and the answer built from it) is actually relevant to the question, and
FactCheckingEvaluator checks whether the answer is supported by that context, both callable from a plain
JUnit assertion. Evaluating RAG Systems covers both evaluators, the golden
question set they run against and how they compare to RAGAS. See
Spring AI: Evaluation Testing for the full
evaluator reference.
Switching stores by starter
The VectorStore interface makes moving the DocsAssistant store between engines a starter-and-property
change, not a code change, as long as the new store supports the filter expressions and index parameters the
application already relies on:
| Store | Starter artifact | Distinguishing configuration |
|---|---|---|
pgvector |
|
|
Redis |
|
|
Qdrant |
|
|
Elasticsearch |
|
|
MongoDB Atlas |
|
|
Neo4j |
|
|
Comparing Vector Databases covers the operational, licensing and cost trade-offs
behind this choice; this table is only about the Spring AI wiring once a choice is made. In every case the
application code from The VectorStore API above — vectorStore.add(…),
vectorStore.delete(…), vectorStore.similaritySearch(SearchRequest.builder()…build()) — is unchanged;
only the dependency and the spring.ai.vectorstore.* property block differ.
The advisor chain assembled across this page composes into one request/response pipeline around the model call:
LangChain equivalents
Every advisor and store above has a LangChain counterpart built on the same underlying concepts:
VectorStore.add/similaritySearch corresponds to LangChain’s add_documents/similarity_search;
RetrievalAugmentationAdvisor’s query transformers and expander correspond to LangChain’s query-rewriting and
multi-query retrievers; `@Tool searchDocs corresponds to a LangChain retriever tool passed to create_agent;
and VectorStoreChatMemoryAdvisor corresponds to a LangGraph long-term memory store keyed the same way. See
Integrating with LangChain for the LangChain and LangChain4j side of this same
scenario.
Related pages
-
RAG Fundamentals — the naive-RAG pipeline
QuestionAnswerAdvisorimplements. -
Retrieval Strategies and Prompt Augmentation & Generation — the modular-RAG stages this page’s advisors compose.
-
Document Ingestion & Chunking — the ETL pipeline’s chunking and metadata detail.
-
Metadata Filtering — the filter-expression concept this page’s Java API implements.
-
Agent Memory & Semantic Cache — the chat-memory schema behind the two advisors above.
-
Agentic RAG — the full
@Tool searchDocsagentic-RAG walkthrough. -
Evaluating RAG Systems —
RelevancyEvaluatorandFactCheckingEvaluatorin context. -
Comparing Vector Databases — choosing among the stores in the switching table above.
-
Spring Boot + Redis as a vector database — a full worked example of the Redis starter.
-
Integrating with LangChain — the LangChain and LangChain4j equivalents.
-
Exposing RAG through MCP — exposing a Spring AI
VectorStorethrough an MCP server built with the Spring AI MCP starters.