You are here

AI Document Search with Spring Boot Using OpenAI and Redis Vector Store

AI Document Search with Spring Boot Using OpenAI and Redis Vector Store 2026

AI document search with Spring Boot Keyword search finds documents that contain your exact words. AI-powered document search finds documents that match what you mean, even when the wording is completely different. In this tutorial, we’ll build a working document search API in Spring Boot that stores document embeddings in Redis and ranks results by meaning, not keywords. By the end you’ll have a /api/search endpoint, a document ingestion pipeline, and a bonus RAG endpoint that answers questions using your own documents.

What You Will Learn

  • What embeddings and vector similarity search actually are, in plain terms
  • How to wire Spring AI’s OpenAI starter and Redis Vector Store starter into a Spring Boot 4.1 project
  • How to chunk and ingest documents so they embed cleanly
  • How to build a semantic search endpoint that ranks results by meaning
  • How to add a simple Retrieval-Augmented Generation (RAG) endpoint on top of the same data
  • The mistakes that trip up almost everyone the first time they build this

What Is AI-Powered Document Search?

AI-powered document search converts documents and queries into numeric vectors called embeddings, then finds matches by measuring vector similarity instead of matching exact words. This lets a search for “employee time off policy” return a document titled “vacation and leave guidelines,” because the two phrases mean the same thing even though they share no words.

Under the hood, this involves three moving parts working together:

  • An embedding model (here, OpenAI’s text-embedding-3-small) turns text into a vector: a list of a few hundred numbers that captures meaning.
  • A vector store (here, Redis, via RediSearch and RedisJSON) stores those vectors and can quickly find the nearest ones to a given query vector.
  • Spring AI ties both together with a consistent Java API, so swapping OpenAI for another provider or Redis for another vector database later doesn’t mean rewriting your controllers.

This is the same underlying mechanism that powers “search my knowledge base,” “chat with your PDFs,” and internal company search tools.

Prerequisites

You should be comfortable with:

  • Basic Spring Boot (controllers, @Service, dependency injection)
  • Running a Docker container
  • Making REST calls with curl or Postman

You will need:

  • Java 21 (LTS) installed.(check with java -version)
  • Docker Desktop or Docker Engine
  • Maven 3.9+ (or the Maven wrapper that comes with a new Spring Boot project)
  • An OpenAI API key with billing enabled (platform.openai.com)
  • Spring Boot 4.1.0 ·
  • Spring AI 2.0.0 ·
  • Redis Stack

No prior AI or machine learning experience is required. If you can call a REST API, you can follow this tutorial.

Why This Matters for Your Career

“AI integration” now shows up as a line item in a huge share of backend Java job postings, not because every company is building a chatbot, but because most companies have a pile of internal documents wikis, PDFs, support tickets, policy docs etc. that keyword search handles badly. Being able to say “I built a semantic search feature end-to-end in Spring Boot” is a concrete, verifiable skill that separates a resume from the stack of candidates who’ve only used AI through a chat window.

It’s also one of the more approachable ways into the “AI engineering” space if your background is plain Java and Spring. You don’t need to train a model or understand the math behind transformers. Spring AI handles that layer. You need to understand data flow, configuration, and how to design a clean API around a new kind of data store, all of which are skills you likely already have.

How to Implement AI Document Search with Spring Boot?

Let’s go through it step bt step.

Step#1: Create the Spring Boot Project

Generate a new project from start.spring.io with:

  • Project: Maven
  • Language: Java
  • Spring Boot: 4.1.0
  • Java: 21
  • Dependencies: Spring Web

Alternatively, If youare using code editor like STS, create a Project using STS.

Or frame it with curl:

bash
curl https://start.spring.io/starter.zip \
  -d dependencies=web \
  -d type=maven-project \
  -d language=java \
  -d bootVersion=4.1.0 \
  -d javaVersion=21 \
  -d artifactId=ai-document-search \
  -o ai-document-search.zip

unzip ai-document-search.zip -d ai-document-search

