Common vector store functionality for Spring AI providing a portable abstraction layer for integrating vector databases with comprehensive filtering, similarity search, and observability support.
Practical examples of using Spring AI Vector Store in real applications.
Build a semantic search service for documents with metadata filtering.
@Service
public class DocumentSearchService {
private final VectorStore vectorStore;
public DocumentSearchService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public List<Document> searchDocuments(
String query,
String category,
Integer minYear,
Boolean featuredOnly) {
// Build dynamic filter
FilterExpressionBuilder b = new FilterExpressionBuilder();
List<FilterExpressionBuilder.Op> conditions = new ArrayList<>();
if (category != null) {
conditions.add(b.eq("category", category));
}
if (minYear != null) {
conditions.add(b.gte("year", minYear));
}
if (featuredOnly != null && featuredOnly) {
conditions.add(b.eq("featured", true));
}
// Combine conditions
Filter.Expression filter = conditions.isEmpty() ? null :
conditions.stream()
.reduce((a, b) -> b.and(a, b))
.get()
.build();
// Search
SearchRequest.Builder requestBuilder = SearchRequest.builder()
.query(query)
.topK(20)
.similarityThreshold(0.7);
if (filter != null) {
requestBuilder.filterExpression(filter);
}
return vectorStore.similaritySearch(requestBuilder.build());
}
public void indexDocument(String content, Map<String, Object> metadata) {
Document doc = new Document(content, metadata);
vectorStore.add(List.of(doc));
}
}Implement a categorized knowledge base with RAG (Retrieval Augmented Generation).
@Service
public class KnowledgeBaseService {
private final VectorStore vectorStore;
private final ChatClient chatClient;
public KnowledgeBaseService(VectorStore vectorStore, ChatClient chatClient) {
this.vectorStore = vectorStore;
this.chatClient = chatClient;
}
public void addArticle(String title, String content, String category, List<String> tags) {
Map<String, Object> metadata = Map.of(
"title", title,
"category", category,
"tags", tags,
"createdAt", Instant.now().toString(),
"type", "article"
);
Document doc = new Document(content, metadata);
vectorStore.add(List.of(doc));
}
public String answerQuestion(String question, String category) {
// 1. Retrieve relevant documents
SearchRequest request = SearchRequest.builder()
.query(question)
.topK(5)
.similarityThreshold(0.75)
.filterExpression(category != null ?
"category == '" + category + "'" : null)
.build();
List<Document> relevantDocs = vectorStore.similaritySearch(request);
// 2. Build context from documents
String context = relevantDocs.stream()
.map(doc -> String.format("Article: %s\nContent: %s",
doc.getMetadata().get("title"),
doc.getContent()))
.collect(Collectors.joining("\n\n"));
// 3. Generate answer using RAG
String prompt = String.format("""
Answer the following question based on the provided context.
If the answer cannot be found in the context, say so.
Context:
%s
Question: %s
Answer:
""", context, question);
return chatClient.call(prompt);
}
}Implement tenant isolation using metadata filtering.
@Service
public class MultiTenantVectorStoreService {
private final VectorStore vectorStore;
public void addDocumentForTenant(String tenantId, Document doc) {
// Add tenant ID to metadata
Map<String, Object> metadata = new HashMap<>(doc.getMetadata());
metadata.put("tenantId", tenantId);
metadata.put("createdAt", Instant.now().toString());
Document tenantDoc = new Document(doc.getId(), doc.getContent(), metadata);
vectorStore.add(List.of(tenantDoc));
}
public List<Document> searchForTenant(String tenantId, String query, int topK) {
// Always filter by tenant ID
SearchRequest request = SearchRequest.builder()
.query(query)
.topK(topK)
.filterExpression("tenantId == '" + tenantId + "'")
.build();
return vectorStore.similaritySearch(request);
}
public void deleteDocumentsForTenant(String tenantId) {
// Bulk delete all documents for tenant
vectorStore.delete("tenantId == '" + tenantId + "'");
}
}Handle document versions with latest-version filtering.
@Service
public class VersionedDocumentService {
private final VectorStore vectorStore;
public void addDocumentVersion(String documentId, String content, int version) {
Map<String, Object> metadata = Map.of(
"documentId", documentId,
"version", version,
"isLatest", true,
"createdAt", Instant.now().toString()
);
// Mark previous versions as not latest
vectorStore.delete(String.format(
"documentId == '%s' && isLatest == true", documentId
));
// Add new version
String versionedId = documentId + "-v" + version;
Document doc = new Document(versionedId, content, metadata);
vectorStore.add(List.of(doc));
}
public List<Document> searchLatestVersions(String query) {
SearchRequest request = SearchRequest.builder()
.query(query)
.filterExpression("isLatest == true")
.topK(10)
.build();
return vectorStore.similaritySearch(request);
}
public List<Document> getDocumentHistory(String documentId) {
SearchRequest request = SearchRequest.builder()
.query(documentId) // Use document ID as query
.filterExpression("documentId == '" + documentId + "'")
.topK(100)
.build();
return vectorStore.similaritySearch(request).stream()
.sorted((a, b) -> {
int versionA = (int) a.getMetadata().get("version");
int versionB = (int) b.getMetadata().get("version");
return Integer.compare(versionB, versionA); // Newest first
})
.toList();
}
}Optimize performance by caching embeddings.
@Service
public class CachedEmbeddingService {
private final VectorStore vectorStore;
private final EmbeddingModel embeddingModel;
private final Cache<String, float[]> embeddingCache;
public CachedEmbeddingService(
VectorStore vectorStore,
EmbeddingModel embeddingModel,
CacheManager cacheManager) {
this.vectorStore = vectorStore;
this.embeddingModel = embeddingModel;
this.embeddingCache = cacheManager.getCache("embeddings");
}
public void addDocumentWithCaching(String content, Map<String, Object> metadata) {
// Check cache first
float[] embedding = embeddingCache.get(content);
if (embedding == null) {
// Generate embedding
embedding = embeddingModel.embed(content).getOutput();
// Cache it
embeddingCache.put(content, embedding);
}
// Create document with pre-computed embedding
Document doc = new Document(content, metadata);
doc.setEmbedding(embedding);
vectorStore.add(List.of(doc)); // No re-embedding
}
}Index large document sets with progress tracking.
@Service
public class BatchIndexingService {
private final VectorStore vectorStore;
private final BatchingStrategy batchingStrategy;
public void indexDocuments(
List<Document> documents,
Consumer<IndexProgress> progressCallback) {
int total = documents.size();
int processed = 0;
int batchSize = 100;
for (int i = 0; i < documents.size(); i += batchSize) {
int end = Math.min(i + batchSize, documents.size());
List<Document> batch = documents.subList(i, end);
try {
vectorStore.add(batch);
processed += batch.size();
// Report progress
IndexProgress progress = new IndexProgress(
processed,
total,
(double) processed / total * 100
);
progressCallback.accept(progress);
} catch (Exception e) {
logger.error("Failed to index batch {}-{}", i, end, e);
// Continue with next batch or implement retry logic
}
}
}
public record IndexProgress(int processed, int total, double percentComplete) {}
}Automatically remove old or inactive documents.
@Service
public class CleanupService {
private final VectorStore vectorStore;
@Scheduled(cron = "0 0 2 * * *") // Run at 2 AM daily
public void cleanupOldDocuments() {
LocalDate cutoffDate = LocalDate.now().minusMonths(6);
int cutoffYear = cutoffDate.getYear();
int cutoffMonth = cutoffDate.getMonthValue();
// Delete documents older than 6 months
String filter = String.format(
"(year < %d) || (year == %d && month < %d)",
cutoffYear, cutoffYear, cutoffMonth
);
try {
vectorStore.delete(filter);
logger.info("Cleaned up documents older than {}", cutoffDate);
} catch (UnsupportedOperationException e) {
// Fall back to ID-based deletion
logger.warn("Filter-based deletion not supported, using manual cleanup");
cleanupManually(cutoffDate);
}
}
private void cleanupManually(LocalDate cutoffDate) {
// Search for old documents
SearchRequest request = SearchRequest.builder()
.query("*") // Match all
.topK(1000)
.filterExpression(String.format("year < %d", cutoffDate.getYear()))
.build();
List<Document> oldDocs = vectorStore.similaritySearch(request);
List<String> idsToDelete = oldDocs.stream()
.map(Document::getId)
.toList();
if (!idsToDelete.isEmpty()) {
vectorStore.delete(idsToDelete);
logger.info("Deleted {} old documents", idsToDelete.size());
}
}
}Dynamically adjust similarity threshold based on result quality.
@Service
public class AdaptiveSearchService {
private final VectorStore vectorStore;
public List<Document> searchWithAdaptiveThreshold(String query, int minResults) {
double[] thresholds = {0.9, 0.8, 0.7, 0.6, 0.5};
for (double threshold : thresholds) {
SearchRequest request = SearchRequest.builder()
.query(query)
.topK(20)
.similarityThreshold(threshold)
.build();
List<Document> results = vectorStore.similaritySearch(request);
if (results.size() >= minResults) {
logger.info("Found {} results with threshold {}",
results.size(), threshold);
return results;
}
}
// If still no results, return whatever we can find
return vectorStore.similaritySearch(
SearchRequest.builder()
.query(query)
.topK(20)
.similarityThresholdAll()
.build()
);
}
}Migrate data from development to production vector store.
@Service
public class MigrationService {
public void migrateData(VectorStore source, VectorStore target) {
logger.info("Starting migration...");
int batchSize = 100;
int offset = 0;
int totalMigrated = 0;
while (true) {
// Note: This is a simplified example
// Actual implementation depends on store capabilities
SearchRequest request = SearchRequest.builder()
.query("*") // Get all documents
.topK(batchSize)
.build();
List<Document> batch = source.similaritySearch(request);
if (batch.isEmpty()) {
break;
}
// Add to target
target.add(batch);
totalMigrated += batch.size();
logger.info("Migrated {} documents", totalMigrated);
if (batch.size() < batchSize) {
break; // Last batch
}
offset += batchSize;
}
logger.info("Migration complete. Total documents migrated: {}", totalMigrated);
}
}Implement health checks for vector store availability.
@Component
public class VectorStoreHealthIndicator implements HealthIndicator {
private final VectorStore vectorStore;
public VectorStoreHealthIndicator(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
@Override
public Health health() {
try {
// Try a simple operation
SearchRequest request = SearchRequest.builder()
.query("health check")
.topK(1)
.build();
long startTime = System.currentTimeMillis();
vectorStore.similaritySearch(request);
long duration = System.currentTimeMillis() - startTime;
return Health.up()
.withDetail("responseTime", duration + "ms")
.withDetail("status", "operational")
.build();
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.withDetail("status", "unavailable")
.build();
}
}
}@Configuration
public class PerformanceConfig {
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
// Optimize batch size for your use case
BatchingStrategy batchingStrategy = new TokenCountBatchingStrategy(
embeddingModel,
8000, // Max tokens per batch
100 // Max documents per batch
);
return SimpleVectorStore.builder(embeddingModel)
.batchingStrategy(batchingStrategy)
.build();
}
}spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new CaffeineCacheManager("embeddings", "searchResults");
}
@Bean
public Caffeine<Object, Object> caffeineConfig() {
return Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES);
}
}@SpringBootTest
@Testcontainers
class VectorStoreIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("pgvector/pgvector:pg16");
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private VectorStore vectorStore;
@Test
void testVectorStoreOperations() {
// Test with real database
vectorStore.add(testDocuments);
List<Document> results = vectorStore.similaritySearch(testRequest);
assertThat(results).isNotEmpty();
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-springframework-ai--spring-ai-vector-store@1.1.0