Spring Boot: Redis as a Vector Database

This content was generated with the assistance of AI and should be verified against the official Redis documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Examples on this page target Redis Open Source 8.10 (8.10.2) with Spring Boot 4.1, Spring AI 2.0, Redis OM Spring 1.0, and Jedis 8. This page assumes Vector Search: Query Engine & Vector Sets for the underlying Query Engine VECTOR field, HNSW, distance metrics, and quantization — none of that theory is repeated here. What this page covers instead is the Spring-side wiring: the Spring AI RedisVectorStore starter and its spring.ai.vectorstore.redis.* properties, the RedisVectorStore Java API for RAG, and the same Bookshop KNN search written by hand three ways (Spring AI, raw Jedis, Redis OM Spring) so the trade-offs between them are visible side by side.

The Bookshop domain model

Every example below indexes the same three Bookshop records used elsewhere in this section, now as a Java record rather than the raw JSON document Query Engine: Indexing & Data Modeling declares:

package com.example.bookshop.model;

import java.math.BigDecimal;
import java.util.List;

public record Book(
        String isbn,
        String title,
        String author,
        List<String> genres,
        BigDecimal price,
        int year) {
}

The three catalog rows carry ISBNs 9780000000001, 9780000000002, and 9780000000003, matching the values used throughout Vector Search: Query Engine & Vector Sets and Query Engine: Indexing & Data Modeling.

spring-ai-starter-vector-store-redis

Spring AI 2.0 ships a dedicated starter that wraps the Query Engine VECTOR field behind its portable VectorStore abstraction, so application code writes and searches embeddings through the same interface it would use against any other Spring AI-supported store.

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-redis</artifactId>
</dependency>

This derives from Spring AI Redis vector store.

spring.ai.vectorstore.redis.* properties

The starter auto-configures a RedisVectorStore bean from spring.ai.vectorstore.redis.*, alongside the ordinary Spring Data Redis connection properties (spring.data.redis.host, .port, and so on) it connects through:

spring:
  data:
    redis:
      host: localhost
      port: 6379
  ai:
    vectorstore:
      redis:
        index-name: spring-ai-books
        prefix: "bookshop:ai:"
        initialize-schema: true
        hnsw:
          m: 16
          ef-construction: 200
          ef-runtime: 10
  • index-name — the Query Engine index Spring AI creates and searches, spring-ai-books for the Bookshop scenario — deliberately a separate index from the hand-built idx:books Query Engine: Indexing & Data Modeling declares, since Spring AI owns this index’s schema end to end.

  • prefix — the key prefix each stored document lives under, bookshop:ai: here, mirroring the bookshop:book: convention used by the rest of this section’s hand-written examples.

  • initialize-schema — when true, Spring AI issues its own FT.CREATE for index-name on startup if the index doesn’t already exist; set to false against an index provisioned by another process or migration step.

  • hnsw.m, hnsw.ef-construction, hnsw.ef-runtime — the HNSW graph parameters Spring AI passes to the VECTOR field it creates (M, EF_CONSTRUCTION, EF_RUNTIME). The values above are the defaults. See Index algorithms for what each one trades off.

That is the whole property surface: RedisVectorStoreProperties binds only index-name, prefix, initialize-schema and hnsw.*. The vector field’s dimension is not a property. Spring AI takes it from the injected EmbeddingModel, so a 384-dimension embedding model produces the 384-dimension field the idx:books schema also uses. The distance metric and the index algorithm default to COSINE and HNSW, which is what the Bookshop scenario uses. Choosing something else (L2/IP, or FLAT) means declaring your own RedisVectorStore bean with RedisVectorStore.builder(redisClient, embeddingModel) and its .distanceMetric(…​) and .vectorAlgorithm(…​) options, instead of relying on the auto-configured one. These names derive from Spring AI Redis vector store.

The RedisVectorStore Java API

Metadata fields and adding documents

A RedisVectorStore bean is autowired like any other Spring bean once the starter and an EmbeddingModel are on the classpath; metadata fields used in filter expressions (below) are registered explicitly so the store knows which JSON-document attributes to index as filterable TAG/NUMERIC fields alongside the vector itself:

