From 538c82787c2d48d2d9b1b5996b2c3f7c85db32a5 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Mon, 20 Apr 2026 16:34:29 +0200 Subject: [PATCH] fix(search): preserve value case for non-lowercased bleve fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bleve compiler lowercased every query value (except Hidden) before handing it to the engine. This matched the index tokens for fields whose analyzer folds case — Name, Tags, Favorites, Content — but silently broke matching for every other field, whose default keyword analyzer preserves case. A query like Title:"Some Title" parsed fine, lowercased to "some title", and missed the indexed token "Some Title". Replace the blanket lowercasing with an allowlist of the four fields whose index mapping actually uses a lowercasing analyzer. Every other field now passes through unchanged, which keeps values like "deadmau5" or "Motörhead" intact instead of normalising them to a case the tag writer didn't choose. --- services/search/pkg/bleve/backend_test.go | 12 ++++++++++++ services/search/pkg/query/bleve/compiler.go | 13 ++++++++++++- services/search/pkg/query/bleve/compiler_test.go | 8 ++++---- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/services/search/pkg/bleve/backend_test.go b/services/search/pkg/bleve/backend_test.go index be770f95ec..c85f63fb1d 100644 --- a/services/search/pkg/bleve/backend_test.go +++ b/services/search/pkg/bleve/backend_test.go @@ -137,6 +137,18 @@ var _ = Describe("Bleve", func() { assertDocCount(rootResource.ID, "Size:<1000", 0) assertDocCount(rootResource.ID, "Size:>100000", 0) }) + + // FIXME: switch Title to audio.artist once + // https://github.com/opencloud-eu/opencloud/pull/2632 lands + // (KQL grammar then accepts dotted keys). + It("preserves value case for fields not explicitly marked lowercase", func() { + parentResource.Document.Title = "Some Title" + err := eng.Upsert(parentResource.ID, parentResource) + Expect(err).ToNot(HaveOccurred()) + + assertDocCount(rootResource.ID, `Title:"Some Title"`, 1) + assertDocCount(rootResource.ID, `Title:"some title"`, 0) + }) }) Context("by filename", func() { diff --git a/services/search/pkg/query/bleve/compiler.go b/services/search/pkg/query/bleve/compiler.go index e9d466e936..f5d58997ad 100644 --- a/services/search/pkg/query/bleve/compiler.go +++ b/services/search/pkg/query/bleve/compiler.go @@ -10,6 +10,17 @@ import ( "github.com/opencloud-eu/opencloud/pkg/kql" ) +// lowercaseFields lists the bleve fields whose index mapping uses a lowercasing analyzer. +// Values bound to these fields are pre-lowercased so that non-analyzed query types +// (e.g. wildcard, fuzzy) still match the lowercased terms in the index. +// Keep in sync with services/search/pkg/bleve/index.go NewMapping. +var lowercaseFields = map[string]bool{ + "Name": true, + "Tags": true, + "Favorites": true, + "Content": true, +} + var _fields = map[string]string{ "rootid": "RootID", "path": "Path", @@ -91,7 +102,7 @@ func walk(offset int, nodes []ast.Node) (bleveQuery.Query, int, error) { v = bleveEscaper.Replace(n.Value) } - if k != "Hidden" { + if lowercaseFields[k] { v = strings.ToLower(v) } diff --git a/services/search/pkg/query/bleve/compiler_test.go b/services/search/pkg/query/bleve/compiler_test.go index d450b02550..ff94e88726 100644 --- a/services/search/pkg/query/bleve/compiler_test.go +++ b/services/search/pkg/query/bleve/compiler_test.go @@ -227,8 +227,8 @@ func Test_compile(t *testing.T) { }, }, want: query.NewConjunctionQuery([]query.Query{ - query.NewQueryStringQuery(`author:john\ smith`), - query.NewQueryStringQuery(`author:jane`), + query.NewQueryStringQuery(`author:John\ Smith`), + query.NewQueryStringQuery(`author:Jane`), }), wantErr: false, }, @@ -249,8 +249,8 @@ func Test_compile(t *testing.T) { }, }, want: query.NewConjunctionQuery([]query.Query{ - query.NewQueryStringQuery(`author:john\ smith`), - query.NewQueryStringQuery(`author:jane`), + query.NewQueryStringQuery(`author:John\ Smith`), + query.NewQueryStringQuery(`author:Jane`), query.NewQueryStringQuery(`Tags:bestseller`), }), wantErr: false,