Files
Dominik Schmidt 094baab7be feat(search): driveId as a query field
scope: takes an opaque resource id, which is hostile to hand-written
queries. Accept driveId:"<storage$space>" as a regular KQL field
instead: it resolves to the indexed RootID, and a bare drive id is
completed to the root resource id (a space root's opaque id is its
space id). Full root ids pass through untouched.

Combined with path: this gives a readable location scope without any
token stripping: both are plain fields, so they compose with groups,
OR and NOT like everything else.
2026-08-31 15:23:25 +02:00

105 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",
"driveid": "RootID",
}
// 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
}