Skip to content

Commit e4fd843

Browse files
authored
Merge pull request #548 from escoffier-labs/fix/evidence-project-filter-search-index
fix(evidence): bound project filters to FTS candidates
2 parents 4aa1862 + dcb7be5 commit e4fd843

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

engines/evidence-ledger/internal/app/app.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1824,6 +1824,8 @@ func buildSearchQuery(opts SearchOpts) (string, []any) {
18241824
where, params := appendSearchResultFilters(opts, []string{"1=1"}, params)
18251825
params = append(params, limit)
18261826

1827+
// CROSS JOIN pins the candidate pool as the outer loop. A reorder through
1828+
// sources and items evaluates correlated filters against the whole archive.
18271829
sqlText := `with fts_candidates as materialized (
18281830
select item_id, snippet(item_fts, 5, '[', ']', '...', 20) as snippet, bm25(item_fts) as fts_score
18291831
from item_fts
@@ -1833,7 +1835,7 @@ func buildSearchQuery(opts SearchOpts) (string, []any) {
18331835
)
18341836
select i.id, s.kind, c.name, c.kind, i.kind, coalesce(a.type,''), coalesce(a.name,''), coalesce(i.created_at,''), fc.snippet, printf('%.6f', fc.fts_score), i.content_hash
18351837
from fts_candidates fc
1836-
join items i on i.id = fc.item_id
1838+
cross join items i on i.id = fc.item_id
18371839
join sources s on s.id = i.source_id
18381840
join collections c on c.id = i.collection_id
18391841
left join actors a on a.id = i.actor_id

engines/evidence-ledger/internal/app/app_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1855,6 +1855,11 @@ values(?,?,?,?,?,?,?,?,?,?)`, id, "source-1", "collection-1", "actor-1", id, "me
18551855
}
18561856
}
18571857
}
1858+
if _, err := db.Exec(`insert into item_metadata(item_id, key, value) values
1859+
('item-000','project','escoffier-labs/brigade'),
1860+
('item-001','cwd','/workspace/brigade')`); err != nil {
1861+
t.Fatal(err)
1862+
}
18581863
}
18591864

18601865
func explainPlan(t *testing.T, db *sql.DB, sqlText string, args ...any) string {
@@ -2018,6 +2023,38 @@ func TestSearchPlanBoundsFTSCandidatesBeforeJoins(t *testing.T) {
20182023
}
20192024
}
20202025

2026+
projectSQL, projectParams := buildSearchQuery(SearchOpts{Query: "needle", Project: "workspace", Limit: 5})
2027+
projectPlan := explainPlan(t, db, projectSQL, projectParams...)
2028+
for _, want := range []string{
2029+
"SCAN fc",
2030+
"SEARCH i USING",
2031+
"SEARCH im USING COVERING INDEX",
2032+
"(item_id=?",
2033+
} {
2034+
if !strings.Contains(projectPlan, want) {
2035+
t.Fatalf("project filter plan missing %q:\n%s", want, projectPlan)
2036+
}
2037+
}
2038+
if strings.Index(projectPlan, "SCAN fc") > strings.Index(projectPlan, "SEARCH i USING") {
2039+
t.Fatalf("project filter plan does not keep FTS candidates outermost:\n%s", projectPlan)
2040+
}
2041+
2042+
exactProject, err := search(db, SearchOpts{Query: "needle", Project: "escoffier-labs/brigade", Limit: 5})
2043+
if err != nil {
2044+
t.Fatalf("exact project search: %v", err)
2045+
}
2046+
if len(exactProject) != 1 || exactProject[0].ID != "item-000" {
2047+
t.Fatalf("exact project results = %#v, want item-000", exactProject)
2048+
}
2049+
2050+
substringProject, err := search(db, SearchOpts{Query: "needle", Project: "workspace/brigade", Limit: 5})
2051+
if err != nil {
2052+
t.Fatalf("substring project search: %v", err)
2053+
}
2054+
if len(substringProject) != 1 || substringProject[0].ID != "item-001" {
2055+
t.Fatalf("substring project results = %#v, want item-001", substringProject)
2056+
}
2057+
20212058
results, err := search(db, opts)
20222059
if err != nil {
20232060
t.Fatalf("search: %v", err)

0 commit comments

Comments
 (0)