From 17144d47912c2b4e5b48dd44adf76a8541b5b41d Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Mon, 10 Aug 2026 21:16:35 +0200 Subject: [PATCH] fix(search): keep OpenSearch path queries as unanalyzed term queries A path value with spaces went through a match_phrase query, which analyzes the query with the path_hierarchy analyzer; the resulting "." prefix token matches every document in the space, breaking descendant matching and the stale-path check after a move. --- .../internal/convert/kql_transpile.go | 7 +++ .../internal/convert/kql_transpile_test.go | 52 +++---------------- 2 files changed, 15 insertions(+), 44 deletions(-) diff --git a/services/search/pkg/opensearch/internal/convert/kql_transpile.go b/services/search/pkg/opensearch/internal/convert/kql_transpile.go index 7938299bab..368f1ad9be 100644 --- a/services/search/pkg/opensearch/internal/convert/kql_transpile.go +++ b/services/search/pkg/opensearch/internal/convert/kql_transpile.go @@ -115,6 +115,13 @@ func (t kqlOpensearchTranspiler) toBuilder(node ast.Node) (osu.Builder, error) { return osu.NewMatchPhraseQuery(field).Query(value), nil } + // a path value is a single term in the path_hierarchy token stream; a + // phrase match would analyze the query into its path prefixes and match + // everything under the root, so paths with spaces must stay term queries. + if query.FieldIsPath(node.Key) { + return osu.NewTermQuery[string](field).Value(value), nil + } + totalTerms := strings.Split(value, " ") isSingleTerm := len(totalTerms) == 1 isMultiTerm := len(totalTerms) >= 1 diff --git a/services/search/pkg/opensearch/internal/convert/kql_transpile_test.go b/services/search/pkg/opensearch/internal/convert/kql_transpile_test.go index d7255b5b8e..8cfbf54472 100644 --- a/services/search/pkg/opensearch/internal/convert/kql_transpile_test.go +++ b/services/search/pkg/opensearch/internal/convert/kql_transpile_test.go @@ -97,60 +97,24 @@ func TestTranspileKQLToOpenSearch(t *testing.T) { Want: osu.NewWildcardQuery("Content").Value("open*"), }, { - Name: "wildcard query - a question mark counts as a wildcard", + // a phrase match would analyze the query with path_hierarchy and match + // everything under the root + Name: "path with spaces stays an unanalyzed term query", Got: &ast.Ast{ Nodes: []ast.Node{ - &ast.StringNode{Key: "Name", Value: "fo?o"}, + &ast.StringNode{Key: "Path", Value: "./parent d!r/child.pdf"}, }, }, - Want: osu.NewBoolQuery(). - Params(&osu.BoolQueryParams{MinimumShouldMatch: 1}). - Should( - osu.NewWildcardQuery("Name.wildcard"). - Value("fo?o"). - Params(&osu.WildcardQueryParams{CaseInsensitive: true}), - osu.NewWildcardQuery("Name.wildcard"). - Value("fo?o.*"). - Params(&osu.WildcardQueryParams{CaseInsensitive: true}), - ), + Want: osu.NewTermQuery[string]("Path").Value("./parent d!r/child.pdf"), }, { - Name: "term query - an equals restriction matches the whole name", + Name: "case-insensitive path with spaces routes to the lowercased sibling as a term query", Got: &ast.Ast{ Nodes: []ast.Node{ - &ast.StringNode{Key: "Name", Value: "foo bar.txt", Exact: true}, + &ast.StringNode{Key: "Path", Value: "./Parent Dir", CaseInsensitive: true}, }, }, - Want: osu.NewTermQuery[string]("Name.wildcard"). - Value("foo bar.txt"). - Params(&osu.TermQueryParams{CaseInsensitive: true}), - }, - { - Name: "term query - a path loses its trailing slash", - Got: &ast.Ast{ - Nodes: []ast.Node{ - &ast.StringNode{Key: "Path", Value: "./Documents/"}, - }, - }, - Want: osu.NewTermQuery[string]("Path").Value("./Documents"), - }, - { - Name: "term query - a hidden string turns into a bool", - Got: &ast.Ast{ - Nodes: []ast.Node{ - &ast.StringNode{Key: "Hidden", Value: "true"}, - }, - }, - Want: osu.NewTermQuery[bool]("Hidden").Value(true), - }, - { - Name: "match-none query - a hidden string that is no bool", - Got: &ast.Ast{ - Nodes: []ast.Node{ - &ast.StringNode{Key: "Hidden", Value: "banana"}, - }, - }, - Want: osu.NewMatchNoneQuery(), + Want: osu.NewTermQuery[string]("Path_lowercase").Value("./parent dir"), }, { Name: "bool query",