package com.example.bookshop.search;

import com.example.bookshop.model.Book;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.redis.RedisVectorStore;
import org.springframework.ai.vectorstore.redis.RedisVectorStore.MetadataField;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
public class BookIngestionService {

    private final RedisVectorStore vectorStore;

    public BookIngestionService(RedisVectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    public void indexBook(Book book, String description) {
        Document document = Document.builder()
                .id(book.isbn())
                .text(description)
                .metadata(Map.of(
                        "isbn", book.isbn(),
                        "genres", book.genres(),
                        "price", book.price().doubleValue(),
                        "year", book.year()))
                .build();
        vectorStore.add(List.of(document));
    }
}

MetadataField.tag("genres"), MetadataField.numeric("price"), and MetadataField.numeric("year") are declared on the RedisVectorStore.builder(…​) at bean-construction time (not shown above, since the starter’s auto-configuration builds the bean from properties) when initialize-schema provisions the index itself, so each metadata key ends up as its own filterable field rather than an opaque JSON blob. This derives from Spring AI Redis vector store.

Similarity search and portable filter expressions

similaritySearch takes a SearchRequest built with Spring AI’s portable filter-expression DSL, which RedisVectorStore translates into a Query Engine pre-filter ahead of the KNN clause — the same filter-before-vector pattern Range queries and hybrid filtering shows written by hand:

package com.example.bookshop.search;

import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.ai.vectorstore.redis.RedisVectorStore;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class BookSearchService {

    private final RedisVectorStore vectorStore;

    public BookSearchService(RedisVectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    public List<Document> findSimilar(String queryText, int topK) {
        var b = new FilterExpressionBuilder();
        SearchRequest request = SearchRequest.builder()
                .query(queryText)
                .topK(topK)
                .filterExpression(b.eq("genres", "fiction").build())
                .build();
        return vectorStore.similaritySearch(request);
    }
}

b.eq("genres", "fiction") compiles to a Query Engine TAG filter equivalent to @genres:{fiction} — RedisVectorStore escapes the braces Redis’s tag syntax requires (see Query Engine: Search & Aggregations), so application code never writes raw FT.SEARCH filter syntax itself. A numeric comparison such as b.gt("year", 2000) compiles instead to a Query Engine NUMERIC range filter ahead of the same KNN clause. This derives from Spring AI Redis vector store and Spring AI metadata filters.

RAG with ChatClient and QuestionAnswerAdvisor

QuestionAnswerAdvisor wraps a VectorStore into a ChatClient advisor that runs the retrieval step automatically ahead of every chat call: it embeds the incoming question, retrieves the nearest documents from RedisVectorStore, and inserts them into the prompt before it ever reaches the LLM — the same shape RAG, semantic cache, and agent memory patterns describes at the Redis-command level, expressed through Spring AI’s advisor chain instead of a hand-written FT.SEARCH call. The class ships in the spring-ai-vector-store-advisor artifact, in the org.springframework.ai.chat.client.advisor.vectorstore package.

package com.example.bookshop.chat;

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.redis.RedisVectorStore;
import org.springframework.stereotype.Service;

@Service
public class BookAssistantService {

    private final ChatClient chatClient;

    public BookAssistantService(ChatClient.Builder chatClientBuilder, RedisVectorStore vectorStore) {
        QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
                .searchRequest(SearchRequest.builder().topK(5).build())
                .build();
        this.chatClient = chatClientBuilder.defaultAdvisors(qaAdvisor).build();
    }

    public String ask(String question) {
        return chatClient.prompt()
                .user(question)
                .call()
                .content();
    }
}

This derives from Spring AI RAG.

Chunking with TokenTextSplitter

A book description is usually short enough to embed whole, but longer source documents (a full chapter, a support article) need splitting before ingestion so each chunk stays within the embedding model’s input limit and retrieval returns focused passages rather than an entire document:

package com.example.bookshop.search;

import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.redis.RedisVectorStore;

import java.util.List;

public class BookChunkIngestionService {

    private final TokenTextSplitter splitter = new TokenTextSplitter();
    private final RedisVectorStore vectorStore;

    public BookChunkIngestionService(RedisVectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    public void indexLongDescription(Document sourceDocument) {
        List<Document> chunks = splitter.apply(List.of(sourceDocument));
        vectorStore.add(chunks);
    }
}

TokenTextSplitter splits on token count (not raw character count), defaulting to roughly 800 tokens per chunk with a small overlap between consecutive chunks so a fact near a chunk boundary isn’t lost entirely from either side. This derives from Spring AI ETL pipeline — TokenTextSplitter.

Chat memory and semantic cache on Redis

Spring AI’s ChatMemory abstraction can be backed by Redis the same way VectorStore is: a RedisChatMemoryRepository persists conversation turns as ordinary Redis data structures so a multi-turn conversation survives across requests and application instances, mirroring the agent memory pattern RAG, semantic cache, and agent memory patterns describes.

package com.example.bookshop.chat;

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.redis.RedisChatMemoryRepository;
import org.springframework.stereotype.Service;

@Service
public class BookConversationService {

    private final ChatClient chatClient;

    public BookConversationService(ChatClient.Builder chatClientBuilder,
                                    RedisChatMemoryRepository chatMemoryRepository) {
        ChatMemory chatMemory = MessageWindowChatMemory.builder()
                .chatMemoryRepository(chatMemoryRepository)
                .build();
        this.chatClient = chatClientBuilder
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }
}

ChatMemory has no builder of its own; MessageWindowChatMemory is the implementation that keeps the last N messages of each conversation and delegates storage to the repository. The Redis repository comes from the spring-ai-starter-model-chat-memory-repository-redis starter, in the org.springframework.ai.chat.memory.repository.redis package. Every call through this client must also carry a conversation ID, derived from the authenticated user rather than taken from the request. The repository options, the other stores and the retention and erasure jobs are covered on Spring AI: Chat Memory Repositories, and how MessageChatMemoryAdvisor sits in the advisor chain next to RAG and tool calling on Spring AI: Advisors.

A semantic cache built on the same RedisVectorStore bean stores a previously answered question’s embedding and response as ordinary vector-store documents, then checks similaritySearch for a near-duplicate question before calling the LLM again — Spring AI has no dedicated semantic-cache advisor as of Spring AI 2.0, so this is typically wired as an explicit pre-check in application code (a similaritySearch call with a distance threshold, short-circuiting the ChatClient call on a close-enough match) rather than a drop-in advisor. This derives from Spring AI chat memory and Redis RAG quickstart.

The same KNN search, three ways

Raw Jedis

Bypassing Spring AI entirely and issuing FT.CREATE/FT.SEARCH directly, exactly as KNN queries and Query Engine: Indexing & Data Modeling describe in full — repeated here only to show the call shape next to the other two approaches:

package com.example.bookshop.search;

import redis.clients.jedis.RedisClient;
import redis.clients.jedis.search.FTCreateParams;
import redis.clients.jedis.search.Query;
import redis.clients.jedis.search.SearchResult;
import redis.clients.jedis.search.schemafields.TagField;
import redis.clients.jedis.search.schemafields.VectorField;

import java.util.Map;

public class RawJedisBookSearch {

    public void createIndex(RedisClient jedis) {
        jedis.ftCreate("idx:books",
                FTCreateParams.createParams().on(redis.clients.jedis.args.SearchDataType.JSON)
                        .prefix("bookshop:book:"),
                TagField.of("$.genres[*]").as("genres"),
                VectorField.of("$.embedding", VectorField.VectorAlgorithm.HNSW, Map.of(
                        "TYPE", "FLOAT32", "DIM", 384, "DISTANCE_METRIC", "COSINE")).as("embedding"));
    }

    public SearchResult findSimilar(RedisClient jedis, byte[] queryEmbedding, int k) {
        Query query = new Query("*=>[KNN " + k + " @embedding $q AS score]")
                .addParam("q", queryEmbedding)
                .setSortBy("score", true)
                .dialect(2);
        return jedis.ftSearch("idx:books", query);
    }
}

This is the same query KNN queries walks through in detail; the same page also covers FT.HYBRID for fusing a full-text score with the vector score, which isn’t repeated here.

Redis OM Spring

Redis OM Spring maps a Java record or class straight onto a Query Engine index through annotations, closer in spirit to Spring Data than to either of the two approaches above:

package com.example.bookshop.search;

import com.redis.om.spring.annotations.Document;
import com.redis.om.spring.annotations.Indexed;
import com.redis.om.spring.annotations.VectorIndexed;
import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm;
import com.redis.om.spring.indexing.DistanceMetric;
import com.redis.om.spring.indexing.VectorType;

import java.math.BigDecimal;
import java.util.List;

@Document
public record IndexedBook(
        String isbn,
        String title,
        String author,
        @Indexed List<String> genres,
        BigDecimal price,
        int year,
        @VectorIndexed(
                algorithm = VectorAlgorithm.HNSW,
                type = VectorType.FLOAT32,
                dimension = 384,
                distanceMetric = DistanceMetric.COSINE)
        float[] embedding) {
}

Querying goes through EntityStream, Redis OM Spring’s fluent, type-safe query builder, rather than a raw query string:

package com.example.bookshop.search;

import com.redis.om.spring.search.stream.EntityStream;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class RedisOmBookSearch {

    private final EntityStream entityStream;

    public RedisOmBookSearch(EntityStream entityStream) {
        this.entityStream = entityStream;
    }

    public List<IndexedBook> findSimilar(float[] queryEmbedding, int k) {
        return entityStream.of(IndexedBook.class)
                .filter(IndexedBook$.GENRES.in("fiction"))
                .sorted(IndexedBook$.EMBEDDING.knn(k, queryEmbedding))
                .collect(java.util.stream.Collectors.toList());
    }
}

IndexedBook$ is a Redis OM Spring-generated metamodel class (analogous to a JPA static metamodel), produced by its annotation processor at build time from the @Document-annotated IndexedBook record above.

Comparison

Spring AI RedisVectorStore Raw Jedis Redis OM Spring

Abstraction level

Spring AI’s portable VectorStore/Document/SearchRequest API, same shape across every supported store

Direct FT.CREATE/FT.SEARCH calls, full control over every index and query option

Annotation-driven entity mapping with a generated metamodel and fluent EntityStream queries

Filter expressions

Portable FilterExpressionBuilder DSL, translated to Query Engine filter syntax internally

Hand-written FT.SEARCH filter syntax

Type-safe metamodel predicates (IndexedBook$.GENRES.in(…​))

RAG/chat integration

Built-in (QuestionAnswerAdvisor, ChatMemory, ChatClient)

None — application code wires retrieval into a prompt itself

None — application code wires retrieval into a prompt itself

Portability

Swappable for another Spring AI-supported vector store with no application code change

Redis-specific, tied to the Jedis client API

Redis-specific, tied to Redis OM Spring’s entity model

Best fit

A Spring AI-based RAG/chat application that wants a store-agnostic API

Fine-grained control over index schema, query options, or FT.HYBRID fusion

A Spring Data-style application already modeling other Redis data through Redis OM Spring

For the same comparison against other vector stores entirely, see RAG Integration Patterns (Qdrant), Vector & semantic search (Elasticsearch dense_vector/semantic_text), and Vector Search and GenAI (Neo4j VECTOR type and GraphRAG); the underlying HNSW graph algorithm any of these approximate-search engines run is covered once, in depth, in kNN / HNSW vector search.

Ingestion and RAG query flow

sequenceDiagram participant Doc as Source document participant Splitter as TokenTextSplitter participant Embed as EmbeddingModel participant Store as RedisVectorStore participant App as BookAssistantService participant Advisor as QuestionAnswerAdvisor participant LLM as LLM Doc->>Splitter: split into chunks Splitter->>Embed: embed(chunk) for each chunk Embed-->>Splitter: chunk embedding Splitter->>Store: add(Document[chunk, embedding, metadata]) Store->>Store: FT.CREATE / FT.SEARCH-backed index (spring-ai-books) App->>Advisor: chatClient.prompt().user(question) Advisor->>Embed: embed(question) Embed-->>Advisor: query embedding Advisor->>Store: similaritySearch(SearchRequest) Store-->>Advisor: top-k Document matches Advisor->>LLM: prompt + retrieved context LLM-->>App: grounded answer

References