Spring Boot 4.1.0 is the current stable release as of this writing and requires Java 17 at minimum. Java 21 (LTS) is used here because it’s the most widely deployed LTS release today and needs no preview flags for anything in this tutorial.

Step#2: Add Spring AI, Redis, and Web Dependencies

Open pom.xml and add the Spring AI BOM plus two starters: one for the OpenAI model integration, one for the Redis vector store.


    21
    2.0.0



    
        org.springframework.boot
        spring-boot-starter-web
    

    
        org.springframework.ai
        spring-ai-starter-model-openai
    

    
        org.springframework.ai
        spring-ai-starter-vector-store-redis
    



    
        
            org.springframework.ai
            spring-ai-bom
            ${spring-ai.version}
            pom
            import
        
    

Both starters follow Spring AI’s current naming convention: spring-ai-starter-model-{provider} for model integrations and spring-ai-starter-vector-store-{store} for vector databases. Each starter pulls in its own auto-configuration, so you don’t wire up any beans by hand for the basic case.

Now start Redis. The plain redis Docker image will not work here. You need Redis Stack, which bundles the RediSearch and RedisJSON modules that vector indexing depends on:

bash
docker run -d --name redis-stack -p 6379:6379 redis/redis-stack-server:latest

Step#3: Configure the OpenAI Model and Redis Vector Store

Create src/main/resources/application.yml:

spring:
  data:
    redis:
      url: redis://localhost:6379
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-5-mini
      embedding:
        options:
          model: text-embedding-3-small
    vectorstore:
      redis:
        initialize-schema: true
        index-name: docsearch-index
        prefix: "doc:"

Export your API key before running the app:

bash
export OPENAI_API_KEY=sk-your-key-here

initialize-schema: true is easy to forget and is the single most common cause of a “search always returns nothing” bug. Redis Vector Store does not create its search index automatically — you have to opt in.

text-embedding-3-small is used here because it’s the current cost-effective default for general-purpose semantic search; swap in text-embedding-3-large later if you need higher retrieval accuracy on a large, multilingual corpus and are fine with the extra cost.

Step#4: Model Documents and Build the Ingestion Service

Spring AI already has a Document class (org.springframework.ai.document.Document) that pairs text content with metadata, so you don’t need to define your own entity for this. What you do need is a sensible way to split long text before embedding it: a full 20-page policy PDF crammed into a single embedding produces a vague, low-quality vector, because the embedding has to average the meaning of everything in it.

package com.javatechonline.docsearch.service;

import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;

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

@Service
public class DocumentIngestionService {

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

    public DocumentIngestionService(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    public int ingest(String title, String content, String source) {
        Document original = new Document(content, Map.of(
                "title", title,
                "source", source
        ));

        List chunks = splitter.apply(List.of(original));
        vectorStore.add(chunks);

        return chunks.size();
    }
}

TokenTextSplitter breaks long text into smaller, overlapping chunks sized for the embedding model, and vectorStore.add() handles calling the embedding model and writing the resulting vectors, content, and metadata into Redis. You never touch the raw floating-point vector yourself.

Step#5: How Do You Search Documents by Meaning Instead of Keywords?

You search by meaning by embedding the search query itself, then asking the vector store for the stored documents whose vectors are closest to it. Spring AI’s VectorStore.similaritySearch() does exactly this in one call, hiding the embedding and distance-ranking math behind a simple query-and-topK interface.

First, add a small request/response shape and a controller:

package com.javatechonline.docsearch.web;

public record IngestRequest(String title, String content, String source) {}
package com.javatechonline.docsearch.web;

import java.util.Map;

public record SearchResult(String content, Map metadata, Double score) {}
package com.javatechonline.docsearch.web;

import com.javatechonline.docsearch.service.DocumentIngestionService;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api")
public class DocumentSearchController {

    private final DocumentIngestionService ingestionService;
    private final VectorStore vectorStore;

    public DocumentSearchController(DocumentIngestionService ingestionService,
                                     VectorStore vectorStore) {
        this.ingestionService = ingestionService;
        this.vectorStore = vectorStore;
    }

