CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

tessl/maven-com-embabel-agent--embabel-agent-rag-core

RAG (Retrieval-Augmented Generation) framework for the Embabel Agent platform providing content ingestion, chunking, hierarchical navigation, and semantic search capabilities

Overview
Eval results
Files

spring-ai-integration.mddocs/advanced/

Advanced Spring AI Integration

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.

Overview

The framework provides seamless integration with Spring AI through:

  • VectorStore adapter for unified search interface
  • PropertyFilter to Spring AI Expression conversion
  • Automatic type mapping and result transformation
  • Spring Boot configuration support

Core Architecture

SpringVectorStoreVectorSearch

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:

  1. Converts RAG TextSimilaritySearchRequest to Spring AI SearchRequest
  2. Translates PropertyFilter expressions to Spring AI Filter.Expression
  3. Maps Spring AI Document results back to RAG Retrievable objects
  4. Handles type compatibility between frameworks

Filter Expression Conversion

PropertyFilter Extension

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.Expression

Supported Conversions

The conversion supports all standard PropertyFilter types:

RAG FilterSpring AI Expression
PropertyFilter.EqFilter.Expression.EQ
PropertyFilter.NeFilter.Expression.NE
PropertyFilter.GtFilter.Expression.GT
PropertyFilter.GteFilter.Expression.GTE
PropertyFilter.LtFilter.Expression.LT
PropertyFilter.LteFilter.Expression.LTE
PropertyFilter.InFilter.Expression.IN
PropertyFilter.NinFilter.Expression.NIN
PropertyFilter.AndFilter.Expression.AND
PropertyFilter.OrFilter.Expression.OR
PropertyFilter.NotFilter.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.


Spring Boot Configuration

Basic Configuration

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

Advanced Configuration with Multiple Vector Stores

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

Advanced Search Patterns

Vector Search with Complex Filters

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

Hybrid Search: Vector + Text

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

VectorStore Implementation Support

PostgreSQL (PgVector)

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

Redis

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

Pinecone

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

Chroma

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

Advanced Filter Conversion Patterns

Complex Boolean Logic

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)

Custom Filter Converter

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

Post-Filtering for Unsupported Operations

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

Performance Optimization

Caching Layer

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

Connection Pooling

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

Batch Operations

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

Error Handling and Resilience

Retry Logic

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

Circuit Breaker

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

Monitoring and Metrics

Search Metrics

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

See Also

  • Vector Search - Basic search patterns
  • Filtering - Filter DSL reference
  • Architecture - System design overview
tessl i tessl/maven-com-embabel-agent--embabel-agent-rag-core@0.3.1

docs

advanced

architecture.md

content-refresh-policies.md

custom-transformers.md

spring-ai-integration.md

index.md

README.md

tile.json