Java SpringAI

Spring AI with PGVector — Production Vector Store Using PostgreSQL

Spring AI with PGVector — Production Vector Store Using PostgreSQL

PGVector is a PostgreSQL extension that adds vector similarity search to a regular PostgreSQL database. It is the most popular production vector store for Spring AI applications because it runs in the same database you already use, supports ACID transactions, and integrates with Spring Data. This tutorial sets up PGVector with Docker and builds a knowledge base search service.

Start PostgreSQL with PGVector Using Docker

# docker-compose.yml
version: '3.8'
services:
  pgvector:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_DB:       vectordb
      POSTGRES_USER:     postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - "5432:5432"
    volumes:
      - pgvector_data:/var/lib/postgresql/data

volumes:
  pgvector_data:
docker-compose up -d

Maven Dependencies

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

application.properties

spring.datasource.url=jdbc:postgresql://localhost:5432/vectordb
spring.datasource.username=postgres
spring.datasource.password=postgres

spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.embedding.options.model=text-embedding-3-small

# Spring AI will auto-create the vector table on startup
spring.ai.vectorstore.pgvector.initialize-schema=true
spring.ai.vectorstore.pgvector.dimensions=1536
spring.ai.vectorstore.pgvector.distance-type=COSINE_DISTANCE
spring.ai.vectorstore.pgvector.index-type=HNSW

Auto-Created Table Schema

When initialize-schema=true, Spring AI creates this table automatically:

-- Created by Spring AI automatically
CREATE TABLE IF NOT EXISTS vector_store (
    id          UUID         DEFAULT gen_random_uuid() PRIMARY KEY,
    content     TEXT,
    metadata    JSON,
    embedding   VECTOR(1536)
);

CREATE INDEX ON vector_store USING hnsw (embedding vector_cosine_ops);

DocumentIngestionService.java

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

@Service
public class DocumentIngestionService {

    private final VectorStore vectorStore;
    private final TokenTextSplitter splitter;

    public DocumentIngestionService(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
        this.splitter    = new TokenTextSplitter(800, 100, 5, 10000, true);
        //                                        ↑    ↑   overlap
        //                                        chunk size
    }

    public int ingest(List<Document> documents, String source) {
        // Add source metadata
        documents.forEach(doc ->
                doc.getMetadata().put("source", source));

        // Split into chunks, then embed and store
        List<Document> chunks = splitter.apply(documents);
        vectorStore.add(chunks);

        System.out.printf("Ingested %d chunks from [%s]%n", chunks.size(), source);
        return chunks.size();
    }
}

KnowledgeSearchService.java

import org.springframework.ai.vectorstore.SearchRequest;

@Service
public class KnowledgeSearchService {

    private final VectorStore vectorStore;
    private final ChatClient  chatClient;

    public KnowledgeSearchService(VectorStore vectorStore, ChatClient.Builder builder) {
        this.vectorStore = vectorStore;
        this.chatClient  = builder
                .defaultAdvisors(new QuestionAnswerAdvisor(
                        vectorStore,
                        SearchRequest.defaults().withTopK(5).withSimilarityThreshold(0.65)
                ))
                .build();
    }

    // RAG-powered answer
    public String answer(String question) {
        return chatClient.prompt()
                .user(question)
                .call()
                .content();
    }

    // Raw search — see which chunks were retrieved
    public List<Document> search(String query, int topK) {
        return vectorStore.similaritySearch(
                SearchRequest.query(query).withTopK(topK)
        );
    }
}

Ingestion and Query Demo

@SpringBootApplication
public class PgVectorDemo implements CommandLineRunner {

    @Autowired DocumentIngestionService ingestion;
    @Autowired KnowledgeSearchService   search;

    @Override
    public void run(String... args) {
        // Index documents
        List<Document> docs = List.of(
            new Document("Spring Boot 3.x requires Java 17 or later and uses Jakarta EE 9 namespaces."),
            new Document("Spring Data JPA auto-implements repository interfaces. Annotate with @Repository."),
            new Document("@Transactional rolls back on RuntimeException by default. Use rollbackFor for checked exceptions."),
            new Document("Spring Security 6 uses a SecurityFilterChain bean instead of WebSecurityConfigurerAdapter.")
        );

        ingestion.ingest(docs, "spring-docs");

        // Query
        System.out.println(search.answer("What Java version does Spring Boot 3 need?"));
        System.out.println(search.answer("How does Spring Security 6 differ from earlier versions?"));
    }
}

Output

Ingested 4 chunks from [spring-docs]

Spring Boot 3.x requires Java 17 or later. It also uses Jakarta EE 9 namespaces
instead of the older javax namespace.

Spring Security 6 changed the configuration model. Instead of extending
WebSecurityConfigurerAdapter, you now define a SecurityFilterChain bean.

Filter by Metadata

// Only search documents from a specific source
List<Document> results = vectorStore.similaritySearch(
        SearchRequest.query("transaction rollback")
                .withTopK(3)
                .withFilterExpression("source == 'spring-docs'")
);
results.forEach(d -> System.out.println(d.getContent()));

Key Points

  • Set spring.ai.vectorstore.pgvector.initialize-schema=true for development; create the schema manually in production
  • HNSW index provides fast approximate nearest-neighbor search — much faster than exact search on large datasets
  • TokenTextSplitter(800, 100, ...) — chunk size 800 tokens, 100-token overlap to preserve context across chunk boundaries
  • PGVector supports full SQL — you can join vector search results with your regular tables
  • For large datasets (100k+ chunks), set distance-type=COSINE_DISTANCE with HNSW for best performance
Topics: Java SpringAI
← Newer Post Older Post →