fix: reject element_filter for row-level vector search (#50301)

issue: https://github.com/milvus-io/milvus/issues/49438
ref: https://github.com/milvus-io/milvus/issues/42148

---------

Signed-off-by: SpadeA <tangchenjie1210@gmail.com>
This commit is contained in:
Spade A
2026-06-10 19:30:20 +08:00
committed by GitHub
parent eecb4c276b
commit a32026fe62
4 changed files with 96 additions and 2 deletions
@@ -116,6 +116,13 @@ PhyVectorSearchNode::GetOutput() {
milvus::BitsetView search_view;
int64_t data_cnt = active_count_;
if (!ph.element_level_ && query_context_->bitset_is_element_level()) {
ThrowInfo(ExprInvalid,
"element-level filter bitset cannot be used for row-level "
"vector search; use MATCH_ANY/MATCH_* for row-level struct "
"array filtering");
}
if (query_context_->get_all_rows_visible() && !ph.element_level_) {
// search_view stays default-constructed (empty)
} else {
+31
View File
@@ -558,6 +558,9 @@ func (t *searchTask) initAdvancedSearchRequest(ctx context.Context) error {
if err != nil {
return err
}
if err := validateElementFilterVectorSearch(plan, t.schema.CollectionSchema, queryInfo.GetQueryFieldId(), placeholderType); err != nil {
return err
}
subSearchInfo := classifyHybridSubSearch(t.schema.CollectionSchema, queryInfo.GetQueryFieldId(), placeholderType)
collapseConfig, elementScopeProvided, sanitizedSearchParams, err := parseAndRemoveElementScope(queryInfo.GetSearchParams())
@@ -958,6 +961,9 @@ func (t *searchTask) initSearchRequest(ctx context.Context) error {
if err != nil {
return err
}
if err := validateElementFilterVectorSearch(plan, t.schema.CollectionSchema, t.FieldId, placeholderType); err != nil {
return err
}
// For ArrayOfVector fields, the placeholder type decides the search semantics:
// - Element-level (plain vector placeholder): behaves like a normal single-vector
@@ -1174,6 +1180,31 @@ func isEmbeddingListPlaceholderType(pt commonpb.PlaceholderType) bool {
}
}
func validateElementFilterVectorSearch(plan *planpb.PlanNode, schema *schemapb.CollectionSchema, fieldID int64, placeholderType commonpb.PlaceholderType) error {
anns := plan.GetVectorAnns()
if anns == nil {
return nil
}
elementFilter := anns.GetPredicates().GetElementFilterExpr()
if elementFilter == nil {
return nil
}
field := typeutil.GetField(schema, fieldID)
parentStructName, isStructSubField := getStructParentFieldName(schema, fieldID)
_, isPlainVectorPlaceholderType := placeholderTypeToDataType[placeholderType]
if field != nil &&
field.GetDataType() == schemapb.DataType_ArrayOfVector &&
isStructSubField &&
parentStructName == elementFilter.GetStructName() &&
isPlainVectorPlaceholderType {
return nil
}
return merr.WrapErrParameterInvalidMsg(
"element_filter is only supported for element-level search on vector sub-fields of the same struct array; use MATCH_ANY/MATCH_* for row-level vector search")
}
func (t *searchTask) PostExecute(ctx context.Context) error {
ctx, sp := otel.Tracer(typeutil.ProxyRole).Start(ctx, "Proxy-Search-PostExecute")
defer sp.End()
+53
View File
@@ -6153,6 +6153,7 @@ func TestSearchTask_ArrayOfVectorSimpleSearch(t *testing.T) {
Name: "struct_array",
Fields: []*schemapb.FieldSchema{
{FieldID: 104, Name: "emb_vec", DataType: schemapb.DataType_ArrayOfVector, ElementType: schemapb.DataType_FloatVector, TypeParams: []*commonpb.KeyValuePair{{Key: common.DimKey, Value: "4"}}},
{FieldID: 105, Name: typeutil.ConcatStructFieldName("struct_array", "price"), DataType: schemapb.DataType_Array, ElementType: schemapb.DataType_Int64},
},
},
},
@@ -6263,6 +6264,37 @@ func TestSearchTask_ArrayOfVectorSimpleSearch(t *testing.T) {
err := task.initSearchRequest(ctx)
assert.NoError(t, err)
})
t.Run("regular vector with element_filter should fail", func(t *testing.T) {
task := makeTask("regular_vec", commonpb.PlaceholderType_FloatVector, plainParams, false, false)
task.request.Dsl = `element_filter(struct_array, $[price] > 10)`
err := task.initSearchRequest(ctx)
assert.Error(t, err)
assert.ErrorIs(t, err, merr.ErrParameterInvalid)
assert.Contains(t, err.Error(), "element_filter is only supported")
})
t.Run("element-level struct vector with element_filter should succeed", func(t *testing.T) {
task := makeTask("emb_vec", commonpb.PlaceholderType_FloatVector, plainParams, false, false)
task.request.Dsl = `element_filter(struct_array, $[price] > 10)`
err := task.initSearchRequest(ctx)
assert.NoError(t, err)
})
t.Run("embedding-list struct vector with element_filter should fail", func(t *testing.T) {
task := makeTask("emb_vec", commonpb.PlaceholderType_EmbListFloatVector, plainParams, false, false)
task.request.Dsl = `element_filter(struct_array, $[price] > 10)`
err := task.initSearchRequest(ctx)
assert.Error(t, err)
assert.ErrorIs(t, err, merr.ErrParameterInvalid)
assert.Contains(t, err.Error(), "element_filter is only supported")
})
}
// TestSearchTask_ArrayOfVectorHybridSearch verifies ArrayOfVector hybrid
@@ -6284,6 +6316,7 @@ func TestSearchTask_ArrayOfVectorHybridSearch(t *testing.T) {
Name: "struct_array",
Fields: []*schemapb.FieldSchema{
{FieldID: 104, Name: "emb_vec", DataType: schemapb.DataType_ArrayOfVector, ElementType: schemapb.DataType_FloatVector, TypeParams: []*commonpb.KeyValuePair{{Key: common.DimKey, Value: "4"}}},
{FieldID: 105, Name: typeutil.ConcatStructFieldName("struct_array", "price"), DataType: schemapb.DataType_Array, ElementType: schemapb.DataType_Int64},
},
},
},
@@ -6392,6 +6425,26 @@ func TestSearchTask_ArrayOfVectorHybridSearch(t *testing.T) {
assert.NoError(t, err)
})
t.Run("hybrid regular vector with element_filter should fail", func(t *testing.T) {
qt := buildHybridTaskWithMetric("regular_vec", metric.L2, commonpb.PlaceholderType_FloatVector, "", false, "")
qt.request.SubReqs[0].Dsl = `element_filter(struct_array, $[price] > 10)`
err := qt.initAdvancedSearchRequest(ctx)
assert.Error(t, err)
assert.ErrorIs(t, err, merr.ErrParameterInvalid)
assert.Contains(t, err.Error(), "element_filter is only supported")
})
t.Run("hybrid element-level struct vector with element_filter should succeed", func(t *testing.T) {
qt := buildElementHybridTask("emb_vec", "", false, "")
qt.request.SubReqs[0].Dsl = `element_filter(struct_array, $[price] > 10)`
err := qt.initAdvancedSearchRequest(ctx)
assert.NoError(t, err)
})
t.Run("hybrid with element-level ArrayOfVector range search should fail", func(t *testing.T) {
qt := buildElementHybridTask("emb_vec", "0.2", false, "")
err := qt.initAdvancedSearchRequest(ctx)
@@ -294,7 +294,7 @@ func runInvertedIndexCase(t *testing.T, ctx CtxT, mc MC, scalarField, namePrefix
index.NewHNSWIndex(entity.COSINE, 16, 200)))
common.CheckErr(t, err, true)
_, err = mc.CreateIndex(ctx, client.NewCreateIndexOption(collName, "structA[embedding]",
index.NewHNSWIndex(entity.MaxSimCosine, 16, 200)))
index.NewHNSWIndex(entity.COSINE, 16, 200)))
common.CheckErr(t, err, true)
_, err = mc.CreateIndex(ctx, client.NewCreateIndexOption(collName, "structA["+scalarField+"]",
index.NewGenericIndex("inv_idx", map[string]string{"index_type": "INVERTED"})))
@@ -305,9 +305,12 @@ func runInvertedIndexCase(t *testing.T, ctx CtxT, mc MC, scalarField, namePrefix
common.CheckErr(t, loadTask.Await(ctx), true)
// Search using row 0's first embedding (deterministic from SeedVector).
queryEmb := entity.FloatVectorArray{entity.FloatVector(hp.SeedVector(0, opt.Dim))}
// Use a plain vector so element_filter runs as element-level search; EmbList
// queries are row-level and must use MATCH_ANY/MATCH_* filters instead.
queryEmb := entity.FloatVector(hp.SeedVector(0, opt.Dim))
rs, err := mc.Search(ctx, client.NewSearchOption(collName, 10, []entity.Vector{queryEmb}).
WithANNSField("structA[embedding]").
WithSearchParam("metric_type", "COSINE").
WithFilter(filter).
WithOutputFields("id").
WithConsistencyLevel(entity.ClStrong))