Course topics

By WebNest Studio

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

Java
@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));
    }
}
Output
dimensions = 1536
login vs password = 0.71
login vs biryani  = 0.08

PGVector setup: dependencies, Docker and properties

Java
<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
Output
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

Java
@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());
    }
}
Output
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)

Java
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);
Output
(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.