diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index f826ecce..75b12827 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -25,7 +25,7 @@ runs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.2.x"]="8.2-RC1-pre" + ["8.2.x"]="8.2-rc2-pre" ["8.0.x"]="8.0.2" ["7.4.x"]="rs-7.4.0-v5" ["7.2.x"]="rs-7.2.0-v17" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ef6eadfc..8424f63c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,9 @@ name: Go on: push: - branches: [master, v9, v9.7, v9.8] + branches: [master, v9, v9.7, v9.8, 'ndyakov/*', 'ofekshenawa/*', 'htemelski-redis/*', 'ce/*'] pull_request: - branches: [master, v9, v9.7, v9.8] + branches: [master, v9, v9.7, v9.8, 'ndyakov/*', 'ofekshenawa/*', 'htemelski-redis/*', 'ce/*'] permissions: contents: read @@ -44,7 +44,7 @@ jobs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.2.x"]="8.2-RC1-pre" + ["8.2.x"]="8.2-rc2-pre" ["8.0.x"]="8.0.2" ["7.4.x"]="rs-7.4.0-v5" ) diff --git a/commands_test.go b/commands_test.go index 19548e13..9e130089 100644 --- a/commands_test.go +++ b/commands_test.go @@ -6169,6 +6169,34 @@ var _ = Describe("Commands", func() { Expect(n).To(Equal(int64(3))) }) + It("should XTrimMaxLenMode", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + n, err := client.XTrimMaxLenMode(ctx, "stream", 0, "KEEPREF").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(BeNumerically(">=", 0)) + }) + + It("should XTrimMaxLenApproxMode", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + n, err := client.XTrimMaxLenApproxMode(ctx, "stream", 0, 0, "KEEPREF").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(BeNumerically(">=", 0)) + }) + + It("should XTrimMinIDMode", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + n, err := client.XTrimMinIDMode(ctx, "stream", "4-0", "KEEPREF").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(BeNumerically(">=", 0)) + }) + + It("should XTrimMinIDApproxMode", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + n, err := client.XTrimMinIDApproxMode(ctx, "stream", "4-0", 0, "KEEPREF").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(BeNumerically(">=", 0)) + }) + It("should XAdd", func() { id, err := client.XAdd(ctx, &redis.XAddArgs{ Stream: "stream", @@ -6222,6 +6250,37 @@ var _ = Describe("Commands", func() { Expect(n).To(Equal(int64(3))) }) + It("should XAckDel", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + // First, create a consumer group + err := client.XGroupCreate(ctx, "stream", "testgroup", "0").Err() + Expect(err).NotTo(HaveOccurred()) + + // Read messages to create pending entries + _, err = client.XReadGroup(ctx, &redis.XReadGroupArgs{ + Group: "testgroup", + Consumer: "testconsumer", + Streams: []string{"stream", ">"}, + }).Result() + Expect(err).NotTo(HaveOccurred()) + + // Test XAckDel with KEEPREF mode + n, err := client.XAckDel(ctx, "stream", "testgroup", "KEEPREF", "1-0", "2-0").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(HaveLen(2)) + + // Clean up + client.XGroupDestroy(ctx, "stream", "testgroup") + }) + + It("should XDelEx", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + // Test XDelEx with KEEPREF mode + n, err := client.XDelEx(ctx, "stream", "KEEPREF", "1-0", "2-0").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(HaveLen(2)) + }) + It("should XLen", func() { n, err := client.XLen(ctx, "stream").Result() Expect(err).NotTo(HaveOccurred()) diff --git a/doctests/geo_index_test.go b/doctests/geo_index_test.go index c497b722..2e944274 100644 --- a/doctests/geo_index_test.go +++ b/doctests/geo_index_test.go @@ -199,11 +199,11 @@ func ExampleClient_geoindex() { // OK // OK // OK - // {1 [{product:46885 map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}]}]} + // {1 [{product:46885 map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}] }]} // OK // OK // OK // OK // OK - // {1 [{shape:4 map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]]}]} + // {1 [{shape:4 map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]] }]} } diff --git a/doctests/home_json_example_test.go b/doctests/home_json_example_test.go index f32bf8d1..d2af4d73 100644 --- a/doctests/home_json_example_test.go +++ b/doctests/home_json_example_test.go @@ -219,7 +219,7 @@ func ExampleClient_search_json() { // STEP_END // Output: - // {1 [{user:3 map[$:{"age":35,"city":"Tel Aviv","email":"paul.zamir@example.com","name":"Paul Zamir"}]}]} + // {1 [{user:3 map[$:{"age":35,"city":"Tel Aviv","email":"paul.zamir@example.com","name":"Paul Zamir"}] }]} // London // Tel Aviv // 0 @@ -329,5 +329,5 @@ func ExampleClient_search_hash() { // STEP_END // Output: - // {1 [{huser:3 map[age:35 city:Tel Aviv email:paul.zamir@example.com name:Paul Zamir]}]} + // {1 [{huser:3 map[age:35 city:Tel Aviv email:paul.zamir@example.com name:Paul Zamir] }]} } diff --git a/doctests/search_quickstart_test.go b/doctests/search_quickstart_test.go index ce5033bc..a41c9c5c 100644 --- a/doctests/search_quickstart_test.go +++ b/doctests/search_quickstart_test.go @@ -257,6 +257,6 @@ func ExampleClient_search_qs() { // Output: // Documents found: 10 - // {1 [{bicycle:0 map[$:{"brand":"Velorim","condition":"new","description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","model":"Jigger","price":270}]}]} - // {1 [{bicycle:4 map[$:{"brand":"Noka Bikes","condition":"used","description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","model":"Kahuna","price":3200}]}]} + // {1 [{bicycle:0 map[$:{"brand":"Velorim","condition":"new","description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","model":"Jigger","price":270}] }]} + // {1 [{bicycle:4 map[$:{"brand":"Noka Bikes","condition":"used","description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","model":"Kahuna","price":3200}] }]} } diff --git a/search_commands.go b/search_commands.go index b31baaa7..f0ca1bfe 100644 --- a/search_commands.go +++ b/search_commands.go @@ -80,8 +80,9 @@ type FieldSchema struct { } type FTVectorArgs struct { - FlatOptions *FTFlatOptions - HNSWOptions *FTHNSWOptions + FlatOptions *FTFlatOptions + HNSWOptions *FTHNSWOptions + VamanaOptions *FTVamanaOptions } type FTFlatOptions struct { @@ -103,6 +104,19 @@ type FTHNSWOptions struct { Epsilon float64 } +type FTVamanaOptions struct { + Type string + Dim int + DistanceMetric string + Compression string + ConstructionWindowSize int + GraphMaxDegree int + SearchWindowSize int + Epsilon float64 + TrainingThreshold int + ReduceDim int +} + type FTDropIndexOptions struct { DeleteDocs bool } @@ -474,6 +488,7 @@ type Document struct { Payload *string SortKey *string Fields map[string]string + Error error } type AggregateQuery []interface{} @@ -498,7 +513,7 @@ func (c cmdable) FTAggregate(ctx context.Context, index string, query string) *M return cmd } -func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery { +func FTAggregateQuery(query string, options *FTAggregateOptions) (AggregateQuery, error) { queryArgs := []interface{}{query} if options != nil { if options.Verbatim { @@ -514,7 +529,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery } if options.LoadAll && options.Load != nil { - panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") + return nil, fmt.Errorf("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") } if options.LoadAll { queryArgs = append(queryArgs, "LOAD", "*") @@ -570,7 +585,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery for _, sortBy := range options.SortBy { sortByOptions = append(sortByOptions, sortBy.FieldName) if sortBy.Asc && sortBy.Desc { - panic("FT.AGGREGATE: ASC and DESC are mutually exclusive") + return nil, fmt.Errorf("FT.AGGREGATE: ASC and DESC are mutually exclusive") } if sortBy.Asc { sortByOptions = append(sortByOptions, "ASC") @@ -615,7 +630,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery queryArgs = append(queryArgs, "DIALECT", 2) } } - return queryArgs + return queryArgs, nil } func ProcessAggregateResult(data []interface{}) (*FTAggregateResult, error) { @@ -717,7 +732,9 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st args = append(args, "ADDSCORES") } if options.LoadAll && options.Load != nil { - panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") + cmd := NewAggregateCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive")) + return cmd } if options.LoadAll { args = append(args, "LOAD", "*") @@ -770,7 +787,9 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st for _, sortBy := range options.SortBy { sortByOptions = append(sortByOptions, sortBy.FieldName) if sortBy.Asc && sortBy.Desc { - panic("FT.AGGREGATE: ASC and DESC are mutually exclusive") + cmd := NewAggregateCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.AGGREGATE: ASC and DESC are mutually exclusive")) + return cmd } if sortBy.Asc { sortByOptions = append(sortByOptions, "ASC") @@ -918,7 +937,9 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp args = append(args, "ON", "JSON") } if options.OnHash && options.OnJSON { - panic("FT.CREATE: ON HASH and ON JSON are mutually exclusive") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: ON HASH and ON JSON are mutually exclusive")) + return cmd } if options.Prefix != nil { args = append(args, "PREFIX", len(options.Prefix)) @@ -969,12 +990,16 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp } } if schema == nil { - panic("FT.CREATE: SCHEMA is required") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA is required")) + return cmd } args = append(args, "SCHEMA") for _, schema := range schema { if schema.FieldName == "" || schema.FieldType == SearchFieldTypeInvalid { - panic("FT.CREATE: SCHEMA FieldName and FieldType are required") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA FieldName and FieldType are required")) + return cmd } args = append(args, schema.FieldName) if schema.As != "" { @@ -983,15 +1008,32 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp args = append(args, schema.FieldType.String()) if schema.VectorArgs != nil { if schema.FieldType != SearchFieldTypeVector { - panic("FT.CREATE: SCHEMA FieldType VECTOR is required for VectorArgs") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA FieldType VECTOR is required for VectorArgs")) + return cmd } - if schema.VectorArgs.FlatOptions != nil && schema.VectorArgs.HNSWOptions != nil { - panic("FT.CREATE: SCHEMA VectorArgs FlatOptions and HNSWOptions are mutually exclusive") + // Check mutual exclusivity of vector options + optionCount := 0 + if schema.VectorArgs.FlatOptions != nil { + optionCount++ + } + if schema.VectorArgs.HNSWOptions != nil { + optionCount++ + } + if schema.VectorArgs.VamanaOptions != nil { + optionCount++ + } + if optionCount != 1 { + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions")) + return cmd } if schema.VectorArgs.FlatOptions != nil { args = append(args, "FLAT") if schema.VectorArgs.FlatOptions.Type == "" || schema.VectorArgs.FlatOptions.Dim == 0 || schema.VectorArgs.FlatOptions.DistanceMetric == "" { - panic("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR FLAT") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR FLAT")) + return cmd } flatArgs := []interface{}{ "TYPE", schema.VectorArgs.FlatOptions.Type, @@ -1010,7 +1052,9 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp if schema.VectorArgs.HNSWOptions != nil { args = append(args, "HNSW") if schema.VectorArgs.HNSWOptions.Type == "" || schema.VectorArgs.HNSWOptions.Dim == 0 || schema.VectorArgs.HNSWOptions.DistanceMetric == "" { - panic("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR HNSW") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR HNSW")) + return cmd } hnswArgs := []interface{}{ "TYPE", schema.VectorArgs.HNSWOptions.Type, @@ -1035,10 +1079,48 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp args = append(args, len(hnswArgs)) args = append(args, hnswArgs...) } + if schema.VectorArgs.VamanaOptions != nil { + args = append(args, "SVS-VAMANA") + if schema.VectorArgs.VamanaOptions.Type == "" || schema.VectorArgs.VamanaOptions.Dim == 0 || schema.VectorArgs.VamanaOptions.DistanceMetric == "" { + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR VAMANA")) + return cmd + } + vamanaArgs := []interface{}{ + "TYPE", schema.VectorArgs.VamanaOptions.Type, + "DIM", schema.VectorArgs.VamanaOptions.Dim, + "DISTANCE_METRIC", schema.VectorArgs.VamanaOptions.DistanceMetric, + } + if schema.VectorArgs.VamanaOptions.Compression != "" { + vamanaArgs = append(vamanaArgs, "COMPRESSION", schema.VectorArgs.VamanaOptions.Compression) + } + if schema.VectorArgs.VamanaOptions.ConstructionWindowSize > 0 { + vamanaArgs = append(vamanaArgs, "CONSTRUCTION_WINDOW_SIZE", schema.VectorArgs.VamanaOptions.ConstructionWindowSize) + } + if schema.VectorArgs.VamanaOptions.GraphMaxDegree > 0 { + vamanaArgs = append(vamanaArgs, "GRAPH_MAX_DEGREE", schema.VectorArgs.VamanaOptions.GraphMaxDegree) + } + if schema.VectorArgs.VamanaOptions.SearchWindowSize > 0 { + vamanaArgs = append(vamanaArgs, "SEARCH_WINDOW_SIZE", schema.VectorArgs.VamanaOptions.SearchWindowSize) + } + if schema.VectorArgs.VamanaOptions.Epsilon > 0 { + vamanaArgs = append(vamanaArgs, "EPSILON", schema.VectorArgs.VamanaOptions.Epsilon) + } + if schema.VectorArgs.VamanaOptions.TrainingThreshold > 0 { + vamanaArgs = append(vamanaArgs, "TRAINING_THRESHOLD", schema.VectorArgs.VamanaOptions.TrainingThreshold) + } + if schema.VectorArgs.VamanaOptions.ReduceDim > 0 { + vamanaArgs = append(vamanaArgs, "REDUCE", schema.VectorArgs.VamanaOptions.ReduceDim) + } + args = append(args, len(vamanaArgs)) + args = append(args, vamanaArgs...) + } } if schema.GeoShapeFieldType != "" { if schema.FieldType != SearchFieldTypeGeoShape { - panic("FT.CREATE: SCHEMA FieldType GEOSHAPE is required for GeoShapeFieldType") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA FieldType GEOSHAPE is required for GeoShapeFieldType")) + return cmd } args = append(args, schema.GeoShapeFieldType) } @@ -1196,7 +1278,7 @@ func (c cmdable) FTExplainWithArgs(ctx context.Context, index string, query stri // FTExplainCli - Returns the execution plan for a complex query. [Not Implemented] // For more information, see https://redis.io/commands/ft.explaincli/ func (c cmdable) FTExplainCli(ctx context.Context, key, path string) error { - panic("not implemented") + return fmt.Errorf("FTExplainCli is not implemented") } func parseFTInfo(data map[string]interface{}) (FTInfoResult, error) { @@ -1654,7 +1736,13 @@ func parseFTSearch(data []interface{}, noContent, withScores, withPayloads, with if i < len(data) { fields, ok := data[i].([]interface{}) if !ok { - return FTSearchResult{}, fmt.Errorf("invalid document fields format") + if data[i] == proto.Nil || data[i] == nil { + doc.Error = proto.Nil + doc.Fields = map[string]string{} + fields = []interface{}{} + } else { + return FTSearchResult{}, fmt.Errorf("invalid document fields format") + } } for j := 0; j < len(fields); j += 2 { @@ -1751,7 +1839,7 @@ type SearchQuery []interface{} // For more information, please refer to the Redis documentation about [FT.SEARCH]. // // [FT.SEARCH]: (https://redis.io/commands/ft.search/) -func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { +func FTSearchQuery(query string, options *FTSearchOptions) (SearchQuery, error) { queryArgs := []interface{}{query} if options != nil { if options.NoContent { @@ -1831,7 +1919,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { for _, sortBy := range options.SortBy { queryArgs = append(queryArgs, sortBy.FieldName) if sortBy.Asc && sortBy.Desc { - panic("FT.SEARCH: ASC and DESC are mutually exclusive") + return nil, fmt.Errorf("FT.SEARCH: ASC and DESC are mutually exclusive") } if sortBy.Asc { queryArgs = append(queryArgs, "ASC") @@ -1859,7 +1947,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { queryArgs = append(queryArgs, "DIALECT", 2) } } - return queryArgs + return queryArgs, nil } // FTSearchWithArgs - Executes a search query on an index with additional options. @@ -1948,7 +2036,9 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin for _, sortBy := range options.SortBy { args = append(args, sortBy.FieldName) if sortBy.Asc && sortBy.Desc { - panic("FT.SEARCH: ASC and DESC are mutually exclusive") + cmd := newFTSearchCmd(ctx, options, args...) + cmd.SetErr(fmt.Errorf("FT.SEARCH: ASC and DESC are mutually exclusive")) + return cmd } if sortBy.Asc { args = append(args, "ASC") diff --git a/search_test.go b/search_test.go index fdcd0d24..ede29c46 100644 --- a/search_test.go +++ b/search_test.go @@ -38,6 +38,17 @@ func encodeFloat32Vector(vec []float32) []byte { return buf.Bytes() } +func encodeFloat16Vector(vec []float32) []byte { + buf := new(bytes.Buffer) + for _, v := range vec { + // Convert float32 to float16 (16-bit representation) + // This is a simplified conversion - in practice you'd use a proper float16 library + f16 := uint16(v * 1000) // Simple scaling for test purposes + binary.Write(buf, binary.LittleEndian, f16) + } + return buf.Bytes() +} + var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { ctx := context.TODO() var client *redis.Client @@ -819,7 +830,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) It("should return only the base query when options is nil", Label("search", "ftaggregate"), func() { - args := redis.FTAggregateQuery("testQuery", nil) + args, err := redis.FTAggregateQuery("testQuery", nil) + Expect(err).NotTo(HaveOccurred()) Expect(args).To(Equal(redis.AggregateQuery{"testQuery"})) }) @@ -828,7 +840,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Verbatim: true, Scorer: "BM25", } - args := redis.FTAggregateQuery("testQuery", options) + args, err := redis.FTAggregateQuery("testQuery", options) + Expect(err).NotTo(HaveOccurred()) Expect(args[0]).To(Equal("testQuery")) Expect(args).To(ContainElement("VERBATIM")) Expect(args).To(ContainElement("SCORER")) @@ -839,7 +852,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { options := &redis.FTAggregateOptions{ AddScores: true, } - args := redis.FTAggregateQuery("q", options) + args, err := redis.FTAggregateQuery("q", options) + Expect(err).NotTo(HaveOccurred()) Expect(args).To(ContainElement("ADDSCORES")) }) @@ -847,7 +861,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { options := &redis.FTAggregateOptions{ LoadAll: true, } - args := redis.FTAggregateQuery("q", options) + args, err := redis.FTAggregateQuery("q", options) + Expect(err).NotTo(HaveOccurred()) Expect(args).To(ContainElement("LOAD")) Expect(args).To(ContainElement("*")) }) @@ -859,7 +874,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { {Field: "field2"}, }, } - args := redis.FTAggregateQuery("q", options) + args, err := redis.FTAggregateQuery("q", options) + Expect(err).NotTo(HaveOccurred()) // Verify LOAD options related arguments Expect(args).To(ContainElement("LOAD")) // Check that field names and aliases are present @@ -872,7 +888,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { options := &redis.FTAggregateOptions{ Timeout: 500, } - args := redis.FTAggregateQuery("q", options) + args, err := redis.FTAggregateQuery("q", options) + Expect(err).NotTo(HaveOccurred()) Expect(args).To(ContainElement("TIMEOUT")) found := false for i, a := range args { @@ -1745,6 +1762,631 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(nanCount).To(Equal(2)) }) + It("should FTCreate VECTOR with VAMANA algorithm - basic", Label("search", "ftcreate"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 2, + DistanceMetric: "L2", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "a", "v", "aaaaaaaa") + client.HSet(ctx, "b", "v", "aaaabaaa") + client.HSet(ctx, "c", "v", "aaaaabaa") + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, + DialectVersion: 2, + Params: map[string]interface{}{"vec": "aaaaaaaa"}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("a")) + Expect(res.Docs[0].Fields["__v_score"]).To(BeEquivalentTo("0")) + }) + + It("should FTCreate VECTOR with VAMANA algorithm - with compression", Label("search", "ftcreate"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT16", + Dim: 256, + DistanceMetric: "COSINE", + Compression: "LVQ8", + TrainingThreshold: 10240, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + }) + + It("should FTCreate VECTOR with VAMANA algorithm - advanced parameters", Label("search", "ftcreate"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 512, + DistanceMetric: "IP", + Compression: "LVQ8", + ConstructionWindowSize: 300, + GraphMaxDegree: 128, + SearchWindowSize: 20, + Epsilon: 0.02, + TrainingThreshold: 20480, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + }) + + It("should fail FTCreate VECTOR with VAMANA - missing required parameters", Label("search", "ftcreate"), func() { + // Test missing Type + cmd := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{ + Dim: 2, + DistanceMetric: "L2", + }}}) + Expect(cmd.Err()).To(HaveOccurred()) + Expect(cmd.Err().Error()).To(ContainSubstring("Type, Dim and DistanceMetric are required for VECTOR VAMANA")) + + // Test missing Dim + cmd = client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{ + Type: "FLOAT32", + DistanceMetric: "L2", + }}}) + Expect(cmd.Err()).To(HaveOccurred()) + Expect(cmd.Err().Error()).To(ContainSubstring("Type, Dim and DistanceMetric are required for VECTOR VAMANA")) + + // Test missing DistanceMetric + cmd = client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 2, + }}}) + Expect(cmd.Err()).To(HaveOccurred()) + Expect(cmd.Err().Error()).To(ContainSubstring("Type, Dim and DistanceMetric are required for VECTOR VAMANA")) + }) + + It("should fail FTCreate VECTOR with multiple vector options", Label("search", "ftcreate"), func() { + // Test VAMANA + HNSW + cmd := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{ + VamanaOptions: &redis.FTVamanaOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"}, + HNSWOptions: &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"}, + }}) + Expect(cmd.Err()).To(HaveOccurred()) + Expect(cmd.Err().Error()).To(ContainSubstring("VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions")) + + // Test VAMANA + FLAT + cmd = client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{ + VamanaOptions: &redis.FTVamanaOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"}, + FlatOptions: &redis.FTFlatOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"}, + }}) + Expect(cmd.Err()).To(HaveOccurred()) + Expect(cmd.Err().Error()).To(ContainSubstring("VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions")) + }) + + It("should test VAMANA L2 distance metric", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 3, + DistanceMetric: "L2", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + // L2 distance test vectors + vectors := [][]float32{ + {1.0, 0.0, 0.0}, + {2.0, 0.0, 0.0}, + {0.0, 1.0, 0.0}, + {5.0, 0.0, 0.0}, + } + + for i, vec := range vectors { + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "score", Asc: true}}, + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 3 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(3)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA COSINE distance metric", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 3, + DistanceMetric: "COSINE", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := [][]float32{ + {1.0, 0.0, 0.0}, + {0.707, 0.707, 0.0}, + {0.0, 1.0, 0.0}, + {-1.0, 0.0, 0.0}, + } + + for i, vec := range vectors { + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "score", Asc: true}}, + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 3 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(3)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA IP distance metric", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 3, + DistanceMetric: "IP", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := [][]float32{ + {1.0, 2.0, 3.0}, + {2.0, 1.0, 1.0}, + {3.0, 3.0, 3.0}, + {0.1, 0.1, 0.1}, + } + + for i, vec := range vectors { + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "score", Asc: true}}, + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 3 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(3)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc2")) + }) + + It("should test VAMANA basic functionality", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 4, + DistanceMetric: "L2", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := [][]float32{ + {1.0, 2.0, 3.0, 4.0}, + {2.0, 3.0, 4.0, 5.0}, + {3.0, 4.0, 5.0, 6.0}, + {10.0, 11.0, 12.0, 13.0}, + } + + for i, vec := range vectors { + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, + DialectVersion: 2, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 3 @v $vec]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(3)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) // Should be closest to itself + Expect(res.Docs[0].Fields["__v_score"]).To(BeEquivalentTo("0")) + }) + + It("should test VAMANA FLOAT16 type", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT16", + Dim: 4, + DistanceMetric: "L2", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := [][]float32{ + {1.5, 2.5, 3.5, 4.5}, + {2.5, 3.5, 4.5, 5.5}, + {3.5, 4.5, 5.5, 6.5}, + } + + for i, vec := range vectors { + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat16Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat16Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(2)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA FLOAT32 type", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 4, + DistanceMetric: "L2", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := [][]float32{ + {1.0, 2.0, 3.0, 4.0}, + {2.0, 3.0, 4.0, 5.0}, + {3.0, 4.0, 5.0, 6.0}, + } + + for i, vec := range vectors { + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(2)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA with default dialect", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 2, + DistanceMetric: "L2", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "a", "v", "aaaaaaaa") + client.HSet(ctx, "b", "v", "aaaabaaa") + client.HSet(ctx, "c", "v", "aaaaabaa") + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, + Params: map[string]interface{}{"vec": "aaaaaaaa"}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(2)) + }) + + It("should test VAMANA with LVQ8 compression", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 8, + DistanceMetric: "L2", + Compression: "LVQ8", + TrainingThreshold: 1024, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := make([][]float32, 20) + for i := 0; i < 20; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + vectors[i] = vec + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(5)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA compression with both vector types", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + + // Test FLOAT16 with LVQ8 + vamanaOptions16 := &redis.FTVamanaOptions{ + Type: "FLOAT16", + Dim: 8, + DistanceMetric: "L2", + Compression: "LVQ8", + TrainingThreshold: 1024, + } + val, err := client.FTCreate(ctx, "idx16", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v16", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions16}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx16") + + // Test FLOAT32 with LVQ8 + vamanaOptions32 := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 8, + DistanceMetric: "L2", + Compression: "LVQ8", + TrainingThreshold: 1024, + } + val, err = client.FTCreate(ctx, "idx32", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v32", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions32}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx32") + + // Add data to both indices + for i := 0; i < 15; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + client.HSet(ctx, fmt.Sprintf("doc16_%d", i), "v16", encodeFloat16Vector(vec)) + client.HSet(ctx, fmt.Sprintf("doc32_%d", i), "v32", encodeFloat32Vector(vec)) + } + + queryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0} + + // Test FLOAT16 index + searchOptions16 := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat16Vector(queryVec)}, + } + res16, err := client.FTSearchWithArgs(ctx, "idx16", "*=>[KNN 3 @v16 $vec as score]", searchOptions16).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res16.Total).To(BeEquivalentTo(3)) + + // Test FLOAT32 index + searchOptions32 := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(queryVec)}, + } + res32, err := client.FTSearchWithArgs(ctx, "idx32", "*=>[KNN 3 @v32 $vec as score]", searchOptions32).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res32.Total).To(BeEquivalentTo(3)) + }) + + It("should test VAMANA construction window size", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 6, + DistanceMetric: "L2", + ConstructionWindowSize: 300, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := make([][]float32, 20) + for i := 0; i < 20; i++ { + vec := make([]float32, 6) + for j := 0; j < 6; j++ { + vec[j] = float32(i + j) + } + vectors[i] = vec + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(5)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA graph max degree", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 6, + DistanceMetric: "COSINE", + GraphMaxDegree: 64, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := make([][]float32, 25) + for i := 0; i < 25; i++ { + vec := make([]float32, 6) + for j := 0; j < 6; j++ { + vec[j] = float32(i + j) + } + vectors[i] = vec + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 6 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(6)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA search window size", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 6, + DistanceMetric: "L2", + SearchWindowSize: 20, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := make([][]float32, 30) + for i := 0; i < 30; i++ { + vec := make([]float32, 6) + for j := 0; j < 6; j++ { + vec[j] = float32(i + j) + } + vectors[i] = vec + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 8 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(8)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA all advanced parameters", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 8, + DistanceMetric: "L2", + Compression: "LVQ8", + ConstructionWindowSize: 200, + GraphMaxDegree: 32, + SearchWindowSize: 15, + Epsilon: 0.01, + TrainingThreshold: 1024, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := make([][]float32, 15) + for i := 0; i < 15; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + vectors[i] = vec + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(5)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + It("should fail when using a non-zero offset with a zero limit", Label("search", "ftsearch"), func() { SkipBeforeRedisVersion(7.9, "requires Redis 8.x") val, err := client.FTCreate(ctx, "testIdx", &redis.FTCreateOptions{}, &redis.FieldSchema{ diff --git a/stream_commands.go b/stream_commands.go index 6d7b2292..4b84e00f 100644 --- a/stream_commands.go +++ b/stream_commands.go @@ -7,7 +7,9 @@ import ( type StreamCmdable interface { XAdd(ctx context.Context, a *XAddArgs) *StringCmd + XAckDel(ctx context.Context, stream string, group string, mode string, ids ...string) *SliceCmd XDel(ctx context.Context, stream string, ids ...string) *IntCmd + XDelEx(ctx context.Context, stream string, mode string, ids ...string) *SliceCmd XLen(ctx context.Context, stream string) *IntCmd XRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd XRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd @@ -31,8 +33,12 @@ type StreamCmdable interface { XAutoClaimJustID(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimJustIDCmd XTrimMaxLen(ctx context.Context, key string, maxLen int64) *IntCmd XTrimMaxLenApprox(ctx context.Context, key string, maxLen, limit int64) *IntCmd + XTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *IntCmd + XTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *IntCmd XTrimMinID(ctx context.Context, key string, minID string) *IntCmd XTrimMinIDApprox(ctx context.Context, key string, minID string, limit int64) *IntCmd + XTrimMinIDMode(ctx context.Context, key string, minID string, mode string) *IntCmd + XTrimMinIDApproxMode(ctx context.Context, key string, minID string, limit int64, mode string) *IntCmd XInfoGroups(ctx context.Context, key string) *XInfoGroupsCmd XInfoStream(ctx context.Context, key string) *XInfoStreamCmd XInfoStreamFull(ctx context.Context, key string, count int) *XInfoStreamFullCmd @@ -54,6 +60,7 @@ type XAddArgs struct { // Approx causes MaxLen and MinID to use "~" matcher (instead of "="). Approx bool Limit int64 + Mode string ID string Values interface{} } @@ -81,6 +88,11 @@ func (c cmdable) XAdd(ctx context.Context, a *XAddArgs) *StringCmd { if a.Limit > 0 { args = append(args, "limit", a.Limit) } + + if a.Mode != "" { + args = append(args, a.Mode) + } + if a.ID != "" { args = append(args, a.ID) } else { @@ -93,6 +105,16 @@ func (c cmdable) XAdd(ctx context.Context, a *XAddArgs) *StringCmd { return cmd } +func (c cmdable) XAckDel(ctx context.Context, stream string, group string, mode string, ids ...string) *SliceCmd { + args := []interface{}{"xackdel", stream, group, mode, "ids", len(ids)} + for _, id := range ids { + args = append(args, id) + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd { args := []interface{}{"xdel", stream} for _, id := range ids { @@ -103,6 +125,16 @@ func (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd return cmd } +func (c cmdable) XDelEx(ctx context.Context, stream string, mode string, ids ...string) *SliceCmd { + args := []interface{}{"xdelex", stream, mode, "ids", len(ids)} + for _, id := range ids { + args = append(args, id) + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) XLen(ctx context.Context, stream string) *IntCmd { cmd := NewIntCmd(ctx, "xlen", stream) _ = c(ctx, cmd) @@ -375,6 +407,8 @@ func xClaimArgs(a *XClaimArgs) []interface{} { return args } +// TODO: refactor xTrim, xTrimMode and the wrappers over the functions + // xTrim If approx is true, add the "~" parameter, otherwise it is the default "=" (redis default). // example: // @@ -418,6 +452,42 @@ func (c cmdable) XTrimMinIDApprox(ctx context.Context, key string, minID string, return c.xTrim(ctx, key, "minid", true, minID, limit) } +func (c cmdable) xTrimMode( + ctx context.Context, key, strategy string, + approx bool, threshold interface{}, limit int64, + mode string, +) *IntCmd { + args := make([]interface{}, 0, 7) + args = append(args, "xtrim", key, strategy) + if approx { + args = append(args, "~") + } + args = append(args, threshold) + if limit > 0 { + args = append(args, "limit", limit) + } + args = append(args, mode) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "maxlen", false, maxLen, 0, mode) +} + +func (c cmdable) XTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "maxlen", true, maxLen, limit, mode) +} + +func (c cmdable) XTrimMinIDMode(ctx context.Context, key string, minID string, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "minid", false, minID, 0, mode) +} + +func (c cmdable) XTrimMinIDApproxMode(ctx context.Context, key string, minID string, limit int64, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "minid", true, minID, limit, mode) +} + func (c cmdable) XInfoConsumers(ctx context.Context, key string, group string) *XInfoConsumersCmd { cmd := NewXInfoConsumersCmd(ctx, key, group) _ = c(ctx, cmd) diff --git a/vectorset_commands.go b/vectorset_commands.go index 2bd9e221..96be1af1 100644 --- a/vectorset_commands.go +++ b/vectorset_commands.go @@ -287,8 +287,7 @@ type VSimArgs struct { FilterEF int64 Truth bool NoThread bool - // The `VSim` command in Redis has the option, by the doc in Redis.io don't have. - // Epsilon float64 + Epsilon float64 } func (v VSimArgs) appendArgs(args []any) []any { @@ -310,13 +309,13 @@ func (v VSimArgs) appendArgs(args []any) []any { if v.NoThread { args = append(args, "nothread") } - // if v.Epsilon > 0 { - // args = append(args, "Epsilon", v.Epsilon) - // } + if v.Epsilon > 0 { + args = append(args, "Epsilon", v.Epsilon) + } return args } -// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [COUNT num] +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [COUNT num] [EPSILON delta] // [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` // note: the API is experimental and may be subject to change. func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *StringSliceCmd { @@ -331,7 +330,7 @@ func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simAr return cmd } -// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [COUNT num] +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [COUNT num] [EPSILON delta] // [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` // note: the API is experimental and may be subject to change. func (c cmdable) VSimWithArgsWithScores(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *VectorScoreSliceCmd {