beginner~2h

Embeddings & Vector Databases

Module 01 introduced embeddings conceptually. This module makes them operational: how similarity is actually computed, how to store millions of vectors, and how to pick from the 15+ store integrations Spring AI supports.

Learning objectives

  • Beginner: Explain what an embedding is and why 'closeness' between two embeddings maps to semantic similarity.
  • Intermediate: Use Spring AI's VectorStore abstraction to store and query embeddings without depending on one specific vector database's API.
  • Advanced: Choose an appropriate vector store integration and metadata filtering strategy for a given retrieval use case's scale and query pattern.

Just as ChatModel / ChatClient abstracts chat providers, EmbeddingModel abstracts embedding providers — call embed() with text, get back a float[] vector. Who calculates these values? Not your application — an embedding model (often a smaller, specialized model distinct from your chat model) does, either via the same provider's API or a locally-run model.

float[] vector = embeddingModel.embed("Spring AI makes LLM integration feel like configuring a bean."); // e.g. a 1536-dimension array for OpenAI's text-embedding-3-small

💻 Code example

float[] vector = embeddingModel.embed("Spring AI makes LLM integration feel like configuring a bean."); // e.g. a 1536-dimension array for OpenAI's text-embedding-3-small

◆ The problem

Given a query's embedding and thousands of stored document embeddings, you need a precise, fast way to answer "which stored vectors are most similar to this one?"

Cosine similarity measures the angle between two vectors, ignoring their magnitude — two vectors pointing in nearly the same direction score close to 1 (highly similar) regardless of length; perpendicular vectors score 0 (unrelated); opposite vectors score -1.

Cosine similarity compares direction, not length — doc A's vector points close to the query's direction (high similarity) despite being a different length than doc B's vector.

◆ Under the hood — why angle, not distance

Using raw Euclidean distance instead would make a longer document's embedding (which can have larger magnitude after certain encodings) look "far" from a short, semantically identical query purely because of length, not meaning. Cosine similarity's magnitude-independence is precisely why it, not raw distance, is the standard metric for text embedding comparison.

A vector store persists embeddings alongside their source text and metadata, and answers "find the k nearest vectors to this query vector" efficiently — typically via an approximate nearest-neighbor (ANN) index rather than brute-force comparison against every stored vector, which wouldn't scale past a small collection.

vectorStore.add(List.of( new Document("Spring AI supports 15+ vector store integrations, including Pinecone.", Map.of("source", "docs", "topic", "vector-stores")) )); List<Document> results = vectorStore.similaritySearch( SearchRequest.builder().query("how many vector databases does Spring AI support?").topK(3).build());

▲ Version note

SearchRequest.query(...).withTopK(...) (the static-factory + with-prefixed builder pattern) is DEPRECATED as of Spring AI 1.0.0-M5 and removed going forward. Current code should use SearchRequest.builder().query(...).topK(...).build() — the fluent builder shown above.

💻 Code example

vectorStore.add(List.of( new Document("Spring AI supports 15+ vector store integrations, including Pinecone.", Map.of("source", "docs", "topic", "vector-stores")) )); List<Document> results = vectorStore.similaritySearch( SearchRequest.builder().query("how many vector databases does Spring AI support?").topK(3).build());

Spring AI's VectorStore interface is implemented against a wide range of backends — the code above is identical regardless of which one you pick; only the starter dependency and connection properties change, exactly like Module 02's provider-swapping demonstration for chat models.

StoreGood fit when…
PGVectorYou already run PostgreSQL — adds vector search as an extension with zero new infrastructure.
QdrantYou want a purpose-built, high-performance vector engine with rich filtering, run via Docker.
RedisYou already run Redis for caching and want vectors co-located with other fast-access data.
ChromaLightweight, developer-friendly, good for local prototyping and smaller-scale RAG.
PineconeYou want a fully-managed, purpose-built vector database with no infrastructure of your own to run.
MongoDB AtlasYou already run MongoDB Atlas and want vector search alongside your document data.
CassandraYou need vector search at very large, horizontally-scaled, multi-datacenter deployments.
Neo4jYour data is naturally graph-shaped and you want similarity search combined with graph traversal.
OracleYou're already an Oracle DB shop with compliance/procurement reasons to stay there.
MilvusPurpose-built, open-source, very large-scale vector search with tunable ANN index types.
Typesense / OpenSearch / Elasticsearch / WeaviateYou want vector search combined with strong traditional full-text search in the same engine.

This is a representative cross-section, not the full list — Spring AI supports 15+ vector store integrations in total (also including Azure Vector Search, GemFire, MariaDB, and others), all behind the same VectorStore interface.

▲ Pitfall

Portability at the code level doesn't mean portability of behavior — different stores support different ANN index types, filtering expressiveness, and consistency guarantees. Swapping the dependency in a demo is trivial; swapping it under a production workload without re-validating recall/latency characteristics is not.

Similarity search alone can't express exact, structured constraints ("only documents from 2024," "only this tenant's data"). A metadata filter combines a similarity search with a structured predicate evaluated against each document's stored metadata.

FilterExpressionBuilder b = new FilterExpressionBuilder(); Filter.Expression filter = b.and( b.eq("tenant", "acme-corp"), b.gt("year", 2023) ).build(); List<Document> results = vectorStore.similaritySearch( SearchRequest.query("refund policy").withFilterExpression(filter).withTopK(5));

◆ Under the hood — why metadata filtering matters for multi-tenancy

Without a tenant filter, a similarity search over a shared vector store could return another tenant's documents purely because they're semantically similar — a serious data-isolation bug, not just a relevance issue. In any multi-tenant RAG system, a tenant-scoped metadata filter is a security control, not an optional relevance tweak.

✓ Quick recap

Why does cosine similarity ignore vector magnitude? So a longer document's embedding isn't penalized as "dissimilar" purely due to length — only direction (meaning) is compared. What stays the same, and what changes, when swapping one VectorStore implementation for another? Your Java code (add/similaritySearch calls) stays identical; only the starter dependency and connection configuration change. Why is a metadata filter a security control in a multi-tenant RAG system? Without it, similarity search alone can return another tenant's documents purely because they're semantically related.

💻 Code example

FilterExpressionBuilder b = new FilterExpressionBuilder(); Filter.Expression filter = b.and( b.eq("tenant", "acme-corp"), b.gt("year", 2023) ).build(); List<Document> results = vectorStore.similaritySearch( SearchRequest.query("refund policy").withFilterExpression(filter).withTopK(5));

Want a visual for this concept?

Generate a diagram tailored to “Embeddings & Vector Databases” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Retrieval-Augmented Generation (RAG)← Back to all Spring AI chapters