CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-springframework-ai--spring-ai-vector-store

Common vector store functionality for Spring AI providing a portable abstraction layer for integrating vector databases with comprehensive filtering, similarity search, and observability support.

Overview
Eval results
Files

real-world-scenarios.mddocs/examples/

Real-World Scenarios

Practical examples of using Spring AI Vector Store in real applications.

Scenario 1: Document Search Service

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));
    }
}

Scenario 2: Knowledge Base with Categories

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);
    }
}

Scenario 3: Multi-Tenant Vector Store

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 + "'");
    }
}

Scenario 4: Versioned Documents

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

Scenario 5: Cached Embeddings

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
    }
}

Scenario 6: Batch Indexing with Progress

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) {}
}

Scenario 7: Periodic Cleanup

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

Scenario 8: Similarity Threshold Tuning

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

Scenario 9: Migration Between Vector Stores

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);
    }
}

Scenario 10: Health Check

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

Performance Tuning

Batch Size Optimization

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

Connection Pooling (for DB-backed stores)

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000

Caching Strategy

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

Testing Patterns

Integration Test with Testcontainers

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

Reference

  • Quick Start Guide - Getting started
  • Filtering Guide - Advanced filtering
  • Edge Cases - Error handling
  • API Reference - Complete API

Install with Tessl CLI

npx tessl i tessl/maven-org-springframework-ai--spring-ai-vector-store

docs

examples

edge-cases.md

real-world-scenarios.md

index.md

tile.json