mirror of
https://github.com/milvus-io/milvus.git
synced 2026-07-21 10:15:43 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user