From 49648e8e25d82647ac8a6b124b3ce4957d482be0 Mon Sep 17 00:00:00 2001 From: Spade A <71589810+SpadeA-Tang@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:50:20 +0800 Subject: [PATCH] fix: reject invalid ArrayOfVector metric types (#50363) issue: https://github.com/milvus-io/milvus/issues/42148 Signed-off-by: SpadeA --- internal/proxy/task_index.go | 18 +------ internal/proxy/task_index_test.go | 45 +++++++++++++++++ internal/util/indexparamcheck/constraints.go | 7 +++ internal/util/indexparamcheck/utils.go | 25 ++++++++++ .../indexparamcheck/vector_index_checker.go | 18 +------ .../vector_index_checker_test.go | 49 ++++++++++++++++++- 6 files changed, 129 insertions(+), 33 deletions(-) diff --git a/internal/proxy/task_index.go b/internal/proxy/task_index.go index f3275a1cf5..3cd4f6e9d1 100644 --- a/internal/proxy/task_index.go +++ b/internal/proxy/task_index.go @@ -570,22 +570,8 @@ func (cit *createIndexTask) parseIndexParams(ctx context.Context) error { return merr.WrapErrParameterInvalid("valid index params", "invalid index params", "int vector index does not support metric type: "+metricType) } } else if typeutil.IsArrayOfVectorType(cit.fieldSchema.DataType) { - if !funcutil.SliceContain(indexparamcheck.EmbListMetrics, metricType) { - if typeutil.IsDenseFloatVectorType(cit.fieldSchema.ElementType) { - if !funcutil.SliceContain(indexparamcheck.FloatVectorMetrics, metricType) { - return merr.WrapErrParameterInvalid("valid index params", "invalid index params", "array of vector with float element type does not support metric type: "+metricType) - } - } else if typeutil.IsBinaryVectorType(cit.fieldSchema.ElementType) { - if !funcutil.SliceContain(indexparamcheck.BinaryVectorMetrics, metricType) { - return merr.WrapErrParameterInvalid("valid index params", "invalid index params", "array of vector with binary element type does not support metric type: "+metricType) - } - } else if typeutil.IsIntVectorType(cit.fieldSchema.ElementType) { - if !funcutil.SliceContain(indexparamcheck.IntVectorMetrics, metricType) { - return merr.WrapErrParameterInvalid("valid index params", "invalid index params", "array of vector with int element type does not support metric type: "+metricType) - } - } else { - return merr.WrapErrParameterInvalid("valid index params", "invalid index params", "array of vector index does not support metric type: "+metricType) - } + if err := indexparamcheck.ValidateArrayOfVectorMetricType(cit.fieldSchema.ElementType, metricType); err != nil { + return merr.WrapErrParameterInvalid("valid index params", "invalid index params", err.Error()) } } } diff --git a/internal/proxy/task_index_test.go b/internal/proxy/task_index_test.go index b7260bea0c..8714a67830 100644 --- a/internal/proxy/task_index_test.go +++ b/internal/proxy/task_index_test.go @@ -1489,6 +1489,51 @@ func Test_arrayOfVector_nonEmbListMetric_indexCompat(t *testing.T) { err := cit.parseIndexParams(context.TODO()) assert.NoError(t, err) }) + + t.Run("ArrayOfVector with float element should reject MaxSimHamming", func(t *testing.T) { + cit := &createIndexTask{ + req: &milvuspb.CreateIndexRequest{ + ExtraParams: []*commonpb.KeyValuePair{ + {Key: common.IndexTypeKey, Value: "HNSW_SQ"}, + {Key: common.MetricTypeKey, Value: metric.MaxSimHamming}, + }, + }, + fieldSchema: &schemapb.FieldSchema{ + FieldID: 101, + Name: "vec_field", + DataType: schemapb.DataType_ArrayOfVector, + ElementType: schemapb.DataType_FloatVector, + TypeParams: []*commonpb.KeyValuePair{ + {Key: common.DimKey, Value: "128"}, + }, + }, + } + err := cit.parseIndexParams(context.TODO()) + assert.Error(t, err) + assert.Contains(t, err.Error(), "array of vector with float element type does not support metric type") + }) + + t.Run("ArrayOfVector with binary element should accept MaxSimHamming", func(t *testing.T) { + cit := &createIndexTask{ + req: &milvuspb.CreateIndexRequest{ + ExtraParams: []*commonpb.KeyValuePair{ + {Key: common.IndexTypeKey, Value: "HNSW"}, + {Key: common.MetricTypeKey, Value: metric.MaxSimHamming}, + }, + }, + fieldSchema: &schemapb.FieldSchema{ + FieldID: 101, + Name: "vec_field", + DataType: schemapb.DataType_ArrayOfVector, + ElementType: schemapb.DataType_BinaryVector, + TypeParams: []*commonpb.KeyValuePair{ + {Key: common.DimKey, Value: "128"}, + }, + }, + } + err := cit.parseIndexParams(context.TODO()) + assert.NoError(t, err) + }) } func Test_ngram_parseIndexParams(t *testing.T) { diff --git a/internal/util/indexparamcheck/constraints.go b/internal/util/indexparamcheck/constraints.go index 7974b0e4b8..7726b1eb30 100644 --- a/internal/util/indexparamcheck/constraints.go +++ b/internal/util/indexparamcheck/constraints.go @@ -59,6 +59,13 @@ var ( BinaryVectorMetrics = []string{metric.HAMMING, metric.JACCARD, metric.SUBSTRUCTURE, metric.SUPERSTRUCTURE, metric.MHJACCARD} IntVectorMetrics = []string{metric.L2, metric.IP, metric.COSINE} EmbListMetrics = []string{metric.MaxSim, metric.MaxSimCosine, metric.MaxSimL2, metric.MaxSimIP, metric.MaxSimHamming, metric.MaxSimJaccard} + + ArrayOfVectorFloatMetrics = []string{metric.L2, metric.IP, metric.COSINE, metric.MaxSim, metric.MaxSimCosine, metric.MaxSimL2, metric.MaxSimIP} + ArrayOfVectorBinaryMetrics = []string{ + metric.HAMMING, metric.JACCARD, metric.SUBSTRUCTURE, metric.SUPERSTRUCTURE, metric.MHJACCARD, + metric.MaxSimHamming, metric.MaxSimJaccard, + } + ArrayOfVectorIntMetrics = []string{metric.L2, metric.IP, metric.COSINE, metric.MaxSim, metric.MaxSimCosine, metric.MaxSimL2, metric.MaxSimIP} ) // BinIDMapMetrics is a set of all metric types supported for binary vector. diff --git a/internal/util/indexparamcheck/utils.go b/internal/util/indexparamcheck/utils.go index d31e2413a4..9d9d7f3ae1 100644 --- a/internal/util/indexparamcheck/utils.go +++ b/internal/util/indexparamcheck/utils.go @@ -24,6 +24,7 @@ import ( "github.com/milvus-io/milvus/pkg/v3/common" "github.com/milvus-io/milvus/pkg/v3/util/funcutil" "github.com/milvus-io/milvus/pkg/v3/util/paramtable" + "github.com/milvus-io/milvus/pkg/v3/util/typeutil" ) // CheckIntByRange check if the data corresponding to the key is in the range of [min, max]. @@ -62,6 +63,30 @@ func CheckStrByValues(params map[string]string, key string, container []string) return funcutil.SliceContain(container, value) } +// ValidateArrayOfVectorMetricType validates both element-level and EmbList metrics +// against the ArrayOfVector element type. +func ValidateArrayOfVectorMetricType(elementType schemapb.DataType, metricType string) error { + if typeutil.IsDenseFloatVectorType(elementType) { + if !funcutil.SliceContain(ArrayOfVectorFloatMetrics, metricType) { + return fmt.Errorf("array of vector with float element type does not support metric type: %s, supported: %v", metricType, ArrayOfVectorFloatMetrics) + } + return nil + } + if typeutil.IsBinaryVectorType(elementType) { + if !funcutil.SliceContain(ArrayOfVectorBinaryMetrics, metricType) { + return fmt.Errorf("array of vector with binary element type does not support metric type: %s, supported: %v", metricType, ArrayOfVectorBinaryMetrics) + } + return nil + } + if typeutil.IsIntVectorType(elementType) { + if !funcutil.SliceContain(ArrayOfVectorIntMetrics, metricType) { + return fmt.Errorf("array of vector with int element type does not support metric type: %s, supported: %v", metricType, ArrayOfVectorIntMetrics) + } + return nil + } + return fmt.Errorf("array of vector index does not support element type: %s", elementType.String()) +} + func errOutOfRange(x interface{}, lb interface{}, ub interface{}) error { return fmt.Errorf("%v out of range: [%v, %v]", x, lb, ub) } diff --git a/internal/util/indexparamcheck/vector_index_checker.go b/internal/util/indexparamcheck/vector_index_checker.go index 8281f3b2be..daa8557e02 100644 --- a/internal/util/indexparamcheck/vector_index_checker.go +++ b/internal/util/indexparamcheck/vector_index_checker.go @@ -73,22 +73,8 @@ func (c vecIndexChecker) StaticCheck(dataType schemapb.DataType, elementType sch return fmt.Errorf("metric type %s not found or not supported, supported: %v", params[Metric], IntVectorMetrics) } } else if typeutil.IsArrayOfVectorType(dataType) { - if !CheckStrByValues(params, Metric, EmbListMetrics) { - if typeutil.IsDenseFloatVectorType(elementType) { - if !CheckStrByValues(params, Metric, FloatVectorMetrics) { - return fmt.Errorf("metric type %s not found or not supported for array of vector with float element type, supported: %v", params[Metric], FloatVectorMetrics) - } - } else if typeutil.IsBinaryVectorType(elementType) { - if !CheckStrByValues(params, Metric, BinaryVectorMetrics) { - return fmt.Errorf("metric type %s not found or not supported for array of vector with binary element type, supported: %v", params[Metric], BinaryVectorMetrics) - } - } else if typeutil.IsIntVectorType(elementType) { - if !CheckStrByValues(params, Metric, IntVectorMetrics) { - return fmt.Errorf("metric type %s not found or not supported for array of vector with int element type, supported: %v", params[Metric], IntVectorMetrics) - } - } else { - return fmt.Errorf("metric type %s not found or not supported for array of vector, supported: %v", params[Metric], EmbListMetrics) - } + if err := ValidateArrayOfVectorMetricType(elementType, params[Metric]); err != nil { + return err } } diff --git a/internal/util/indexparamcheck/vector_index_checker_test.go b/internal/util/indexparamcheck/vector_index_checker_test.go index 2df809343b..806603d3cc 100644 --- a/internal/util/indexparamcheck/vector_index_checker_test.go +++ b/internal/util/indexparamcheck/vector_index_checker_test.go @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/milvus-io/milvus-proto/go-api/v3/schemapb" + "github.com/milvus-io/milvus/pkg/v3/common" + "github.com/milvus-io/milvus/pkg/v3/util/metric" ) func TestVecIndexChecker_StaticCheck(t *testing.T) { @@ -14,6 +16,7 @@ func TestVecIndexChecker_StaticCheck(t *testing.T) { tests := []struct { name string dataType schemapb.DataType + elemType schemapb.DataType params map[string]string wantErr bool }{ @@ -70,11 +73,55 @@ func TestVecIndexChecker_StaticCheck(t *testing.T) { }, wantErr: true, }, + { + name: "ArrayOfVector float accepts MaxSimCosine", + dataType: schemapb.DataType_ArrayOfVector, + elemType: schemapb.DataType_FloatVector, + params: map[string]string{ + common.IndexTypeKey: "HNSW", + common.MetricTypeKey: metric.MaxSimCosine, + HNSWM: "16", + EFConstruction: "200", + }, + wantErr: false, + }, + { + name: "ArrayOfVector float rejects MaxSimHamming", + dataType: schemapb.DataType_ArrayOfVector, + elemType: schemapb.DataType_FloatVector, + params: map[string]string{ + common.IndexTypeKey: "HNSW_SQ", + common.MetricTypeKey: metric.MaxSimHamming, + }, + wantErr: true, + }, + { + name: "ArrayOfVector binary accepts MaxSimHamming", + dataType: schemapb.DataType_ArrayOfVector, + elemType: schemapb.DataType_BinaryVector, + params: map[string]string{ + common.IndexTypeKey: "HNSW", + common.MetricTypeKey: metric.MaxSimHamming, + HNSWM: "16", + EFConstruction: "200", + }, + wantErr: false, + }, + { + name: "ArrayOfVector binary rejects MaxSimCosine", + dataType: schemapb.DataType_ArrayOfVector, + elemType: schemapb.DataType_BinaryVector, + params: map[string]string{ + common.IndexTypeKey: "HNSW", + common.MetricTypeKey: metric.MaxSimCosine, + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := checker.StaticCheck(tt.dataType, schemapb.DataType_None, tt.params) + err := checker.StaticCheck(tt.dataType, tt.elemType, tt.params) if tt.wantErr { assert.Error(t, err) } else {