Skip to content
This repository was archived by the owner on Apr 2, 2026. It is now read-only.

Commit 473f72b

Browse files
author
Amit Kumar
committed
test for text_search support and undo extraneous commit
1 parent 6a929dc commit 473f72b

2 files changed

Lines changed: 341 additions & 1 deletion

File tree

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
package planparserv2
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
9+
"github.com/milvus-io/milvus-proto/go-api/v2/commonpb"
10+
"github.com/milvus-io/milvus-proto/go-api/v2/schemapb"
11+
"github.com/milvus-io/milvus/pkg/v2/proto/planpb"
12+
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
13+
)
14+
15+
// newTestSchemaWithTextSearch creates a test schema with text-searchable fields
16+
func newTestSchemaWithTextSearch() *schemapb.CollectionSchema {
17+
fields := []*schemapb.FieldSchema{
18+
{FieldID: 0, Name: "FieldID", IsPrimaryKey: false, Description: "field no.1", DataType: schemapb.DataType_Int64},
19+
{FieldID: 100, Name: "Int64Field", IsPrimaryKey: true, Description: "int64 field", DataType: schemapb.DataType_Int64},
20+
{FieldID: 101, Name: "FloatVectorField", IsPrimaryKey: false, Description: "float vector field", DataType: schemapb.DataType_FloatVector,
21+
TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "128"}}},
22+
{FieldID: 102, Name: "TextField1", IsPrimaryKey: false, Description: "text field 1", DataType: schemapb.DataType_VarChar,
23+
TypeParams: []*commonpb.KeyValuePair{
24+
{Key: "enable_match", Value: "True"},
25+
{Key: "max_length", Value: "1000"},
26+
}},
27+
{FieldID: 103, Name: "TextField2", IsPrimaryKey: false, Description: "text field 2", DataType: schemapb.DataType_VarChar,
28+
TypeParams: []*commonpb.KeyValuePair{
29+
{Key: "enable_match", Value: "True"},
30+
{Key: "max_length", Value: "1000"},
31+
}},
32+
{FieldID: 104, Name: "NormalField", IsPrimaryKey: false, Description: "normal varchar field", DataType: schemapb.DataType_VarChar,
33+
TypeParams: []*commonpb.KeyValuePair{
34+
{Key: "max_length", Value: "500"},
35+
}},
36+
}
37+
38+
return &schemapb.CollectionSchema{
39+
Name: "test_text_search",
40+
Description: "schema for text_search test",
41+
AutoID: true,
42+
Fields: fields,
43+
EnableDynamicField: true,
44+
}
45+
}
46+
47+
func newTestSchemaHelperWithTextSearch(t *testing.T) *typeutil.SchemaHelper {
48+
schema := newTestSchemaWithTextSearch()
49+
schemaHelper, err := typeutil.CreateSchemaHelper(schema)
50+
assert.NoError(t, err)
51+
return schemaHelper
52+
}
53+
54+
// TestExpr_TextSearch_BasicSyntax tests basic text_search expression syntax
55+
func TestExpr_TextSearch_BasicSyntax(t *testing.T) {
56+
schema := newTestSchemaHelperWithTextSearch(t)
57+
58+
// Valid expressions
59+
validExprs := []string{
60+
`text_search(TextField1, "query")`,
61+
`text_search(TextField2, "search terms")`,
62+
`text_search(TextField1, "multi word query")`,
63+
}
64+
for _, exprStr := range validExprs {
65+
expr, err := ParseExpr(schema, exprStr, nil)
66+
assert.NoError(t, err, "Expression should be valid: %s", exprStr)
67+
assert.NotNil(t, expr, "Expression should not be nil: %s", exprStr)
68+
}
69+
}
70+
71+
// TestExpr_TextSearch_WithTopK tests text_search with topk option
72+
func TestExpr_TextSearch_WithTopK(t *testing.T) {
73+
schema := newTestSchemaHelperWithTextSearch(t)
74+
75+
testCases := []struct {
76+
expr string
77+
topk int64
78+
hasError bool
79+
}{
80+
{`text_search(TextField1, "query", topk=10)`, 10, false},
81+
{`text_search(TextField1, "query", topk=100)`, 100, false},
82+
{`text_search(TextField1, "query", topk=1)`, 1, false},
83+
{`text_search(TextField1, "query", topk=0)`, 0, true}, // Invalid: zero
84+
{`text_search(TextField1, "query", topk=-1)`, 0, true}, // Invalid: negative
85+
}
86+
87+
for _, tc := range testCases {
88+
expr, err := ParseExpr(schema, tc.expr, nil)
89+
if tc.hasError {
90+
assert.Error(t, err, "Expression should have error: %s", tc.expr)
91+
} else {
92+
assert.NoError(t, err, "Expression should be valid: %s", tc.expr)
93+
assert.NotNil(t, expr, "Expression should not be nil: %s", tc.expr)
94+
textSearchExpr := expr.GetTextSearchExpr()
95+
assert.NotNil(t, textSearchExpr, "Should be a text_search expression: %s", tc.expr)
96+
assert.Equal(t, tc.topk, textSearchExpr.GetTopk(), "TopK mismatch for: %s", tc.expr)
97+
}
98+
}
99+
}
100+
101+
// TestExpr_TextSearch_MultiField tests text_search with multiple fields
102+
func TestExpr_TextSearch_MultiField(t *testing.T) {
103+
schema := newTestSchemaHelperWithTextSearch(t)
104+
105+
// Multi-field text search
106+
expr, err := ParseExpr(schema, `text_search([TextField1, TextField2], "query")`, nil)
107+
assert.NoError(t, err)
108+
assert.NotNil(t, expr)
109+
textSearchExpr := expr.GetTextSearchExpr()
110+
assert.NotNil(t, textSearchExpr)
111+
assert.Len(t, textSearchExpr.GetFieldIds(), 2)
112+
}
113+
114+
// TestExpr_TextSearch_WithWeights tests text_search with weights option
115+
func TestExpr_TextSearch_WithWeights(t *testing.T) {
116+
schema := newTestSchemaHelperWithTextSearch(t)
117+
118+
// Single field with weight
119+
expr1, err := ParseExpr(schema, `text_search(TextField1, "query", weights=[1.0])`, nil)
120+
assert.NoError(t, err)
121+
assert.NotNil(t, expr1)
122+
textSearch1 := expr1.GetTextSearchExpr()
123+
assert.NotNil(t, textSearch1)
124+
assert.Len(t, textSearch1.GetWeights(), 1)
125+
assert.InDelta(t, 1.0, textSearch1.GetWeights()[0], 0.01)
126+
127+
// Multi-field with weights
128+
expr2, err := ParseExpr(schema, `text_search([TextField1, TextField2], "query", weights=[0.5, 0.5])`, nil)
129+
assert.NoError(t, err)
130+
assert.NotNil(t, expr2)
131+
textSearch2 := expr2.GetTextSearchExpr()
132+
assert.NotNil(t, textSearch2)
133+
assert.Len(t, textSearch2.GetWeights(), 2)
134+
135+
// Invalid: weights count mismatch
136+
_, err = ParseExpr(schema, `text_search([TextField1, TextField2], "query", weights=[0.5])`, nil)
137+
assert.Error(t, err, "Should error when weights count doesn't match field count")
138+
}
139+
140+
// TestExpr_TextSearch_WithAggregation tests text_search with aggregation option
141+
func TestExpr_TextSearch_WithAggregation(t *testing.T) {
142+
schema := newTestSchemaHelperWithTextSearch(t)
143+
144+
testCases := []struct {
145+
expr string
146+
aggregation planpb.BM25AggregationType
147+
hasError bool
148+
}{
149+
{`text_search([TextField1, TextField2], "query", aggregation="weighted_sum")`, planpb.BM25AggregationType_BM25AggWeightedSum, false},
150+
{`text_search([TextField1, TextField2], "query", aggregation="max")`, planpb.BM25AggregationType_BM25AggMax, false},
151+
{`text_search([TextField1, TextField2], "query", aggregation="invalid")`, 0, true},
152+
}
153+
154+
for _, tc := range testCases {
155+
expr, err := ParseExpr(schema, tc.expr, nil)
156+
if tc.hasError {
157+
assert.Error(t, err, "Expression should have error: %s", tc.expr)
158+
} else {
159+
assert.NoError(t, err, "Expression should be valid: %s", tc.expr)
160+
assert.NotNil(t, expr, "Expression should not be nil: %s", tc.expr)
161+
textSearchExpr := expr.GetTextSearchExpr()
162+
assert.NotNil(t, textSearchExpr, "Should be a text_search expression: %s", tc.expr)
163+
assert.Equal(t, tc.aggregation, textSearchExpr.GetAggregation(), "Aggregation mismatch for: %s", tc.expr)
164+
}
165+
}
166+
}
167+
168+
// TestExpr_TextSearch_WithSlop tests text_search with slop option
169+
func TestExpr_TextSearch_WithSlop(t *testing.T) {
170+
schema := newTestSchemaHelperWithTextSearch(t)
171+
172+
testCases := []struct {
173+
expr string
174+
slop uint32
175+
hasError bool
176+
}{
177+
{`text_search(TextField1, "query", slop=0)`, 0, false},
178+
{`text_search(TextField1, "query", slop=1)`, 1, false},
179+
{`text_search(TextField1, "query", slop=10)`, 10, false},
180+
}
181+
182+
for _, tc := range testCases {
183+
expr, err := ParseExpr(schema, tc.expr, nil)
184+
if tc.hasError {
185+
assert.Error(t, err, "Expression should have error: %s", tc.expr)
186+
} else {
187+
assert.NoError(t, err, "Expression should be valid: %s", tc.expr)
188+
assert.NotNil(t, expr, "Expression should not be nil: %s", tc.expr)
189+
textSearchExpr := expr.GetTextSearchExpr()
190+
assert.NotNil(t, textSearchExpr, "Should be a text_search expression: %s", tc.expr)
191+
assert.Equal(t, tc.slop, textSearchExpr.GetSlop(), "Slop mismatch for: %s", tc.expr)
192+
}
193+
}
194+
}
195+
196+
// TestExpr_TextSearch_WithMinShouldMatch tests text_search with minimum_should_match option
197+
func TestExpr_TextSearch_WithMinShouldMatch(t *testing.T) {
198+
schema := newTestSchemaHelperWithTextSearch(t)
199+
200+
testCases := []struct {
201+
expr string
202+
minShouldMatch uint32
203+
hasError bool
204+
}{
205+
{`text_search(TextField1, "query", minimum_should_match=1)`, 1, false},
206+
{`text_search(TextField1, "query", minimum_should_match=2)`, 2, false},
207+
{`text_search(TextField1, "query", minimum_should_match=10)`, 10, false},
208+
}
209+
210+
for _, tc := range testCases {
211+
expr, err := ParseExpr(schema, tc.expr, nil)
212+
if tc.hasError {
213+
assert.Error(t, err, "Expression should have error: %s", tc.expr)
214+
} else {
215+
assert.NoError(t, err, "Expression should be valid: %s", tc.expr)
216+
assert.NotNil(t, expr, "Expression should not be nil: %s", tc.expr)
217+
textSearchExpr := expr.GetTextSearchExpr()
218+
assert.NotNil(t, textSearchExpr, "Should be a text_search expression: %s", tc.expr)
219+
assert.Equal(t, tc.minShouldMatch, textSearchExpr.GetMinimumShouldMatch(), "MinShouldMatch mismatch for: %s", tc.expr)
220+
}
221+
}
222+
}
223+
224+
// TestExpr_TextSearch_CombinedOptions tests text_search with multiple options
225+
func TestExpr_TextSearch_CombinedOptions(t *testing.T) {
226+
schema := newTestSchemaHelperWithTextSearch(t)
227+
228+
expr, err := ParseExpr(schema, `text_search([TextField1, TextField2], "search query", topk=50, weights=[0.7, 0.3], aggregation="max", slop=2, minimum_should_match=1)`, nil)
229+
assert.NoError(t, err)
230+
assert.NotNil(t, expr)
231+
232+
textSearch := expr.GetTextSearchExpr()
233+
assert.NotNil(t, textSearch)
234+
assert.Equal(t, "search query", textSearch.GetQuery())
235+
assert.Equal(t, int64(50), textSearch.GetTopk())
236+
assert.Len(t, textSearch.GetFieldIds(), 2)
237+
assert.Len(t, textSearch.GetWeights(), 2)
238+
assert.InDelta(t, 0.7, textSearch.GetWeights()[0], 0.01)
239+
assert.InDelta(t, 0.3, textSearch.GetWeights()[1], 0.01)
240+
assert.Equal(t, planpb.BM25AggregationType_BM25AggMax, textSearch.GetAggregation())
241+
assert.Equal(t, uint32(2), textSearch.GetSlop())
242+
assert.Equal(t, uint32(1), textSearch.GetMinimumShouldMatch())
243+
}
244+
245+
// TestExpr_TextSearch_InvalidField tests text_search with invalid fields
246+
func TestExpr_TextSearch_InvalidField(t *testing.T) {
247+
schema := newTestSchemaHelperWithTextSearch(t)
248+
249+
invalidExprs := []string{
250+
// Non-existent field
251+
`text_search(NonExistentField, "query")`,
252+
// Non-string field
253+
`text_search(Int64Field, "query")`,
254+
// Field without enable_match
255+
`text_search(NormalField, "query")`,
256+
}
257+
258+
for _, exprStr := range invalidExprs {
259+
_, err := ParseExpr(schema, exprStr, nil)
260+
assert.Error(t, err, "Expression should have error for invalid field: %s", exprStr)
261+
}
262+
}
263+
264+
// TestExpr_TextSearch_DefaultValues tests text_search default option values
265+
func TestExpr_TextSearch_DefaultValues(t *testing.T) {
266+
schema := newTestSchemaHelperWithTextSearch(t)
267+
268+
expr, err := ParseExpr(schema, `text_search(TextField1, "query")`, nil)
269+
assert.NoError(t, err)
270+
assert.NotNil(t, expr)
271+
272+
textSearch := expr.GetTextSearchExpr()
273+
assert.NotNil(t, textSearch)
274+
assert.Equal(t, int64(10), textSearch.GetTopk()) // default topk is 10
275+
assert.Equal(t, planpb.BM25AggregationType_BM25AggWeightedSum, textSearch.GetAggregation()) // default aggregation
276+
assert.Equal(t, uint32(0), textSearch.GetSlop()) // default slop
277+
assert.Equal(t, uint32(0), textSearch.GetMinimumShouldMatch()) // default min_should_match
278+
}
279+
280+
// TestExpr_TextSearch_InSearchPlan tests using text_search in a search plan
281+
func TestExpr_TextSearch_InSearchPlan(t *testing.T) {
282+
schema := newTestSchemaHelperWithTextSearch(t)
283+
284+
// Note: text_search is typically used as a predicate/filter in search plans
285+
// The actual BM25 scoring is handled via TEXT_BM25 metric type
286+
expr := `text_search(TextField1, "search query", topk=100)`
287+
288+
plan, err := CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
289+
Topk: 10,
290+
MetricType: "L2",
291+
SearchParams: "",
292+
RoundDecimal: 0,
293+
}, nil, nil)
294+
295+
assert.NoError(t, err, "Should be able to create search plan with text_search predicate")
296+
assert.NotNil(t, plan)
297+
assert.NotNil(t, plan.GetVectorAnns())
298+
}
299+
300+
// TestExpr_TextSearch_QueryText tests various query text formats
301+
func TestExpr_TextSearch_QueryText(t *testing.T) {
302+
schema := newTestSchemaHelperWithTextSearch(t)
303+
304+
queryTexts := []string{
305+
"simple",
306+
"multi word query",
307+
"special-characters_here",
308+
"numbers 123 456",
309+
"中文查询",
310+
"mixed English 和 中文",
311+
}
312+
313+
for _, queryText := range queryTexts {
314+
exprStr := fmt.Sprintf(`text_search(TextField1, "%s")`, queryText)
315+
expr, err := ParseExpr(schema, exprStr, nil)
316+
assert.NoError(t, err, "Should handle query text: %s", queryText)
317+
assert.NotNil(t, expr)
318+
textSearch := expr.GetTextSearchExpr()
319+
assert.NotNil(t, textSearch)
320+
assert.Equal(t, queryText, textSearch.GetQuery())
321+
}
322+
}
323+
324+
// TestExpr_TextSearch_CaseSensitivity tests case handling in text_search
325+
func TestExpr_TextSearch_CaseSensitivity(t *testing.T) {
326+
schema := newTestSchemaHelperWithTextSearch(t)
327+
328+
// Function name should be case-insensitive in ANTLR grammar
329+
exprs := []string{
330+
`text_search(TextField1, "query")`,
331+
`TEXT_SEARCH(TextField1, "query")`,
332+
`Text_Search(TextField1, "query")`,
333+
}
334+
335+
for _, exprStr := range exprs {
336+
expr, err := ParseExpr(schema, exprStr, nil)
337+
assert.NoError(t, err, "Should handle case variation: %s", exprStr)
338+
assert.NotNil(t, expr)
339+
}
340+
}

scripts/start_standalone.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ if [[ "$OSTYPE" == "linux-gnu"* ]]; then
2929
fi
3030

3131
echo "Starting standalone..."
32-
./bin/milvus run standalone --run-with-subprocess
32+
nohup ./bin/milvus run standalone --run-with-subprocess >/tmp/standalone.log 2>&1 &

0 commit comments

Comments
 (0)