RAG (Retrieval-Augmented Generation) framework for the Embabel Agent platform providing content ingestion, chunking, hierarchical navigation, and semantic search capabilities
Deep integration with Spring AI framework, providing adapters and converters for using RAG capabilities with Spring AI's VectorStore abstraction. This guide covers advanced patterns for production Spring applications.
The framework provides seamless integration with Spring AI through:
Adapter class that wraps Spring AI VectorStore to provide FilteringVectorSearch capabilities.
class SpringVectorStoreVectorSearch(
/**
* Spring AI VectorStore instance to wrap
*/
val vectorStore: VectorStore
) : FilteringVectorSearch, TypeRetrievalOperations {
/**
* Perform vector similarity search with optional filtering
* @param request Search request with query and parameters
* @param clazz Class of retrievable items to search
* @param metadataFilter Optional property filter for metadata
* @param entityFilter Optional entity filter for labels
* @return List of similarity results
*/
override fun <T : Retrievable> vectorSearchWithFilter(
request: TextSimilaritySearchRequest,
clazz: Class<T>,
metadataFilter: PropertyFilter?,
entityFilter: EntityFilter?
): List<SimilarityResult<T>>
/**
* Perform basic vector similarity search
* @param request Search request with query and parameters
* @param clazz Class of retrievable items to search
* @return List of similarity results
*/
override fun <T : Retrievable> vectorSearch(
request: TextSimilaritySearchRequest,
clazz: Class<T>
): List<SimilarityResult<T>>
/**
* Check if type is supported
* @param type Type name to check
* @return true if type is supported
*/
override fun supportsType(type: String): Boolean
}The adapter performs several key functions:
Extension function for converting PropertyFilter to Spring AI expressions.
/**
* Convert PropertyFilter to Spring AI Filter.Expression
* @return Spring AI Filter.Expression equivalent
* @throws IllegalArgumentException if filter cannot be converted
*/
fun PropertyFilter.toSpringAiExpression(): Filter.ExpressionThe conversion supports all standard PropertyFilter types:
| RAG Filter | Spring AI Expression |
|---|---|
PropertyFilter.Eq | Filter.Expression.EQ |
PropertyFilter.Ne | Filter.Expression.NE |
PropertyFilter.Gt | Filter.Expression.GT |
PropertyFilter.Gte | Filter.Expression.GTE |
PropertyFilter.Lt | Filter.Expression.LT |
PropertyFilter.Lte | Filter.Expression.LTE |
PropertyFilter.In | Filter.Expression.IN |
PropertyFilter.Nin | Filter.Expression.NIN |
PropertyFilter.And | Filter.Expression.AND |
PropertyFilter.Or | Filter.Expression.OR |
PropertyFilter.Not | Filter.Expression.NOT |
Note: String-specific filters (Contains, StartsWith, EndsWith, Like) require custom handling or post-filtering as they don't have direct Spring AI equivalents.
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.ai.vectorstore.VectorStore
import org.springframework.ai.vectorstore.PgVectorStore
import org.springframework.ai.embedding.EmbeddingClient
import com.embabel.agent.rag.service.spring.SpringVectorStoreVectorSearch
import com.embabel.agent.rag.service.FilteringVectorSearch
@Configuration
class RagConfiguration {
@Bean
fun ragVectorSearch(springVectorStore: VectorStore): FilteringVectorSearch {
return SpringVectorStoreVectorSearch(springVectorStore)
}
@Bean
fun ragTools(ragVectorSearch: FilteringVectorSearch): ToolishRag {
return ToolishRag(
name = "knowledge_search",
description = "Search organizational knowledge base",
searchOperations = ragVectorSearch
)
}
@Bean
fun vectorStore(
embeddingClient: EmbeddingClient,
jdbcTemplate: JdbcTemplate
): VectorStore {
return PgVectorStore.builder()
.jdbcTemplate(jdbcTemplate)
.embeddingClient(embeddingClient)
.dimensions(1536)
.build()
}
}@Configuration
class MultiVectorStoreConfiguration {
@Bean
@Qualifier("documentStore")
fun documentVectorStore(
embeddingClient: EmbeddingClient,
jdbcTemplate: JdbcTemplate
): VectorStore {
return PgVectorStore.builder()
.jdbcTemplate(jdbcTemplate)
.embeddingClient(embeddingClient)
.dimensions(1536)
.schemaName("documents")
.build()
}
@Bean
@Qualifier("entityStore")
fun entityVectorStore(
embeddingClient: EmbeddingClient,
redisTemplate: RedisTemplate<String, String>
): VectorStore {
return RedisVectorStore(
embeddingClient,
redisTemplate,
"entities"
)
}
@Bean
@Qualifier("documentSearch")
fun documentRagSearch(
@Qualifier("documentStore") vectorStore: VectorStore
): FilteringVectorSearch {
return SpringVectorStoreVectorSearch(vectorStore)
}
@Bean
@Qualifier("entitySearch")
fun entityRagSearch(
@Qualifier("entityStore") vectorStore: VectorStore
): FilteringVectorSearch {
return SpringVectorStoreVectorSearch(vectorStore)
}
}import com.embabel.agent.rag.service.spring.*
import com.embabel.agent.rag.filter.*
import org.springframework.ai.vectorstore.VectorStore
import com.embabel.agent.rag.model.Chunk
@Service
class DocumentSearchService(
private val vectorStore: VectorStore
) {
private val ragSearch = SpringVectorStoreVectorSearch(vectorStore)
fun searchDocumentation(
query: String,
version: String?,
category: String?,
minConfidence: Double = 0.7
): List<Chunk> {
// Build filter dynamically
val filters = mutableListOf<PropertyFilter>()
version?.let {
filters.add(PropertyFilter.eq("version", it))
}
category?.let {
filters.add(PropertyFilter.eq("category", it))
}
val metadataFilter = when {
filters.isEmpty() -> null
filters.size == 1 -> filters.first()
else -> PropertyFilter.and(*filters.toTypedArray())
}
// Execute search
val results = ragSearch.vectorSearchWithFilter(
request = TextSimilaritySearchRequest(
query = query,
topK = 20,
similarityThreshold = minConfidence
),
clazz = Chunk::class.java,
metadataFilter = metadataFilter,
entityFilter = null
)
return results.map { it.content }
}
}Combine Spring AI vector search with traditional text search.
@Service
class HybridSearchService(
private val vectorStore: VectorStore,
private val textSearchRepository: TextSearchRepository
) {
private val ragSearch = SpringVectorStoreVectorSearch(vectorStore)
fun hybridSearch(
query: String,
vectorWeight: Double = 0.7,
textWeight: Double = 0.3
): List<ScoredChunk> {
require(vectorWeight + textWeight == 1.0) {
"Weights must sum to 1.0"
}
// Perform vector search
val vectorResults = ragSearch.vectorSearch(
request = TextSimilaritySearchRequest(
query = query,
topK = 50
),
clazz = Chunk::class.java
)
// Perform text search
val textResults = textSearchRepository.search(query, limit = 50)
// Combine and re-rank results
val combinedScores = mutableMapOf<String, Double>()
vectorResults.forEach { result ->
val currentScore = combinedScores[result.content.id] ?: 0.0
combinedScores[result.content.id] = currentScore + (result.score * vectorWeight)
}
textResults.forEach { result ->
val currentScore = combinedScores[result.chunk.id] ?: 0.0
combinedScores[result.chunk.id] = currentScore + (result.score * textWeight)
}
// Get all chunks and sort by combined score
val allChunkIds = (vectorResults.map { it.content.id } +
textResults.map { it.chunk.id }).distinct()
return allChunkIds
.mapNotNull { id ->
val chunk = findChunkById(id)
val score = combinedScores[id] ?: 0.0
chunk?.let { ScoredChunk(it, score) }
}
.sortedByDescending { it.score }
.take(20)
}
data class ScoredChunk(val chunk: Chunk, val score: Double)
}import org.springframework.ai.vectorstore.PgVectorStore
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class PgVectorConfiguration {
@Bean
fun pgVectorStore(
embeddingClient: EmbeddingClient,
jdbcTemplate: JdbcTemplate
): VectorStore {
return PgVectorStore.builder()
.jdbcTemplate(jdbcTemplate)
.embeddingClient(embeddingClient)
.dimensions(1536)
.distanceType(PgVectorStore.PgDistanceType.COSINE_DISTANCE)
.removeExistingVectorStoreTable(false)
.build()
}
@Bean
fun ragSearch(pgVectorStore: VectorStore): FilteringVectorSearch {
return SpringVectorStoreVectorSearch(pgVectorStore)
}
}
// Usage with connection pooling
@Configuration
@EnableConfigurationProperties(DataSourceProperties::class)
class DataSourceConfiguration {
@Bean
fun dataSource(properties: DataSourceProperties): DataSource {
return HikariDataSource().apply {
jdbcUrl = properties.url
username = properties.username
password = properties.password
maximumPoolSize = 20
minimumIdle = 5
connectionTimeout = 30000
}
}
}import org.springframework.ai.vectorstore.RedisVectorStore
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class RedisVectorConfiguration {
@Bean
fun redisVectorStore(
embeddingClient: EmbeddingClient,
redisProperties: RedisProperties
): VectorStore {
val config = RedisVectorStore.RedisVectorStoreConfig.builder()
.withIndexName("rag-embeddings")
.withPrefix("doc:")
.build()
return RedisVectorStore(config, embeddingClient)
}
@Bean
fun ragSearch(redisVectorStore: VectorStore): FilteringVectorSearch {
return SpringVectorStoreVectorSearch(redisVectorStore)
}
}import org.springframework.ai.vectorstore.PineconeVectorStore
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class PineconeConfiguration {
@Bean
fun pineconeVectorStore(
embeddingClient: EmbeddingClient,
@Value("\${pinecone.api-key}") apiKey: String,
@Value("\${pinecone.environment}") environment: String,
@Value("\${pinecone.index-name}") indexName: String
): VectorStore {
return PineconeVectorStore.builder()
.apiKey(apiKey)
.environment(environment)
.indexName(indexName)
.embeddingClient(embeddingClient)
.build()
}
@Bean
fun ragSearch(pineconeVectorStore: VectorStore): FilteringVectorSearch {
return SpringVectorStoreVectorSearch(pineconeVectorStore)
}
}import org.springframework.ai.vectorstore.ChromaVectorStore
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class ChromaConfiguration {
@Bean
fun chromaVectorStore(
embeddingClient: EmbeddingClient,
@Value("\${chroma.url}") chromaUrl: String
): VectorStore {
return ChromaVectorStore.builder()
.embeddingClient(embeddingClient)
.collectionName("rag-documents")
.chromaUrl(chromaUrl)
.build()
}
@Bean
fun ragSearch(chromaVectorStore: VectorStore): FilteringVectorSearch {
return SpringVectorStoreVectorSearch(chromaVectorStore)
}
}import com.embabel.agent.rag.filter.*
import com.embabel.agent.rag.service.spring.*
// Complex nested filters
val complexFilter = PropertyFilter.and(
PropertyFilter.or(
PropertyFilter.eq("type", "documentation"),
PropertyFilter.eq("type", "tutorial"),
PropertyFilter.eq("type", "guide")
),
PropertyFilter.gte("lastUpdated", System.currentTimeMillis() - 86400000),
PropertyFilter.not(
PropertyFilter.or(
PropertyFilter.eq("status", "archived"),
PropertyFilter.eq("status", "deleted")
)
),
PropertyFilter.`in`("language", "en", "es", "fr")
)
// Convert to Spring AI expression
val springExpression = complexFilter.toSpringAiExpression()
// Use with Spring AI VectorStore
val request = SearchRequest.query("search text")
.withFilterExpression(springExpression)
.withTopK(10)
val documents = springVectorStore.similaritySearch(request)Handle filters not directly supported by Spring AI.
import com.embabel.agent.rag.filter.*
import org.springframework.ai.vectorstore.filter.Filter
class EnhancedFilterConverter {
fun convertToSpringExpression(filter: PropertyFilter): Filter.Expression {
return when (filter) {
// Handle string operations with workarounds
is PropertyFilter.Contains -> {
// Use regex or post-filtering
throw UnsupportedOperationException(
"Contains filter requires post-filtering"
)
}
is PropertyFilter.StartsWith -> {
// Convert to regex pattern
val pattern = "^${Regex.escape(filter.value)}.*"
Filter.Expression("${filter.key} ~= '${pattern}'")
}
is PropertyFilter.EndsWith -> {
// Convert to regex pattern
val pattern = ".*${Regex.escape(filter.value)}$"
Filter.Expression("${filter.key} ~= '${pattern}'")
}
// Delegate to standard conversion for other types
else -> filter.toSpringAiExpression()
}
}
}import com.embabel.agent.rag.filter.*
import com.embabel.agent.rag.service.spring.*
class PostFilteringVectorSearch(
private val delegate: SpringVectorStoreVectorSearch
) : FilteringVectorSearch by delegate {
override fun <T : Retrievable> vectorSearchWithFilter(
request: TextSimilaritySearchRequest,
clazz: Class<T>,
metadataFilter: PropertyFilter?,
entityFilter: EntityFilter?
): List<SimilarityResult<T>> {
// Split filters into supported and unsupported
val (supported, unsupported) = splitFilters(metadataFilter)
// Perform search with supported filters
val results = delegate.vectorSearchWithFilter(
request = request.copy(topK = request.topK * 2), // Fetch more for post-filtering
clazz = clazz,
metadataFilter = supported,
entityFilter = entityFilter
)
// Apply unsupported filters in-memory
return if (unsupported != null) {
results.filter { result ->
InMemoryPropertyFilter.matchesMetadata(unsupported, result.content.metadata)
}.take(request.topK)
} else {
results.take(request.topK)
}
}
private fun splitFilters(
filter: PropertyFilter?
): Pair<PropertyFilter?, PropertyFilter?> {
if (filter == null) return null to null
return when (filter) {
is PropertyFilter.Contains,
is PropertyFilter.ContainsIgnoreCase -> null to filter
is PropertyFilter.And -> {
val (supportedList, unsupportedList) = filter.filters
.map { splitFilters(it) }
.unzip()
val supported = supportedList.filterNotNull().takeIf { it.isNotEmpty() }
?.let { PropertyFilter.and(*it.toTypedArray()) }
val unsupported = unsupportedList.filterNotNull().takeIf { it.isNotEmpty() }
?.let { PropertyFilter.and(*it.toTypedArray()) }
supported to unsupported
}
else -> filter to null
}
}
}Add caching to reduce vector store queries.
import org.springframework.cache.annotation.Cacheable
import org.springframework.cache.annotation.CacheConfig
import org.springframework.stereotype.Service
@Service
@CacheConfig(cacheNames = ["vectorSearch"])
class CachedVectorSearchService(
private val ragSearch: FilteringVectorSearch
) {
@Cacheable(key = "#request.query + '-' + #clazz.simpleName")
fun search(
request: TextSimilaritySearchRequest,
clazz: Class<out Retrievable>
): List<SimilarityResult<out Retrievable>> {
return ragSearch.vectorSearch(request, clazz)
}
@Cacheable(
key = "#request.query + '-' + #clazz.simpleName + '-' + " +
"#metadataFilter?.hashCode() + '-' + #entityFilter?.hashCode()"
)
fun searchWithFilter(
request: TextSimilaritySearchRequest,
clazz: Class<out Retrievable>,
metadataFilter: PropertyFilter?,
entityFilter: EntityFilter?
): List<SimilarityResult<out Retrievable>> {
return ragSearch.vectorSearchWithFilter(
request, clazz, metadataFilter, entityFilter
)
}
}Optimize database connections for vector stores.
@Configuration
class VectorStoreDataSourceConfiguration {
@Bean
@ConfigurationProperties("spring.datasource.vectorstore")
fun vectorStoreDataSourceProperties(): DataSourceProperties {
return DataSourceProperties()
}
@Bean
fun vectorStoreDataSource(
properties: DataSourceProperties
): DataSource {
return HikariDataSource().apply {
jdbcUrl = properties.url
username = properties.username
password = properties.password
// Optimized for vector operations
maximumPoolSize = 50
minimumIdle = 10
connectionTimeout = 30000
idleTimeout = 600000
maxLifetime = 1800000
// Vector-specific optimizations
addDataSourceProperty("prepStmtCacheSize", "250")
addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
addDataSourceProperty("cachePrepStmts", "true")
addDataSourceProperty("useServerPrepStmts", "true")
}
}
}Efficiently process multiple queries.
@Service
class BatchVectorSearchService(
private val ragSearch: FilteringVectorSearch
) {
fun batchSearch(
queries: List<String>,
clazz: Class<out Retrievable>,
topKPerQuery: Int = 10
): Map<String, List<SimilarityResult<out Retrievable>>> {
return queries.parallelStream()
.map { query ->
val request = TextSimilaritySearchRequest(
query = query,
topK = topKPerQuery
)
query to ragSearch.vectorSearch(request, clazz)
}
.collect(Collectors.toMap({ it.first }, { it.second }))
}
fun batchSearchAsync(
queries: List<String>,
clazz: Class<out Retrievable>,
topKPerQuery: Int = 10
): CompletableFuture<Map<String, List<SimilarityResult<out Retrievable>>>> {
val futures = queries.map { query ->
CompletableFuture.supplyAsync {
val request = TextSimilaritySearchRequest(
query = query,
topK = topKPerQuery
)
query to ragSearch.vectorSearch(request, clazz)
}
}
return CompletableFuture.allOf(*futures.toTypedArray())
.thenApply {
futures.map { it.get() }.toMap()
}
}
}import org.springframework.retry.annotation.Backoff
import org.springframework.retry.annotation.Retryable
import org.springframework.stereotype.Service
@Service
class ResilientVectorSearchService(
private val ragSearch: FilteringVectorSearch
) {
@Retryable(
value = [Exception::class],
maxAttempts = 3,
backoff = Backoff(delay = 1000, multiplier = 2.0)
)
fun searchWithRetry(
request: TextSimilaritySearchRequest,
clazz: Class<out Retrievable>
): List<SimilarityResult<out Retrievable>> {
return ragSearch.vectorSearch(request, clazz)
}
}import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
import org.springframework.stereotype.Service
@Service
class CircuitBreakerVectorSearchService(
private val ragSearch: FilteringVectorSearch
) {
@CircuitBreaker(
name = "vectorSearch",
fallbackMethod = "searchFallback"
)
fun searchWithCircuitBreaker(
request: TextSimilaritySearchRequest,
clazz: Class<out Retrievable>
): List<SimilarityResult<out Retrievable>> {
return ragSearch.vectorSearch(request, clazz)
}
private fun searchFallback(
request: TextSimilaritySearchRequest,
clazz: Class<out Retrievable>,
exception: Exception
): List<SimilarityResult<out Retrievable>> {
// Return cached results or empty list
logger.warn("Circuit breaker activated, returning empty results", exception)
return emptyList()
}
}import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Timer
import org.springframework.stereotype.Service
@Service
class MeteredVectorSearchService(
private val ragSearch: FilteringVectorSearch,
private val meterRegistry: MeterRegistry
) {
fun search(
request: TextSimilaritySearchRequest,
clazz: Class<out Retrievable>
): List<SimilarityResult<out Retrievable>> {
val timer = Timer.builder("vector.search")
.tag("type", clazz.simpleName)
.register(meterRegistry)
return timer.recordCallable {
val results = ragSearch.vectorSearch(request, clazz)
// Record result count
meterRegistry.counter(
"vector.search.results",
"type", clazz.simpleName
).increment(results.size.toDouble())
results
}!!
}
}