Spring AI — Semantic Search with SimpleVectorStore and OpenAI

A hands-on Spring Boot example that shows how to build semantic search over a PDF document using Spring AI, OpenAI embeddings and the in-memory SimpleVectorStore. It also includes a set of small embedding experiments (cosine similarity, similarity matrices) and a basic chat REST endpoint.

View source on GitHub Portfolio index

A hands-on Spring Boot example that shows how to build semantic search over a PDF document using Spring AI, OpenAI embeddings and the in-memory SimpleVectorStore. It also includes a set of small embedding experiments (cosine similarity, similarity matrices) and a basic chat REST endpoint.

This project is part of the Java & Spring AI Examples series (module 03 – Semantic Search).


Table of Contents


What it does

On startup the application:

  1. Reads src/main/resources/documents/spring_ai.pdf page by page.
  2. Splits the pages into token-based chunks.
  3. Generates an embedding for every chunk using OpenAI.
  4. Stores the chunks and their vectors in an in-memory SimpleVectorStore.
  5. Runs a sample semantic query ("¿Por qué usar RAG?") and prints the top 3 most similar chunks with their similarity score, metadata and a text preview.

It also exposes a simple REST endpoint to chat with the OpenAI model through Spring AI's ChatClient.

Tech stack

Component Version / Detail
Java 24
Spring Boot 4.1.0
Spring AI 2.0.0 (via spring-ai-bom)
AI provider OpenAI (chat + embeddings)
Vector store SimpleVectorStore (in-memory)
Document reader PagePdfDocumentReader (spring-ai-pdf-document-reader)
Text splitter TokenTextSplitter
Build tool Maven (wrapper included)

Dependencies

Project structure

src/main/
├── java/com/mx/development/jose/said/
│   ├── ApiSpringAiDemoV1Application.java   # Spring Boot entry point
│   ├── config/
│   │   └── VectorStoreConfig.java          # SimpleVectorStore bean
│   ├── controller/
│   │   └── ChatController.java             # GET /api/v1/chat-service/chat
│   ├── dto/
│   │   ├── IndexingResult.java             # Pages read / chunks indexed
│   │   └── Resultado.java                  # Text + similarity score
│   ├── runner/
│   │   ├── PdfIndexingRunner.java          # Indexes the PDF and runs a query at startup
│   │   └── SemanticSearchRunner.java       # Embedding / cosine similarity experiments (disabled)
│   └── service/
│       ├── chat/
│       │   ├── ChatService.java
│       │   └── ChatServiceImpl.java        # ChatClient wrapper
│       └── pdf/
│           └── PdfIndexingService.java     # Indexing + similarity search
└── resources/
    ├── application.yml                     # Port, profile, logging
    ├── application-openai.yml              # OpenAI API key
    └── documents/
        └── spring_ai.pdf                   # Source document to index

How it works

                 ┌──────────────────────── INDEXING (once) ────────────────────────┐
 spring_ai.pdf → PagePdfDocumentReader → TokenTextSplitter → OpenAI Embeddings → SimpleVectorStore
                 └─────────────────────────────────────────────────────────────────┘

                 ┌──────────────────────── SEARCH (n times) ───────────────────────┐
 user query    → OpenAI Embedding → similaritySearch(topK=3, threshold=0.50) → ranked chunks
                 └─────────────────────────────────────────────────────────────────┘

1. Indexing (runs once)

PdfIndexingService.indexar(Resource):

2. Semantic search (runs n times)

PdfIndexingService.buscar(String) builds a SearchRequest with:

Parameter Value Meaning
topK 3 Return at most the 3 most similar chunks
similarityThreshold 0.50 Discard chunks with similarity score below 0.50

The query is embedded with the same model and compared against the stored vectors (cosine similarity). Matches are returned as Documents with a score.

PdfIndexingRunner (a CommandLineRunner) orchestrates both steps at startup and prints the results to the console.

3. Embedding experiments

SemanticSearchRunner contains several self-contained CommandLineRunner beans to explore how embeddings behave. They are disabled by default (the class's @Component and most @Bean annotations are commented out). Enable the one you want to try:

Runner What it shows
embedding Generates a single embedding, prints its dimension and the first 10 values.
embeddingCompare Cosine similarity between technical sentences vs. unrelated ones (pizza, dog).
embedding2 Similarity matrix across terms like Inteligencia Artificial, Artificial Intelligence, Machine Learning, Deep Learning, Perro.
embedding3 Similarity matrix of synonyms (Automóvil, Coche, Carro, …) and unrelated words.
embedding4 Ranks a list of words by similarity to a query (e.g. a misspelled "Pinsa" vs "Pizza").

The class also includes a manual cosineSimilarity(float[], float[]) implementation for educational purposes.

To enable them, uncomment @Component on SemanticSearchRunner and the @Bean annotation of the runner(s) you want to execute.

4. Chat endpoint

ChatServiceImpl wraps Spring AI's ChatClient and sends the user question directly to the OpenAI chat model (no retrieval augmentation — the vector store is not used here).

Prerequisites

Configuration

application.yml

server:
  port: 8088
spring:
  application:
    name: api-spring-ai-demo-v1
  profiles:
    active: openai
logging:
  level:
    org.springframework.ai: DEBUG
    org.springframework.web.client: DEBUG
app:
  indexing:
    enabled: true

application-openai.yml

spring:
  ai:
    openai:
      api-key: ${API_KEY}

The OpenAI key is read from the API_KEY environment variable. Never commit your real key to the repository.

Note: with DEBUG logging enabled for org.springframework.ai and org.springframework.web.client, request/response details are written to the console. Lower the level to INFO if you don't need them.

Running the application

From the command line

export API_KEY=sk-...your-openai-key...
./mvnw spring-boot:run

Or build and run the JAR:

./mvnw clean package
API_KEY=sk-... java -jar target/api-spring-ai-simple-vector-store-openai-v1-0.0.1-SNAPSHOT.jar

From IntelliJ IDEA

  1. Open Run → Edit Configurations… and select ApiSpringAiDemoV1Application.
  2. In Environment variables, add API_KEY=<your OpenAI key>.
  3. Run the application.

The app starts on http://localhost:8088.

Sample output

INDEXACIÓN DEL DOCUMENTO
------------------------------
Documentos leídos  : <pages>
Chunks indexados   : <chunks>
Estado             : Finalizado

BÚSQUEDA SEMÁNTICA
------------------------------
Consulta: ¿Por qué usar RAG?
Resultados encontrados: 3

RESULTADO 1
------------------------------
Similitud : 0.8xxx
Metadatos : {page_number=..., file_name=spring_ai.pdf, ...}
Contenido : <first 200 characters of the matching chunk>...
...

Actual numbers depend on the PDF content and the embedding model.

REST API

GET /api/v1/chat-service/chat

Sends a question to the OpenAI chat model and returns the plain-text answer.

Query param Type Required Description
question string yes The prompt to send

Example

curl "http://localhost:8088/api/v1/chat-service/chat?question=What%20is%20Spring%20AI%3F"

Customization

Limitations

Key concepts