mirror of
https://github.com/milvus-io/milvus.git
synced 2026-07-21 02:05:41 +00:00
feat: impl StructArray -- support index access (#48987)
issue: https://github.com/milvus-io/milvus/issues/42148 design doc: https://github.com/milvus-io/milvus-design-docs/blob/main/design_docs/20260306-struct.md --------- Signed-off-by: SpadeA <tangchenjie1210@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@ expr:
|
||||
| (Identifier|Meta) # Identifier
|
||||
| JSONIdentifier # JSONIdentifier
|
||||
| StructFieldIdentifier # StructField
|
||||
| StructIndexFieldIdentifier # StructIndexField
|
||||
| StructSubFieldIdentifier # StructSubField
|
||||
| LBRACE Identifier RBRACE # TemplateVariable
|
||||
| '(' expr ')' # Parens
|
||||
@@ -38,8 +39,8 @@ expr:
|
||||
| STIsValid'('Identifier')' # STIsValid
|
||||
| ArrayLength'('(Identifier | JSONIdentifier | StructFieldIdentifier)')' # ArrayLength
|
||||
| Identifier '(' ( expr (',' expr )* ','? )? ')' # Call
|
||||
| expr op1 = (LT | LE) (Identifier | JSONIdentifier | StructSubFieldIdentifier) op2 = (LT | LE) expr # Range
|
||||
| expr op1 = (GT | GE) (Identifier | JSONIdentifier | StructSubFieldIdentifier) op2 = (GT | GE) expr # ReverseRange
|
||||
| expr op1 = (LT | LE) (Identifier | JSONIdentifier | StructSubFieldIdentifier | StructIndexFieldIdentifier) op2 = (LT | LE) expr # Range
|
||||
| expr op1 = (GT | GE) (Identifier | JSONIdentifier | StructSubFieldIdentifier | StructIndexFieldIdentifier) op2 = (GT | GE) expr # ReverseRange
|
||||
| expr op = (LT | LE | GT | GE) expr # Relational
|
||||
| expr op = (EQ | NE) expr # Equality
|
||||
| expr BAND expr # BitAnd
|
||||
@@ -140,6 +141,7 @@ Meta: '$meta';
|
||||
|
||||
StringLiteral: EncodingPrefix? ('"' DoubleSCharSequence? '"' | '\'' SingleSCharSequence? '\'');
|
||||
JSONIdentifier: (Identifier | Meta)('[' (StringLiteral | DecimalConstant) ']')+;
|
||||
StructIndexFieldIdentifier: Identifier '[' DecimalConstant ']' '[' Identifier ']';
|
||||
StructFieldIdentifier: Identifier '[' Identifier ']';
|
||||
StructSubFieldIdentifier: '$[' Identifier ']';
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -69,10 +69,11 @@ Identifier=68
|
||||
Meta=69
|
||||
StringLiteral=70
|
||||
JSONIdentifier=71
|
||||
StructFieldIdentifier=72
|
||||
StructSubFieldIdentifier=73
|
||||
Whitespace=74
|
||||
Newline=75
|
||||
StructIndexFieldIdentifier=72
|
||||
StructFieldIdentifier=73
|
||||
StructSubFieldIdentifier=74
|
||||
Whitespace=75
|
||||
Newline=76
|
||||
'('=1
|
||||
')'=2
|
||||
'['=3
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -69,10 +69,11 @@ Identifier=68
|
||||
Meta=69
|
||||
StringLiteral=70
|
||||
JSONIdentifier=71
|
||||
StructFieldIdentifier=72
|
||||
StructSubFieldIdentifier=73
|
||||
Whitespace=74
|
||||
Newline=75
|
||||
StructIndexFieldIdentifier=72
|
||||
StructFieldIdentifier=73
|
||||
StructSubFieldIdentifier=74
|
||||
Whitespace=75
|
||||
Newline=76
|
||||
'('=1
|
||||
')'=2
|
||||
'['=3
|
||||
|
||||
@@ -183,6 +183,10 @@ func (v *BasePlanVisitor) VisitBitAnd(ctx *BitAndContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BasePlanVisitor) VisitStructIndexField(ctx *StructIndexFieldContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BasePlanVisitor) VisitIsNull(ctx *IsNullContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -139,6 +139,9 @@ type PlanVisitor interface {
|
||||
// Visit a parse tree produced by PlanParser#BitAnd.
|
||||
VisitBitAnd(ctx *BitAndContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by PlanParser#StructIndexField.
|
||||
VisitStructIndexField(ctx *StructIndexFieldContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by PlanParser#IsNull.
|
||||
VisitIsNull(ctx *IsNullContext) interface{}
|
||||
|
||||
|
||||
@@ -665,10 +665,6 @@ func isElementFilterExpr(expr *ExprWithType) bool {
|
||||
return expr.expr.GetElementFilterExpr() != nil
|
||||
}
|
||||
|
||||
func isMatchExpr(expr *ExprWithType) bool {
|
||||
return expr.expr.GetMatchExpr() != nil
|
||||
}
|
||||
|
||||
const EPSILON = 1e-10
|
||||
|
||||
func (v *ParserVisitor) VisitRandomSample(ctx *parser.RandomSampleContext) interface{} {
|
||||
@@ -849,7 +845,32 @@ func (v *ParserVisitor) getColumnInfoFromStructSubField(tokenText string) (*plan
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *ParserVisitor) getChildColumnInfo(identifier, child, structSubField antlr.TerminalNode) (*planpb.ColumnInfo, error) {
|
||||
func (v *ParserVisitor) getColumnInfoFromStructIndexField(identifier string) (*planpb.ColumnInfo, error) {
|
||||
// Parse "struct_arr[0][sub_field]" -> fieldName="struct_arr", index="0", subField="sub_field"
|
||||
parts := strings.SplitN(identifier, "[", 3)
|
||||
if len(parts) != 3 {
|
||||
return nil, merr.WrapErrParameterInvalidMsg("invalid struct index field identifier: %s", identifier)
|
||||
}
|
||||
|
||||
fieldName := parts[0]
|
||||
index := strings.TrimSuffix(parts[1], "]")
|
||||
subField := strings.TrimSuffix(parts[2], "]")
|
||||
|
||||
structFieldName := fieldName + "[" + subField + "]"
|
||||
field, err := v.schema.GetFieldFromName(structFieldName)
|
||||
if err != nil {
|
||||
return nil, merr.WrapErrParameterInvalidMsg("struct field not found: %s, error: %s", structFieldName, err)
|
||||
}
|
||||
|
||||
return &planpb.ColumnInfo{
|
||||
FieldId: field.FieldID,
|
||||
DataType: field.DataType,
|
||||
NestedPath: []string{index},
|
||||
ElementType: field.GetElementType(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *ParserVisitor) getChildColumnInfo(identifier, child, structSubField, structIndexField antlr.TerminalNode) (*planpb.ColumnInfo, error) {
|
||||
if identifier != nil {
|
||||
childExpr, err := v.translateIdentifier(identifier.GetText())
|
||||
if err != nil {
|
||||
@@ -862,6 +883,10 @@ func (v *ParserVisitor) getChildColumnInfo(identifier, child, structSubField ant
|
||||
return v.getColumnInfoFromStructSubField(structSubField.GetText())
|
||||
}
|
||||
|
||||
if structIndexField != nil {
|
||||
return v.getColumnInfoFromStructIndexField(structIndexField.GetText())
|
||||
}
|
||||
|
||||
return v.getColumnInfoFromJSONIdentifier(child.GetText())
|
||||
}
|
||||
|
||||
@@ -892,7 +917,12 @@ func (v *ParserVisitor) VisitCall(ctx *parser.CallContext) interface{} {
|
||||
|
||||
// VisitRange translates expr to range plan.
|
||||
func (v *ParserVisitor) VisitRange(ctx *parser.RangeContext) interface{} {
|
||||
columnInfo, err := v.getChildColumnInfo(ctx.Identifier(), ctx.JSONIdentifier(), ctx.StructSubFieldIdentifier())
|
||||
columnInfo, err := v.getChildColumnInfo(
|
||||
ctx.Identifier(),
|
||||
ctx.JSONIdentifier(),
|
||||
ctx.StructSubFieldIdentifier(),
|
||||
ctx.StructIndexFieldIdentifier(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -973,7 +1003,12 @@ func (v *ParserVisitor) VisitRange(ctx *parser.RangeContext) interface{} {
|
||||
|
||||
// VisitReverseRange parses the expression like "1 > a > 0".
|
||||
func (v *ParserVisitor) VisitReverseRange(ctx *parser.ReverseRangeContext) interface{} {
|
||||
columnInfo, err := v.getChildColumnInfo(ctx.Identifier(), ctx.JSONIdentifier(), ctx.StructSubFieldIdentifier())
|
||||
columnInfo, err := v.getChildColumnInfo(
|
||||
ctx.Identifier(),
|
||||
ctx.JSONIdentifier(),
|
||||
ctx.StructSubFieldIdentifier(),
|
||||
ctx.StructIndexFieldIdentifier(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1431,6 +1466,28 @@ func (v *ParserVisitor) VisitStructField(ctx *parser.StructFieldContext) interfa
|
||||
}
|
||||
}
|
||||
|
||||
// VisitStructIndexField handles struct_arr[index][sub_field] syntax for accessing
|
||||
// a specific element's sub-field in a struct array.
|
||||
func (v *ParserVisitor) VisitStructIndexField(ctx *parser.StructIndexFieldContext) interface{} {
|
||||
identifier := ctx.StructIndexFieldIdentifier().GetText()
|
||||
columnInfo, err := v.getColumnInfoFromStructIndexField(identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return &ExprWithType{
|
||||
expr: &planpb.Expr{
|
||||
Expr: &planpb.Expr_ColumnExpr{
|
||||
ColumnExpr: &planpb.ColumnExpr{
|
||||
Info: columnInfo,
|
||||
},
|
||||
},
|
||||
},
|
||||
dataType: columnInfo.GetDataType(),
|
||||
nodeDependent: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *ParserVisitor) VisitExists(ctx *parser.ExistsContext) interface{} {
|
||||
child := ctx.Expr().Accept(v)
|
||||
if err := getError(child); err != nil {
|
||||
@@ -1538,7 +1595,7 @@ func (v *ParserVisitor) VisitEmptyArray(ctx *parser.EmptyArrayContext) interface
|
||||
}
|
||||
|
||||
func (v *ParserVisitor) VisitIsNotNull(ctx *parser.IsNotNullContext) interface{} {
|
||||
column, err := v.getChildColumnInfo(ctx.Identifier(), ctx.JSONIdentifier(), nil)
|
||||
column, err := v.getChildColumnInfo(ctx.Identifier(), ctx.JSONIdentifier(), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1582,7 +1639,7 @@ func (v *ParserVisitor) VisitIsNotNull(ctx *parser.IsNotNullContext) interface{}
|
||||
}
|
||||
|
||||
func (v *ParserVisitor) VisitIsNull(ctx *parser.IsNullContext) interface{} {
|
||||
column, err := v.getChildColumnInfo(ctx.Identifier(), ctx.JSONIdentifier(), nil)
|
||||
column, err := v.getChildColumnInfo(ctx.Identifier(), ctx.JSONIdentifier(), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1804,7 +1861,7 @@ func (v *ParserVisitor) VisitArrayLength(ctx *parser.ArrayLengthContext) interfa
|
||||
ElementType: field.GetElementType(),
|
||||
}
|
||||
} else {
|
||||
columnInfo, err = v.getChildColumnInfo(ctx.Identifier(), ctx.JSONIdentifier(), nil)
|
||||
columnInfo, err = v.getChildColumnInfo(ctx.Identifier(), ctx.JSONIdentifier(), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2740,6 +2740,194 @@ func TestExpr_ArrayContains(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpr_StructIndexField(t *testing.T) {
|
||||
schema := newTestSchema(true)
|
||||
helper, err := typeutil.CreateSchemaHelper(schema)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Valid struct_arr[index][sub_field] expressions
|
||||
validExprs := []string{
|
||||
// comparison
|
||||
`struct_array[0][sub_int] > 50`,
|
||||
`struct_array[0][sub_int] == 100`,
|
||||
`struct_array[1][sub_str] == "apple"`,
|
||||
`struct_array[0][sub_str] != "banana"`,
|
||||
// range via &&
|
||||
`struct_array[0][sub_int] >= 30 && struct_array[0][sub_int] <= 70`,
|
||||
// IN / NOT IN
|
||||
`struct_array[0][sub_int] in [10, 20, 30, 40, 50]`,
|
||||
`struct_array[0][sub_int] not in [0, 1, 2, 3]`,
|
||||
`struct_array[0][sub_str] in ["apple", "banana"]`,
|
||||
`struct_array[0][sub_str] not in ["apple", "banana"]`,
|
||||
}
|
||||
|
||||
for _, expr := range validExprs {
|
||||
assertValidExpr(t, helper, expr)
|
||||
}
|
||||
|
||||
// Invalid expressions
|
||||
invalidExprs := []string{
|
||||
// non-existent parent or sub-field
|
||||
`non_existent[0][sub_int] > 50`,
|
||||
`struct_array[0][non_existent] > 50`,
|
||||
// unsupported form: sub-field first then index
|
||||
`struct_array[sub_int][0] > 50`,
|
||||
`struct_array[sub_str][0] == "apple"`,
|
||||
`struct_array[sub_int][0] in [10, 20, 30]`,
|
||||
}
|
||||
|
||||
for _, expr := range invalidExprs {
|
||||
assertInvalidExpr(t, helper, expr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpr_StructIndexField_PlanShape(t *testing.T) {
|
||||
schema := newTestSchemaHelper(t)
|
||||
|
||||
plan, err := CreateSearchPlan(schema, `struct_array[0][sub_int] == 100`, "FloatVectorField", &planpb.QueryInfo{
|
||||
Topk: 0,
|
||||
MetricType: "",
|
||||
SearchParams: "",
|
||||
RoundDecimal: 0,
|
||||
}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
predicates := plan.GetVectorAnns().GetPredicates()
|
||||
require.NotNil(t, predicates)
|
||||
|
||||
ure := predicates.GetUnaryRangeExpr()
|
||||
require.NotNil(t, ure)
|
||||
assert.Equal(t, planpb.OpType_Equal, ure.GetOp())
|
||||
assert.Equal(t, int64(100), ure.GetValue().GetInt64Val())
|
||||
|
||||
columnInfo := ure.GetColumnInfo()
|
||||
require.NotNil(t, columnInfo)
|
||||
assert.Equal(t, int64(134), columnInfo.GetFieldId())
|
||||
assert.Equal(t, schemapb.DataType_Array, columnInfo.GetDataType())
|
||||
assert.Equal(t, schemapb.DataType_Int32, columnInfo.GetElementType())
|
||||
assert.Equal(t, []string{"0"}, columnInfo.GetNestedPath())
|
||||
|
||||
ret := handleExpr(schema, `struct_array[1][sub_str] in ["apple", "banana"]`)
|
||||
ewt, ok := ret.(*ExprWithType)
|
||||
require.True(t, ok, "handleExpr should return *ExprWithType for term expression")
|
||||
|
||||
te := ewt.expr.GetTermExpr()
|
||||
require.NotNil(t, te)
|
||||
assert.Len(t, te.GetValues(), 2)
|
||||
assert.Equal(t, "apple", te.GetValues()[0].GetStringVal())
|
||||
assert.Equal(t, "banana", te.GetValues()[1].GetStringVal())
|
||||
|
||||
columnInfo = te.GetColumnInfo()
|
||||
require.NotNil(t, columnInfo)
|
||||
assert.Equal(t, int64(133), columnInfo.GetFieldId())
|
||||
assert.Equal(t, schemapb.DataType_Array, columnInfo.GetDataType())
|
||||
assert.Equal(t, schemapb.DataType_VarChar, columnInfo.GetElementType())
|
||||
assert.Equal(t, []string{"1"}, columnInfo.GetNestedPath())
|
||||
}
|
||||
|
||||
func TestExpr_StructFieldArrayLength(t *testing.T) {
|
||||
schema := newTestSchemaHelper(t)
|
||||
|
||||
ret := handleExpr(schema, `array_length(struct_array[sub_int])`)
|
||||
ewt, ok := ret.(*ExprWithType)
|
||||
require.True(t, ok, "handleExpr should return *ExprWithType")
|
||||
assert.Equal(t, schemapb.DataType_Int64, ewt.dataType)
|
||||
|
||||
bae := ewt.expr.GetBinaryArithExpr()
|
||||
require.NotNil(t, bae)
|
||||
assert.Equal(t, planpb.ArithOpType_ArrayLength, bae.GetOp())
|
||||
require.Nil(t, bae.GetRight())
|
||||
|
||||
columnInfo := bae.GetLeft().GetColumnExpr().GetInfo()
|
||||
require.NotNil(t, columnInfo)
|
||||
assert.Equal(t, int64(134), columnInfo.GetFieldId())
|
||||
assert.Equal(t, schemapb.DataType_Array, columnInfo.GetDataType())
|
||||
assert.Equal(t, schemapb.DataType_Int32, columnInfo.GetElementType())
|
||||
assert.Empty(t, columnInfo.GetNestedPath())
|
||||
|
||||
validExprs := []string{
|
||||
`array_length(struct_array[sub_int]) == 10`,
|
||||
`array_length(struct_array[sub_str]) > 0`,
|
||||
}
|
||||
for _, expr := range validExprs {
|
||||
_, err := CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
||||
Topk: 0,
|
||||
MetricType: "",
|
||||
SearchParams: "",
|
||||
RoundDecimal: 0,
|
||||
}, nil, nil)
|
||||
assert.NoError(t, err, expr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpr_StructIndexField_RangeForms(t *testing.T) {
|
||||
schema := newTestSchemaHelper(t)
|
||||
|
||||
testCases := []struct {
|
||||
expr string
|
||||
lower int64
|
||||
upper int64
|
||||
lowerInclusive bool
|
||||
upperInclusive bool
|
||||
}{
|
||||
{
|
||||
expr: `1 < struct_array[0][sub_int] < 3`,
|
||||
lower: 1,
|
||||
upper: 3,
|
||||
lowerInclusive: false,
|
||||
upperInclusive: false,
|
||||
},
|
||||
{
|
||||
expr: `0 <= struct_array[0][sub_int] <= 100`,
|
||||
lower: 0,
|
||||
upper: 100,
|
||||
lowerInclusive: true,
|
||||
upperInclusive: true,
|
||||
},
|
||||
{
|
||||
expr: `100 > struct_array[0][sub_int] > 0`,
|
||||
lower: 0,
|
||||
upper: 100,
|
||||
lowerInclusive: false,
|
||||
upperInclusive: false,
|
||||
},
|
||||
{
|
||||
expr: `100 >= struct_array[0][sub_int] >= 0`,
|
||||
lower: 0,
|
||||
upper: 100,
|
||||
lowerInclusive: true,
|
||||
upperInclusive: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
plan, err := CreateSearchPlan(schema, tc.expr, "FloatVectorField", &planpb.QueryInfo{
|
||||
Topk: 0,
|
||||
MetricType: "",
|
||||
SearchParams: "",
|
||||
RoundDecimal: 0,
|
||||
}, nil, nil)
|
||||
require.NoError(t, err, tc.expr)
|
||||
|
||||
predicates := plan.GetVectorAnns().GetPredicates()
|
||||
require.NotNil(t, predicates, tc.expr)
|
||||
|
||||
bre := predicates.GetBinaryRangeExpr()
|
||||
require.NotNil(t, bre, tc.expr)
|
||||
assert.Equal(t, tc.lower, bre.GetLowerValue().GetInt64Val(), tc.expr)
|
||||
assert.Equal(t, tc.upper, bre.GetUpperValue().GetInt64Val(), tc.expr)
|
||||
assert.Equal(t, tc.lowerInclusive, bre.GetLowerInclusive(), tc.expr)
|
||||
assert.Equal(t, tc.upperInclusive, bre.GetUpperInclusive(), tc.expr)
|
||||
|
||||
columnInfo := bre.GetColumnInfo()
|
||||
require.NotNil(t, columnInfo, tc.expr)
|
||||
assert.Equal(t, int64(134), columnInfo.GetFieldId(), tc.expr)
|
||||
assert.Equal(t, schemapb.DataType_Array, columnInfo.GetDataType(), tc.expr)
|
||||
assert.Equal(t, schemapb.DataType_Int32, columnInfo.GetElementType(), tc.expr)
|
||||
assert.Equal(t, []string{"0"}, columnInfo.GetNestedPath(), tc.expr)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Timestamptz Expression Tests
|
||||
// These tests cover VisitTimestamptzCompareForward and VisitTimestamptzCompareReverse
|
||||
|
||||
Reference in New Issue
Block a user