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

spring-ai-starter-vector-store-pgvector

spring.ai.vectorstore.pgvector.* (index-type, distance-type, dimensions, table-name) — this page’s default.

Redis

spring-ai-starter-vector-store-redis

spring.ai.vectorstore.redis.*; see Spring Boot + Redis as a vector database for a full worked example against the same VectorStore contract.

Qdrant

spring-ai-starter-vector-store-qdrant

spring.ai.vectorstore.qdrant.* (host, port, collection-name); pairs with Qdrant’s RAG integration patterns.

Elasticsearch

spring-ai-starter-vector-store-elasticsearch

spring.ai.vectorstore.elasticsearch.* (index-name, similarity, dimensions); see Vector & semantic search.

MongoDB Atlas

spring-ai-starter-vector-store-mongodb-atlas

spring.ai.vectorstore.mongodb.*, backed by an Atlas Vector Search index defined outside Spring; see Text, wildcard, geospatial & Atlas search.

Neo4j

spring-ai-starter-vector-store-neo4j

spring.ai.vectorstore.neo4j.* (label, embedding-dimension, distance-type); see Vector Search and GenAI.

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:

flowchart LR U[User question] --> QT[Query transformers\nrewrite / compress / translate] QT --> QE[Query expander\nmulti-query] QE --> DR[Document retriever\nVectorStoreDocumentRetriever] DR --> DJ[Document joiner] DJ --> QA[Query augmenter\nContextualQueryAugmenter] QA --> CM[Chat memory advisor\nMessageChatMemoryAdvisor / VectorStoreChatMemoryAdvisor] CM --> CC[ChatClient call\nmodel + tools] CC --> A[Answer]

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.