Skip to content

Commit c66675b

Browse files
committed
Share search property filter dispatch
Signed-off-by: Arnab Nandy <arnab_nandy7@yahoo.com>
1 parent 6834cbd commit c66675b

2 files changed

Lines changed: 121 additions & 36 deletions

File tree

embabel-agent-rag/embabel-agent-rag-core/src/main/kotlin/com/embabel/agent/rag/tools/CoreSearchTools.kt

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,15 @@ internal fun List<SimilarityResult<out Retrievable>>.withNeighbours(
7979
}
8080

8181
@Suppress("UNCHECKED_CAST")
82-
internal fun <T : Retrievable> VectorSearch.searchWithFilter(
82+
internal fun <T : Retrievable> VectorSearch.vectorSearchWithFilter(
8383
request: TextSimilaritySearchRequest,
8484
clazz: Class<T>,
8585
metadataFilter: PropertyFilter?,
8686
entityFilter: EntityFilter?,
87-
): List<SimilarityResult<T>> {
88-
if (metadataFilter == null && entityFilter == null) {
89-
return vectorSearch(request, clazz)
90-
}
91-
if (this is FilteringVectorSearch) {
92-
return vectorSearchWithFilter(request, clazz, metadataFilter, entityFilter)
93-
}
94-
return PostFilteringSearch.search(
87+
): List<SimilarityResult<T>> = when {
88+
metadataFilter == null && entityFilter == null -> vectorSearch(request, clazz)
89+
this is FilteringVectorSearch -> vectorSearchWithFilter(request, clazz, metadataFilter, entityFilter)
90+
else -> PostFilteringSearch.search(
9591
request,
9692
metadataFilter,
9793
entityFilter,
@@ -147,7 +143,7 @@ internal class VectorSearchTools @JvmOverloads constructor(
147143

148144
private fun searchForAllTypes(request: TextSimilaritySearchRequest): List<SimilarityResult<out Retrievable>> {
149145
val allResults = searchFor.flatMap { clazz ->
150-
vectorSearch.searchWithFilter(request, clazz, metadataFilter, entityFilter)
146+
vectorSearch.vectorSearchWithFilter(request, clazz, metadataFilter, entityFilter)
151147
}
152148
return deduplicateByIdKeepingHighestScore(allResults)
153149
}
@@ -380,6 +376,25 @@ internal class SectionReadingTools @JvmOverloads constructor(
380376
}
381377
}
382378

379+
@Suppress("UNCHECKED_CAST")
380+
internal fun <T : Retrievable> TextSearch.textSearchWithFilter(
381+
request: TextSimilaritySearchRequest,
382+
clazz: Class<T>,
383+
metadataFilter: PropertyFilter?,
384+
entityFilter: EntityFilter?,
385+
): List<SimilarityResult<T>> = when {
386+
metadataFilter == null && entityFilter == null -> textSearch(request, clazz)
387+
this is FilteringTextSearch -> textSearchWithFilter(request, clazz, metadataFilter, entityFilter)
388+
else -> PostFilteringSearch.search(
389+
request,
390+
metadataFilter,
391+
entityFilter,
392+
TopKInflationStrategy.DEFAULT,
393+
) { inflatedRequest ->
394+
textSearch(inflatedRequest, clazz)
395+
} as List<SimilarityResult<T>>
396+
}
397+
383398
/**
384399
* Tools to perform text search operations with the syntax supported by
385400
* the backing [TextSearch] store.
@@ -480,36 +495,11 @@ internal class TextSearchTools @JvmOverloads constructor(
480495

481496
private fun searchForAllTypes(request: TextSimilaritySearchRequest): List<SimilarityResult<out Retrievable>> {
482497
val allResults = searchFor.flatMap { clazz ->
483-
searchWithFilter(request, clazz)
498+
textSearch.textSearchWithFilter(request, clazz, metadataFilter, entityFilter)
484499
}
485500
return deduplicateByIdKeepingHighestScore(allResults)
486501
}
487502

488-
@Suppress("UNCHECKED_CAST")
489-
private fun <T : Retrievable> searchWithFilter(
490-
request: TextSimilaritySearchRequest,
491-
clazz: Class<T>,
492-
): List<SimilarityResult<T>> {
493-
if (metadataFilter == null && entityFilter == null) {
494-
return textSearch.textSearch(request, clazz)
495-
}
496-
497-
// If backend supports native filtering, use it
498-
if (textSearch is FilteringTextSearch) {
499-
return textSearch.textSearchWithFilter(request, clazz, metadataFilter, entityFilter)
500-
}
501-
502-
// Fallback: inflate topK, search, post-filter, take topK
503-
return PostFilteringSearch.search(
504-
request,
505-
metadataFilter,
506-
entityFilter,
507-
TopKInflationStrategy.DEFAULT
508-
) { inflatedRequest ->
509-
textSearch.textSearch(inflatedRequest, clazz)
510-
} as List<SimilarityResult<T>>
511-
}
512-
513503
companion object {
514504
/**
515505
* Compose the tool's top-level description from the store's

embabel-agent-rag/embabel-agent-rag-core/src/test/kotlin/com/embabel/agent/rag/tools/ToolishRagTest.kt

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.embabel.agent.rag.tools
1717

1818
import com.embabel.agent.api.tool.Tool
19+
import com.embabel.agent.filter.PropertyFilter
1920
import com.embabel.agent.rag.model.Chunk
2021
import com.embabel.agent.rag.model.ContentElement
2122
import com.embabel.agent.rag.model.NamedEntityData.Companion.ENTITY_LABEL
@@ -462,6 +463,28 @@ class ToolishRagTest {
462463
}
463464
}
464465

466+
@Nested
467+
inner class SearchWithFilterExtensionsTests {
468+
469+
@Test
470+
fun `filter helpers should be callable on CoreSearchOperations`() {
471+
val searchOperations = mockk<CoreSearchOperations>()
472+
val request = TextSimilaritySearchRequest("test query", 0.5, 5)
473+
every {
474+
searchOperations.vectorSearch(request, Chunk::class.java)
475+
} returns emptyList()
476+
every {
477+
searchOperations.textSearch(request, Chunk::class.java)
478+
} returns emptyList()
479+
480+
val vectorResults = searchOperations.vectorSearchWithFilter(request, Chunk::class.java, null, null)
481+
val textResults = searchOperations.textSearchWithFilter(request, Chunk::class.java, null, null)
482+
483+
assertTrue(vectorResults.isEmpty())
484+
assertTrue(textResults.isEmpty())
485+
}
486+
}
487+
465488
@Nested
466489
inner class TextSearchToolsTests {
467490

@@ -506,6 +529,78 @@ class ToolishRagTest {
506529
assertEquals("0 results:", result)
507530
}
508531

532+
@Test
533+
fun `textSearch should use native filtering when supported`() {
534+
val textSearch = mockk<FilteringTextSearch>()
535+
val metadataFilter = PropertyFilter.eq("ownerId", "alice")
536+
val chunk = createChunk("chunk1", "Alice's content")
537+
every {
538+
textSearch.textSearchWithFilter(
539+
any<TextSimilaritySearchRequest>(),
540+
Chunk::class.java,
541+
metadataFilter,
542+
null,
543+
)
544+
} returns listOf(SimpleSimilaritySearchResult(match = chunk, score = 0.9))
545+
val tools = TextSearchTools(textSearch, metadataFilter = metadataFilter)
546+
547+
val result = tools.textSearch("test query", 5, 0.5)
548+
549+
verify(exactly = 1) {
550+
textSearch.textSearchWithFilter(
551+
match<TextSimilaritySearchRequest> { it.topK == 5 },
552+
Chunk::class.java,
553+
metadataFilter,
554+
null,
555+
)
556+
}
557+
assertTrue(result.contains("Alice's content"))
558+
}
559+
560+
@Test
561+
fun `textSearch should inflate topK and post-filter when native filtering is unavailable`() {
562+
val textSearch = mockk<TextSearch>()
563+
val metadataFilter = PropertyFilter.eq("ownerId", "alice")
564+
val included = Chunk(
565+
id = "included",
566+
text = "Alice's content",
567+
parentId = "parent",
568+
metadata = mapOf("ownerId" to "alice"),
569+
)
570+
val excluded = Chunk(
571+
id = "excluded",
572+
text = "Bob's content",
573+
parentId = "parent",
574+
metadata = mapOf("ownerId" to "bob"),
575+
)
576+
val truncated = Chunk(
577+
id = "truncated",
578+
text = "Alice's lower-ranked content",
579+
parentId = "parent",
580+
metadata = mapOf("ownerId" to "alice"),
581+
)
582+
every {
583+
textSearch.textSearch(any<TextSimilaritySearchRequest>(), Chunk::class.java)
584+
} returns listOf(
585+
SimpleSimilaritySearchResult(match = excluded, score = 0.9),
586+
SimpleSimilaritySearchResult(match = included, score = 0.8),
587+
SimpleSimilaritySearchResult(match = truncated, score = 0.7),
588+
)
589+
val tools = TextSearchTools(textSearch, metadataFilter = metadataFilter)
590+
591+
val result = tools.textSearch("test query", 1, 0.5)
592+
593+
verify(exactly = 1) {
594+
textSearch.textSearch(
595+
match<TextSimilaritySearchRequest> { it.topK == 3 },
596+
Chunk::class.java,
597+
)
598+
}
599+
assertTrue(result.contains("Alice's content"))
600+
assertFalse(result.contains("Bob's content"))
601+
assertFalse(result.contains("Alice's lower-ranked content"))
602+
}
603+
509604
}
510605

511606
@Nested

0 commit comments

Comments
 (0)