mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-10-08 11:52:00 -04:00
KQL searches case-insensitively, so every keyword field gets its lowercased search sibling unless it opts out: ids are opaque, paths are POSIX, the mime type is normalized already. That takes the facets along, artist or camera model match regardless of case, while the case-preserved base still answers and aggregates. Which siblings a field carries is decided once, from the struct and the overrides, and the renderers, the document writer and the query lowering all read it from there.
104 lines
3.1 KiB
Go
104 lines
3.1 KiB
Go
package query
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/opencloud-eu/opencloud/services/search/pkg/mapping"
|
|
"github.com/opencloud-eu/opencloud/services/search/pkg/search"
|
|
)
|
|
|
|
// aliases are KQL spellings the derived index can't produce (fields are plural).
|
|
var aliases = map[string]string{
|
|
"tag": "Tags",
|
|
"favorite": "Favorites",
|
|
}
|
|
|
|
// fieldIndex maps a lowercased KQL key to its canonical field name ("" is the
|
|
// bare-search default).
|
|
var fieldIndex = sync.OnceValue(func() map[string]string {
|
|
idx := mapping.FieldNameIndex(reflect.TypeFor[search.Resource](), search.Resource{}.SearchFieldOverrides())
|
|
for k, v := range aliases {
|
|
idx[k] = v
|
|
}
|
|
idx[""] = idx["name"]
|
|
return idx
|
|
})
|
|
|
|
// siblingFields lists which search siblings every field carries, from the
|
|
// resource struct and its overrides.
|
|
var siblingFields = sync.OnceValue(func() map[string]mapping.Siblings {
|
|
return mapping.SearchSiblings(reflect.TypeFor[search.Resource](), search.Resource{}.SearchFieldOverrides())
|
|
})
|
|
|
|
// pathFields are hierarchical path fields (TypePath), derived from the overrides.
|
|
var pathFields = sync.OnceValue(func() map[string]struct{} {
|
|
out := map[string]struct{}{}
|
|
for field, opts := range (search.Resource{}).SearchFieldOverrides() {
|
|
if opts.Type == mapping.TypePath {
|
|
out[field] = struct{}{}
|
|
}
|
|
}
|
|
return out
|
|
})
|
|
|
|
// fulltextFields are analyzed full-text fields (TypeFulltext), derived from the
|
|
// overrides.
|
|
var fulltextFields = sync.OnceValue(func() map[string]struct{} {
|
|
out := map[string]struct{}{}
|
|
for field, opts := range (search.Resource{}).SearchFieldOverrides() {
|
|
if opts.Type == mapping.TypeFulltext {
|
|
out[field] = struct{}{}
|
|
}
|
|
}
|
|
return out
|
|
})
|
|
|
|
// ResolveField maps a KQL key to its canonical field name; unknown keys pass through.
|
|
func ResolveField(name string) string {
|
|
if v, ok := fieldIndex()[strings.ToLower(name)]; ok {
|
|
return v
|
|
}
|
|
return name
|
|
}
|
|
|
|
// normalizedValueFields have their stored values normalized to lowercase at
|
|
// index time, so query values fold to match even though the fields themselves
|
|
// are case-preserved keywords.
|
|
var normalizedValueFields = map[string]struct{}{
|
|
"MimeType": {},
|
|
"Type": {},
|
|
"Hidden": {},
|
|
}
|
|
|
|
// FieldValueIsNormalized reports whether a field's stored values are
|
|
// normalized lowercase.
|
|
func FieldValueIsNormalized(field string) bool {
|
|
_, ok := normalizedValueFields[field]
|
|
return ok
|
|
}
|
|
|
|
// FieldIsCaseInsensitive reports whether a field's default search is case-insensitive.
|
|
func FieldIsCaseInsensitive(field string) bool {
|
|
return siblingFields()[field].Lowercase
|
|
}
|
|
|
|
// FieldIsPath reports whether a field is a hierarchical path field.
|
|
func FieldIsPath(field string) bool {
|
|
_, ok := pathFields()[field]
|
|
return ok
|
|
}
|
|
|
|
// FieldIsFulltext reports whether a field is an analyzed full-text field.
|
|
func FieldIsFulltext(field string) bool {
|
|
_, ok := fulltextFields()[field]
|
|
return ok
|
|
}
|
|
|
|
// FieldIsWordBroken reports whether a field is split into words, so a value
|
|
// without a wildcard matches it as a phrase of those words instead of as a whole.
|
|
func FieldIsWordBroken(field string) bool {
|
|
return siblingFields()[field].Words
|
|
}
|