Skip to content

Commit

Permalink
fix: Fix bug where prefix matching fails when wildcards are in prefix (
Browse files Browse the repository at this point in the history
…#40020)

issue: #40019

---------

Signed-off-by: Cai Zhang <[email protected]>
  • Loading branch information
xiaocai2333 authored Feb 28, 2025
1 parent 762a644 commit dc46b08
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 39 deletions.
35 changes: 22 additions & 13 deletions internal/parser/planparserv2/pattern_match.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package planparserv2

import (
"strings"

"github.com/milvus-io/milvus/pkg/v2/proto/planpb"
)

Expand All @@ -12,20 +14,27 @@ var wildcards = map[byte]struct{}{
var escapeCharacter byte = '\\'

// hasWildcards returns true if pattern contains any wildcard.
func hasWildcards(pattern string) bool {
l := len(pattern)
i := l - 1
for ; i >= 0; i-- {
_, ok := wildcards[pattern[i]]
if ok {
if i > 0 && pattern[i-1] == escapeCharacter {
i--
func hasWildcards(pattern string) (string, bool) {
var result strings.Builder
hasWildcard := false

for i := 0; i < len(pattern); i++ {
if pattern[i] == escapeCharacter && i+1 < len(pattern) {
next := pattern[i+1]
if _, ok := wildcards[next]; ok {
result.WriteByte(next)
i++
continue
}
return true
}

if _, ok := wildcards[pattern[i]]; ok {
hasWildcard = true
}
result.WriteByte(pattern[i])
}
return false

return result.String(), hasWildcard
}

// findLastNotOfWildcards find the last location not of last wildcard.
Expand Down Expand Up @@ -55,14 +64,14 @@ func translatePatternMatch(pattern string) (op planpb.OpType, operand string, er
return planpb.OpType_PrefixMatch, "", nil
}

exist := hasWildcards(pattern[:loc+1])
newPattern, exist := hasWildcards(pattern[:loc+1])
if loc >= l-1 && !exist {
// equal match.
return planpb.OpType_Equal, pattern, nil
return planpb.OpType_Equal, newPattern, nil
}
if !exist {
// prefix match.
return planpb.OpType_PrefixMatch, pattern[:loc+1], nil
return planpb.OpType_PrefixMatch, newPattern, nil
}

return planpb.OpType_Match, pattern, nil
Expand Down
22 changes: 14 additions & 8 deletions internal/parser/planparserv2/pattern_match_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,44 @@ func Test_hasWildcards(t *testing.T) {
pattern string
}
tests := []struct {
name string
args args
want bool
name string
args args
want bool
target string
}{
{
args: args{
pattern: "no-wildcards",
},
want: false,
want: false,
target: "no-wildcards",
},
{
args: args{
pattern: "has\\%",
},
want: false,
want: false,
target: "has%",
},
{
args: args{
pattern: "%",
},
want: true,
want: true,
target: "%",
},
{
args: args{
pattern: "has%",
},
want: true,
want: true,
target: "has%",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := hasWildcards(tt.args.pattern); got != tt.want {
patten, got := hasWildcards(tt.args.pattern)
if got != tt.want || patten != tt.target {
t.Errorf("hasWildcards(%s) = %v, want %v", tt.args.pattern, got, tt.want)
}
})
Expand Down
55 changes: 37 additions & 18 deletions internal/parser/planparserv2/plan_parser_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,25 +204,44 @@ func TestExpr_Like(t *testing.T) {
helper, err := typeutil.CreateSchemaHelper(schema)
assert.NoError(t, err)

exprStrs := []string{
`VarCharField like "prefix%"`,
`VarCharField like "equal"`,
`JSONField["A"] like "name*"`,
`$meta["A"] like "name*"`,
}
for _, exprStr := range exprStrs {
assertValidExpr(t, helper, exprStr)
}
expr := `A like "8\\_0%"`
plan, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
Topk: 0,
MetricType: "",
SearchParams: "",
RoundDecimal: 0,
}, nil)
assert.NoError(t, err, expr)
assert.NotNil(t, plan)
fmt.Println(plan)
assert.Equal(t, planpb.OpType_PrefixMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
assert.Equal(t, "8_0", plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal())

expr = `A like "8_\\_0%"`
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
Topk: 0,
MetricType: "",
SearchParams: "",
RoundDecimal: 0,
}, nil)
assert.NoError(t, err, expr)
assert.NotNil(t, plan)
fmt.Println(plan)
assert.Equal(t, planpb.OpType_Match, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
assert.Equal(t, `8_\_0%`, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal())

// TODO: enable these after regex-match is supported.
//unsupported := []string{
// `VarCharField like "not_%_supported"`,
// `JSONField["A"] like "not_%_supported"`,
// `$meta["A"] like "not_%_supported"`,
//}
//for _, exprStr := range unsupported {
// assertInvalidExpr(t, helper, exprStr)
//}
expr = `A like "8\\%-0%"`
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
Topk: 0,
MetricType: "",
SearchParams: "",
RoundDecimal: 0,
}, nil)
assert.NoError(t, err, expr)
assert.NotNil(t, plan)
fmt.Println(plan)
assert.Equal(t, planpb.OpType_PrefixMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
assert.Equal(t, `8%-0`, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal())
}

func TestExpr_TextMatch(t *testing.T) {
Expand Down

0 comments on commit dc46b08

Please sign in to comment.