Spring Boot Tutorial
Embeddings and Vector Stores
Keyword search fails when users do not use your exact words: a search for "can't log in" misses the help article titled "Resetting your password". Embeddings fix this by representing text as a list of numbers (a vector) that captures its meaning, so texts with similar meaning end up close together even when they share no words.
A vector store is a database optimised for storing those vectors and finding the nearest ones to a query. Together they power semantic search, recommendations, duplicate detection and — most importantly — Retrieval Augmented Generation. This lesson shows how to create embeddings, store documents with metadata, search by similarity and filter results in Spring AI, using PostgreSQL with PGVector.
What an Embedding Is
An embedding model converts text into a fixed-length vector, for example 1536 floating-point numbers. The model is trained so that sentences with similar meaning produce vectors pointing in similar directions. The similarity of two vectors is usually measured with cosine similarity: 1.0 means identical direction, around 0 means unrelated.
Embeddings are cheap compared with chat calls, and you embed each document only once when you store it. Always use the same embedding model for storing and for querying; vectors from different models are not comparable.
EmbeddingModel in Spring AI
Model starters auto-configure an EmbeddingModel bean (for example OpenAI's text-embedding-3-small or Ollama's nomic-embed-text). Call embed(text) to get a float[], or embedForResponse(list) for batches. Most of the time you do not call it directly; the vector store calls it for you.
Documents and Metadata
Spring AI stores content as Document objects: text plus a metadata map (source file, category, language, tenant id, last updated). Metadata is essential: it lets you filter searches ("only billing articles", "only this customer's files") and cite sources in answers.
Choosing a Vector Store
If you already run PostgreSQL, the PGVector extension is usually the simplest choice: vectors live next to your relational data, backed up and secured the same way. Redis, MongoDB Atlas, Elasticsearch/OpenSearch and dedicated engines like Qdrant, Milvus, Weaviate and Pinecone suit larger or specialised workloads. SimpleVectorStore keeps everything in memory and is only for demos and tests. All share the same VectorStore interface, so switching later mostly means changing the starter.
Similarity Search and Filters
vectorStore.similaritySearch(SearchRequest) embeds the query and returns the closest documents. Tune topK (how many results) and similarityThreshold (minimum score, 0–1), and add a filterExpression such as "category == 'billing' && year >= 2025" that is translated to the store's native filter language.
Examples
Comparing sentences with embeddings
@Component
public class EmbeddingDemo implements CommandLineRunner {
private final EmbeddingModel embeddingModel;
public EmbeddingDemo(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
@Override
public void run(String... args) {
float[] a = embeddingModel.embed("I can't log in to my account");
float[] b = embeddingModel.embed("How do I reset my password?");
float[] c = embeddingModel.embed("Best biryani recipe");
System.out.println("dimensions = " + a.length);
System.out.printf("login vs password = %.2f%n", cosine(a, b));
System.out.printf("login vs biryani = %.2f%n", cosine(a, c));
}
static double cosine(float[] x, float[] y) {
double dot = 0, nx = 0, ny = 0;
for (int i = 0; i < x.length; i++) {
dot += x[i] * y[i];
nx += x[i] * x[i];
ny += y[i] * y[i];
}
return dot / (Math.sqrt(nx) * Math.sqrt(ny));
}
}
dimensions = 1536
login vs password = 0.71
login vs biryani = 0.08
PGVector setup: dependencies, Docker and properties
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
# compose.yaml — Spring Boot's Docker Compose support starts it automatically
services:
postgres:
image: pgvector/pgvector:pg17
environment:
POSTGRES_DB: webnest
POSTGRES_USER: webnest
POSTGRES_PASSWORD: webnest
ports:
- "5432:5432"
# application.yml
spring:
ai:
vectorstore:
pgvector:
initialize-schema: true
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1536
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS public.vector_store (id uuid PRIMARY KEY, content text, metadata json, embedding vector(1536));
CREATE INDEX ... USING HNSW (embedding vector_cosine_ops);
Storing documents with metadata and searching them
@Service
public class HelpCenterSearch {
private final VectorStore vectorStore;
public HelpCenterSearch(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void load() {
vectorStore.add(List.of(
new Document("To reset your password, click 'Forgot password' on the login page.",
Map.of("category", "account", "articleId", "KB-1")),
new Document("Refunds are available within 7 days of purchase from the Billing page.",
Map.of("category", "billing", "articleId", "KB-2")),
new Document("Certificates are issued after you complete every lesson and quiz.",
Map.of("category", "courses", "articleId", "KB-3"))));
}
public List<Document> search(String query) {
return vectorStore.similaritySearch(SearchRequest.builder()
.query(query)
.topK(2)
.similarityThreshold(0.5)
.build());
}
public List<Document> searchBilling(String query) {
return vectorStore.similaritySearch(SearchRequest.builder()
.query(query)
.topK(3)
.filterExpression("category == 'billing'")
.build());
}
}
search("I forgot my login details")
-> [KB-1] To reset your password, click 'Forgot password' on the login page. (score 0.78)
searchBilling("can I get my money back")
-> [KB-2] Refunds are available within 7 days of purchase from the Billing page. (score 0.74)
Filter expressions built in code (safe with user input)
FilterExpressionBuilder b = new FilterExpressionBuilder();
SearchRequest request = SearchRequest.builder()
.query(question)
.topK(5)
.filterExpression(b.and(
b.eq("tenantId", currentTenantId), // never let a tenant see another tenant's docs
b.in("category", "billing", "account"))
.build())
.build();
List<Document> results = vectorStore.similaritySearch(request);
(Translated by PGVector into: metadata::jsonb @@ '$.tenantId == "acme" && ($.category == "billing" || $.category == "account")')
Common Mistakes
- Changing the embedding model after documents are stored — old and new vectors are incompatible, so everything must be re-embedded.
- Setting vector dimensions in the store that do not match the embedding model's output size.
- Storing documents without metadata, making it impossible to filter by tenant or cite sources.
- Building filter expressions by concatenating user input into a string instead of using FilterExpressionBuilder.
- Using SimpleVectorStore in production; it lives in memory and does not scale.
Key Points to Remember
- Embeddings turn text into vectors where similar meanings are close together.
- Use the same embedding model for indexing and querying, and match the store's dimensions to it.
- Documents carry metadata that powers filtering, multi-tenancy and source citations.
- PGVector is a practical default when you already use PostgreSQL; all stores share the VectorStore API.
- similaritySearch with topK, similarityThreshold and filterExpression controls relevance.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.