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=truefor 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_DISTANCEwith HNSW for best performance
Comments