fix(search): emit SORTABLE after field options (#4199)

FT.CREATE stops reading type-specific field options at SORTABLE, UNF
and NOINDEX. A field with Sortable and Weight, Separator,
CaseSensitive, PhoneticMatcher, WithSuffixtrie, IndexEmpty or
IndexMissing was rejected by the server. Emit SORTABLE, UNF and NOINDEX
after the other options.
This commit is contained in:
Raphael Fakhri authored and GitHub committed 2026-10-26 11:15:36 +02:00
1 parent 29bfa68281
commit 7852b431b5
2 files changed
+71 -9

No files matched your search

+11 -9
View File
@@ -1656,15 +1656,6 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp
if schema.NoStem {
args = append(args, "NOSTEM")
}
if schema.Sortable {
args = append(args, "SORTABLE")
}
if schema.UNF {
args = append(args, "UNF")
}
if schema.NoIndex {
args = append(args, "NOINDEX")
}
if schema.PhoneticMatcher != "" {
args = append(args, "PHONETIC", schema.PhoneticMatcher)
}
@@ -1686,6 +1677,17 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp
if schema.IndexMissing {
args = append(args, "INDEXMISSING")
}
// FT.CREATE stops reading type-specific options at SORTABLE, UNF and
// NOINDEX, so they must come last.
if schema.Sortable {
args = append(args, "SORTABLE")
}
if schema.UNF {
args = append(args, "UNF")
}
if schema.NoIndex {
args = append(args, "NOINDEX")
}
}
cmd := NewStatusCmd(ctx, args...)
_ = c(ctx, cmd)
+60
View File
@@ -524,6 +524,66 @@ func TestFTCreateHNSWRerank_Args(t *testing.T) {
}
}
// TestFTCreateSortableAfterFieldOptions verifies that SORTABLE, UNF and
// NOINDEX follow the type-specific field options. FT.CREATE reads no
// type-specific option after them, so "TEXT SORTABLE WEIGHT 2" fails.
func TestFTCreateSortableAfterFieldOptions(t *testing.T) {
tests := []struct {
name string
schema *FieldSchema
want []interface{}
}{
{
name: "text",
schema: &FieldSchema{
FieldName: "t", FieldType: SearchFieldTypeText,
NoStem: true, PhoneticMatcher: "dm:en", Weight: 2, WithSuffixtrie: true,
IndexEmpty: true, IndexMissing: true, Sortable: true, UNF: true,
},
want: []interface{}{
"FT.CREATE", "idx", "SCHEMA", "t", "TEXT", "NOSTEM", "PHONETIC", "dm:en",
"WEIGHT", float64(2), "WITHSUFFIXTRIE", "INDEXEMPTY", "INDEXMISSING",
"SORTABLE", "UNF",
},
},
{
name: "tag",
schema: &FieldSchema{
FieldName: "g", FieldType: SearchFieldTypeTag,
Separator: ";", CaseSensitive: true, Sortable: true, NoIndex: true,
},
want: []interface{}{
"FT.CREATE", "idx", "SCHEMA", "g", "TAG", "SEPARATOR", ";", "CASESENSITIVE",
"SORTABLE", "NOINDEX",
},
},
{
name: "numeric",
schema: &FieldSchema{
FieldName: "n", FieldType: SearchFieldTypeNumeric,
IndexMissing: true, Sortable: true,
},
want: []interface{}{
"FT.CREATE", "idx", "SCHEMA", "n", "NUMERIC", "INDEXMISSING", "SORTABLE",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &mockCmdable{}
c := m.asCmdable()
cmd := c.FTCreate(context.Background(), "idx", nil, tt.schema)
if cmd.Err() != nil {
t.Fatalf("unexpected error: %v", cmd.Err())
}
if !reflect.DeepEqual(cmd.Args(), tt.want) {
t.Errorf("args mismatch\n got: %#v\nwant: %#v", cmd.Args(), tt.want)
}
})
}
}
// TestFTCreateHNSWCompression_Args verifies the COMPRESSION and
// TRAINING_THRESHOLD attributes (Redis 8.12+, scalar-quantized HNSW) are
// emitted as key-value pairs counted in the attribute-count token.