fix(search): preserve query value case for case-sensitive fields on OpenSearch

OpenSearch lowercased every KQL query value, so exact-match queries on
case-preserved keyword fields (facet values, ids) never matched their stored
token. Fold the value only for fields with a lowercasing analyzer, mirroring the
bleve backend. The field set is derived once in search.LowercaseValueFields and
shared by both backends (bleve's local buildLowercaseFields is dropped).
This commit is contained in:
Dominik Schmidt committed 2026-08-31 13:40:42 +02:00
1 parent 7d55849900
commit 40a2f14600
3 files changed
+37 -22

No files matched your search

@@ -3,13 +3,13 @@ package convert
import (
"fmt"
"reflect"
"slices"
"strconv"
"strings"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/opencloud/pkg/ast"
"github.com/opencloud-eu/opencloud/services/search/pkg/search"
)
func ExpandKQL(nodes []ast.Node) ([]ast.Node, error) {
@@ -101,11 +101,13 @@ func (_ kqlExpander) remapKey(current string, defaultKey string) string {
}
func (_ kqlExpander) lowerValue(key, value string) string {
if slices.Contains([]string{"Name", "Title", "Tags", "Content", "MimeType", "Type", "Hidden"}, key) {
return strings.ToLower(value)
// only fold the value for fields whose index analyzer lowercases too;
// case-preserved (keyword) fields must keep their casing or they never match
// their stored token. Shared with bleve via search.LowercaseValueFields.
if _, ok := search.LowercaseValueFields()[key]; !ok {
return value
}
return value
return strings.ToLower(value)
}
func (_ kqlExpander) unfoldValue(key, value string) []ast.Node {
+4 -17
View File
@@ -12,26 +12,13 @@ import (
bleveQuery "github.com/blevesearch/bleve/v2/search/query"
"github.com/opencloud-eu/opencloud/pkg/ast"
"github.com/opencloud-eu/opencloud/pkg/kql"
"github.com/opencloud-eu/opencloud/services/search/pkg/mapping"
"github.com/opencloud-eu/opencloud/services/search/pkg/search"
)
// lowercaseFields is derived from Resource.SearchFieldOverrides(): any
// field whose override picks a lowercasing analyzer (`lowercaseKeyword`)
// or the fulltext type (which uses a lowercasing analyzer under the hood)
// gets its query-side value pre-lowercased so compile-time matches the
// index-time tokenization. Anything else keeps its original casing.
var lowercaseFields = buildLowercaseFields()
func buildLowercaseFields() map[string]struct{} {
out := map[string]struct{}{}
for key, opts := range (search.Resource{}).SearchFieldOverrides() {
if opts.Analyzer == "lowercaseKeyword" || opts.Type == mapping.TypeFulltext {
out[key] = struct{}{}
}
}
return out
}
// lowercaseFields holds the fields whose query-side value is pre-lowercased so
// it matches the index-time lowercasing analyzer. Shared with the OpenSearch
// backend via search.LowercaseValueFields; every other field keeps its casing.
var lowercaseFields = search.LowercaseValueFields()
var _fields = map[string]string{
"rootid": "RootID",
+26
View File
@@ -89,6 +89,32 @@ func (Resource) SearchFieldOverrides() map[string]mapping.FieldOpts {
return resourceFieldOverrides()
}
// lowercaseValueFields is the set of index field names whose query values must
// be lowercased to match their index-time lowercasing analyzer (lowercaseKeyword
// or the fulltext type). Built once from the field overrides.
var lowercaseValueFields = sync.OnceValue(func() map[string]struct{} {
out := map[string]struct{}{}
for key, opts := range resourceFieldOverrides() {
if opts.Analyzer == "lowercaseKeyword" || opts.Type == mapping.TypeFulltext {
out[key] = struct{}{}
}
}
// stored values are normalized lowercase, so query values must fold too
// even though the index fields preserve case
for _, key := range []string{"MimeType", "Type", "Hidden"} {
out[key] = struct{}{}
}
return out
})
// LowercaseValueFields returns the set of index field names whose query values
// must be lowercased so query-side matching lines up with the index-time
// analyzer. Both search backends use it, so value casing stays consistent; every
// other (case-preserved) field keeps its original case. Read-only, do not mutate.
func LowercaseValueFields() map[string]struct{} {
return lowercaseValueFields()
}
// ResolveReference makes sure the path is relative to the space root
func ResolveReference(ctx context.Context, ref *provider.Reference, ri *provider.ResourceInfo, gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) (*provider.Reference, error) {
if ref.GetResourceId().GetOpaqueId() == ref.GetResourceId().GetSpaceId() {