fix(search): review fixes for path AND-term and content wildcard

bleve compiled a path restriction to a DisjunctionQuery, which mapBinary redistributes as an OR-chain, so `path:/Foo AND name:bar` matched the folder itself unconditionally. It is now a BooleanQuery (should: folder OR descendants), which mapBinary keeps atomic under an enclosing AND.

The OpenSearch full-text branch ran before the wildcard check, so `content:foo*` degraded to a phrase match and diverged from bleve; the wildcard check now comes first.

Adds the missing coverage the review flagged: path AND term, content wildcard, case-insensitive tags (the array sibling branch), and a spaced path with descendants on OpenSearch.
This commit is contained in:
Dominik Schmidt committed 2026-08-18 17:12:32 +02:00
1 parent f7087c973b
commit d352ee53c5
4 files changed
+41 -15

No files matched your search

+19
View File
@@ -125,6 +125,17 @@ var _ = Describe("Bleve", func() {
assertDocCount(rootResource.ID, "Tags:baz", 0)
})
It("finds files by tags case-insensitively", func() {
// exercises the []string/[]any sibling-lowercasing branch end-to-end.
parentResource.Document.Tags = []string{"Work", "Urgent"}
Expect(eng.Upsert(parentResource.ID, parentResource)).To(Succeed())
assertDocCount(rootResource.ID, "tag:work", 1) // stored "Work", queried lower
assertDocCount(rootResource.ID, "tag:WORK", 1) // queried upper
assertDocCount(rootResource.ID, "Tags:Urgent", 1)
assertDocCount(rootResource.ID, "tag:missing", 0)
})
It("finds files by size", func() {
parentResource.Document.Size = 12345
err := eng.Upsert(parentResource.ID, parentResource)
@@ -286,6 +297,13 @@ var _ = Describe("Bleve", func() {
It("matches case-insensitively", func() {
assertDocCount(rootResource.ID, `path:"./PARENT D!R"`, 3)
})
It("applies an AND filter to the folder itself, not only descendants", func() {
// regression: the folder-itself clause used to match unconditionally
// under an AND, so the parent leaked in despite the name filter.
matches := assertDocCount(rootResource.ID, `path:"./parent d!r" AND name:child.pdf`, 1)
Expect(matches[0].Entity.Name).To(Equal("child.pdf"))
})
})
Context("by content", func() {
@@ -296,6 +314,7 @@ var _ = Describe("Bleve", func() {
assertDocCount(rootResource.ID, "content:running", 1)
assertDocCount(rootResource.ID, "content:RUNNING", 1) // case-insensitive
assertDocCount(rootResource.ID, "content:run", 1) // porter stemming
assertDocCount(rootResource.ID, "content:run*", 1) // wildcard over the stemmed term
assertDocCount(rootResource.ID, "content:cat", 0)
})
})
@@ -107,15 +107,15 @@ func (t kqlOpensearchTranspiler) toBuilder(node ast.Node) (osu.Builder, error) {
value = strings.ToLower(value)
}
if query.FieldIsFulltext(node.Key) {
return osu.NewMatchPhraseQuery(field).Query(value), nil
}
isWildcard := strings.Contains(value, "*")
if isWildcard {
return osu.NewWildcardQuery(field).Value(value), nil
}
if query.FieldIsFulltext(node.Key) {
return osu.NewMatchPhraseQuery(field).Query(value), nil
}
totalTerms := strings.Split(value, " ")
isSingleTerm := len(totalTerms) == 1
isMultiTerm := len(totalTerms) >= 1
+8 -7
View File
@@ -85,13 +85,14 @@ func walk(offset int, nodes []ast.Node) (bleveQuery.Query, int, error) {
var q bleveQuery.Query = bleveQuery.NewQueryStringQuery(k + ":" + v)
if searchQuery.FieldIsPath(n.Key) {
// bleve has no path hierarchy analyzer, unlike OpenSearch: match
// the folder itself and its descendants (`\/*`, a trailing
// wildcard on the value).
q = bleveQuery.NewDisjunctionQuery([]bleveQuery.Query{
q,
bleveQuery.NewQueryStringQuery(k + ":" + v + `\/*`),
})
// bleve has no path hierarchy analyzer, unlike OpenSearch: match the
// folder itself and its descendants (`\/*`). A BooleanQuery keeps
// this atomic; a DisjunctionQuery would be redistributed by an
// enclosing AND (mapBinary treats a left disjunction as an OR-chain).
bq := bleve.NewBooleanQuery()
bq.AddShould(q, bleveQuery.NewQueryStringQuery(k+":"+v+`\/*`))
bq.SetMinShould(1)
q = bq
}
if prev == nil {
@@ -52,10 +52,16 @@ func Test_compile(t *testing.T) {
&ast.StringNode{Key: "path", Value: "/Foo"},
},
},
want: query.NewDisjunctionQuery([]query.Query{
query.NewQueryStringQuery(`Path_lowercase:\/foo`),
query.NewQueryStringQuery(`Path_lowercase:\/foo\/*`),
}),
// a BooleanQuery (should: exact OR descendants), not a DisjunctionQuery,
// so an enclosing AND does not redistribute the folder-itself clause.
want: func() query.Query {
bq := query.NewBooleanQuery(nil, []query.Query{
query.NewQueryStringQuery(`Path_lowercase:\/foo`),
query.NewQueryStringQuery(`Path_lowercase:\/foo\/*`),
}, nil)
bq.SetMinShould(1)
return query.NewConjunctionQuery([]query.Query{bq})
}(),
wantErr: false,
},
{