    @PostMapping("/documents")
    public ResponseEntity ingest(@RequestBody IngestRequest request) {
        int chunks = ingestionService.ingest(request.title(), request.content(), request.source());
        return ResponseEntity.ok("Stored " + chunks + " chunk(s) for '" + request.title() + "'");
    }

    @GetMapping("/search")
    public List search(@RequestParam String query,
                                      @RequestParam(defaultValue = "5") int topK) {

        List results = vectorStore.similaritySearch(
                SearchRequest.builder().query(query).topK(topK).build());

        return results.stream()
                .map(doc -> new SearchResult(doc.getText(), doc.getMetadata(), doc.getScore()))
                .toList();
    }
}

Try it out. First, ingest a document:

curl -X POST http://localhost:8080/api/documents \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Leave Policy",
    "content": "Full-time employees accrue 18 days of paid vacation per year. Unused days roll over up to a maximum of 10 days into the next calendar year.",
    "source": "hr-handbook"
  }'

Then search using completely different wording:

curl "http://localhost:8080/api/search?query=how+much+time+off+do+I+get&topK=3"

You’ll get the leave policy chunk back with a similarity score, even though “time off” never appears in the stored text, that’s the difference between keyword matching and semantic search in one request.

Step#6: How Does RAG (Retrieval-Augmented Generation) Improve Search Answers?

RAG (Retrieval-Augmented Generation) improves on plain search by feeding the top matching documents into a chat model as context, so instead of returning raw text chunks for a human to read, the API returns a direct, natural-language answer grounded in your own data. This is the same pattern behind “chat with your documents” features.

Add a ChatClient and one more endpoint:

package com.javatechonline.docsearch.web;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.stream.Collectors;

@RestController
public class DocumentQaController {

    private final VectorStore vectorStore;
    private final ChatClient chatClient;

    public DocumentQaController(VectorStore vectorStore, ChatClient.Builder chatClientBuilder) {
        this.vectorStore = vectorStore;
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/api/ask")
    public String ask(@RequestParam String question) {

        List relevant = vectorStore.similaritySearch(
                SearchRequest.builder().query(question).topK(3).build());

        String context = relevant.stream()
                .map(Document::getText)
                .collect(Collectors.joining("\n---\n"));

        String promptText = """
                Answer the question using only the context below.
                If the context does not contain the answer, say you don't know.

                Context:
                %s

                Question: %s
                """.formatted(context, question);

        return chatClient.prompt()
                .user(promptText)
                .call()
                .content();
    }
}

Test it:

bash
curl "http://localhost:8080/api/ask?question=how+many+vacation+days+roll+over"

You should get back a plain-English answer like “Up to 10 unused vacation days roll over into the next calendar year,” pulled directly from the ingested policy text rather than the model guessing from general training data.

This manual context-building approach is shown deliberately instead of relying on a pre-built advisor class, because seeing the retrieval and prompt-assembly steps explicitly is what actually teaches you how RAG works. You can always wrap this into an advisor once the mechanics click.

Common Mistakes to Avoid

