vector: round-trip and enforce non-filterable metadata keys (#12157)

vector: non-filterable metadata round-trips and filters reject it

- GetIndex omitted nonFilterableMetadataKeys — the catalog stores the
  list as a JSON string but the reader only accepted an array; both
  forms now decode.
- QueryVectors now rejects filters referencing non-filterable keys
  instead of silently scanning them.
This commit is contained in:
Chris Lu authored and GitHub committed 2026-10-28 01:02:41 -07:00
1 parent 190ab32cfb
commit 237db29dca
1 file changed
+38 -2
+38 -2
View File
@@ -557,7 +557,13 @@ func indexFromRecord(s *Server, bucket, name string, m map[string]interface{}) *
fmt.Sscan(fmt.Sprint(fm["dimension"]), &dim)
idx.Dimension = int32(dim)
idx.DistanceMetric = fmt.Sprint(fm["distanceMetric"])
if keys, ok := fm["nonFilterableMetadataKeys"].([]interface{}); ok {
switch keys := fm["nonFilterableMetadataKeys"].(type) {
case string:
var list []string
if err := json.Unmarshal([]byte(keys), &list); err == nil && list != nil {
idx.MetadataConfiguration = &MetadataConfiguration{NonFilterableMetadataKeys: list}
}
case []interface{}:
var list []string
for _, k := range keys {
list = append(list, fmt.Sprint(k))
@@ -945,6 +951,24 @@ func translateFilter(filter map[string]interface{}) (string, error) {
return filterExpr(filter)
}
// filterFields collects the field names a filter expression references —
// $-operators recurse; every other key is a field.
func filterFields(filter map[string]interface{}, out map[string]bool) {
for key, cond := range filter {
if strings.HasPrefix(key, "$") {
if sub, ok := cond.([]interface{}); ok {
for _, item := range sub {
if m, ok := item.(map[string]interface{}); ok {
filterFields(m, out)
}
}
}
continue
}
out[key] = true
}
}
func filterExpr(filter map[string]interface{}) (string, error) {
parts := make([]string, 0, len(filter))
for key, cond := range filter {
@@ -1185,7 +1209,19 @@ func (s *Server) handleQueryVectors(w http.ResponseWriter, r *http.Request) {
catalogErr(w, err)
return
}
metric := indexFromRecord(s, arns.bucket, arns.index, record).DistanceMetric
idx := indexFromRecord(s, arns.bucket, arns.index, record)
if cfg := idx.MetadataConfiguration; cfg != nil && len(cfg.NonFilterableMetadataKeys) > 0 {
fields := map[string]bool{}
filterFields(req.Filter, fields)
for _, k := range cfg.NonFilterableMetadataKeys {
if fields[k] {
writeError(w, http.StatusBadRequest, "ValidationException",
fmt.Sprintf("metadata key %q is not filterable", k))
return
}
}
}
metric := idx.DistanceMetric
filter, err := translateFilter(req.Filter)
if err != nil {
writeError(w, http.StatusBadRequest, "ValidationException", err.Error())