  • Using plain Redis instead of Redis Stack. Vanilla Redis has no RediSearch or RedisJSON module, so vector indexing fails at startup with an unhelpful error. Always use redis-stack-server for local development.
  • Forgetting initialize-schema: true. Without it, Redis Vector Store never creates its search index, and similaritySearch() silently returns an empty list instead of throwing an error.
  • Embedding whole documents instead of chunks. A single embedding for a 10-page document averages out all its meaning into one vague vector. Always chunk first.
  • Mixing embedding models on the same index. Switching from text-embedding-3-small to text-embedding-3-large mid-project changes the vector dimensions, and old and new vectors become incomparable in the same index. Re-embed everything if you change models.
  • Assuming zero results means an error. An empty topK result is often just an empty or freshly created index, check that ingestion actually ran before debugging the search endpoint.

Interview Questions on This Topic

Q#1 (Easy): What is an embedding, and why is it useful for search?

An embedding is a numeric vector that represents the meaning of a piece of text. Two pieces of text with similar meaning produce vectors that are close together, which lets you rank results by similarity instead of exact word matches.

Q#2 (Medium): How does Redis store and search vector embeddings?

Redis Stack uses RedisJSON to store each document’s content, metadata, and vector together, and RediSearch to build a vector index (typically HNSW) over those embeddings, enabling fast approximate nearest-neighbor lookups.

Q#3 (Medium): Why does chunking matter before generating embeddings?

Embedding models have a limited context window and produce one vector per input. Splitting long documents into smaller, semantically coherent chunks keeps each vector focused, which improves search precision.

Q#4 (Hard): How does RAG differ from just calling a chat model directly?

A plain chat model answers from what it learned during training. RAG first retrieves relevant documents from your own data using vector similarity, then includes that content in the prompt, so the model’s answer is grounded in your specific, current data instead of general knowledge.

Q#5 (Hard): How do you handle updating a document that’s already been indexed as vectors?

Because embeddings aren’t editable in place, the usual pattern is to delete the old chunks for that document (by ID or metadata filter) and re-ingest the updated content as new chunks, rather than trying to modify the stored vector directly.

FAQs (Frequently Asked Questions)

What is AI-powered document search?

It’s a search technique that converts documents and queries into numeric vectors (embeddings) and ranks results by vector similarity, so it can match based on meaning rather than requiring the exact same words to appear in both the query and the document.

Is Spring AI production-ready in 2026?

Yes. Spring AI reached 1.0 GA in 2025 and 2.0 GA shipped alongside Spring Boot 4.1 in June 2026, with first-class auto-configuration, observability, and support for over 20 vector store integrations.

Do I need a GPU to run this Redis vector search example?

No. The embedding computation happens on OpenAI’s servers via the API call; your Spring Boot application and Redis instance only store and compare vectors, which is not GPU-intensive at small to moderate scale.

Can I use a different vector store instead of Redis?

Yes. Spring AI’s VectorStore interface is implemented by over 20 providers, including PostgreSQL (pgvector), Elasticsearch, MongoDB Atlas, and Pinecone. Swapping providers mainly means changing one starter dependency and its configuration block.

How much does the OpenAI embeddings API cost for a small app?

Very little. text-embedding-3-small is priced per token at a fraction of a cent per thousand tokens, so ingesting a few hundred pages of internal documentation typically costs well under a dollar.

Can I use a local, open-source embedding model instead of OpenAI?

Yes. Spring AI supports local models through Ollama, so you can swap the OpenAI starter for the Ollama starter if you want to avoid sending document content to an external API.

This tutorial covers general-purpose technical content. If you’re building document search over sensitive personal or health data, review your organization’s data-handling policies before sending document content to any third-party embedding API.

Conclusion

You now have a working AI-powered document search API: documents go in through /api/documents, get chunked and embedded automatically, and come back out through /api/search ranked by meaning, plus a bonus /api/ask endpoint that turns retrieved chunks into a direct answer. In a real deployment, you’d typically add a cache in front of /api/search for repeated identical queries, since the embedding call is the slowest part of the request path and doesn’t need to be redone for the same text.

What to Learn Next

From here, natural next steps include:

  • Hybrid search: combining vector similarity with traditional keyword filters (e.g., “search by meaning, but only within a specific source metadata field”)
  • Evaluating retrieval quality: measuring whether your top-K results are actually the most relevant ones, not just the closest by cosine distance
  • Trying another vector store, like pgvector, if your team is already running PostgreSQL and doesn’t want to operate Redis Stack separately
  • Exploring Spring AI’s MCP support, which lets your Spring Boot app expose tools to AI agents, not just answer search queries

For the full reference on the Redis Vector Store integration used here, see the official Spring AI documentation, and for embedding model details and pricing, see OpenAI’s embeddings guide.

Got stuck on a step, or want to see this extended into a full hybrid search example? Drop a comment below, we answer these threads regularly.

Leave a Reply



Top