From 1910d5f0140cff7638b0a2ae707f177c121e98b4 Mon Sep 17 00:00:00 2001 From: ofekshenawa Date: Mon, 28 Jul 2025 11:23:46 +0300 Subject: [PATCH 1/5] Add VAMANA vector type to redisearch --- search_commands.go | 67 ++++- search_test.go | 637 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 700 insertions(+), 4 deletions(-) diff --git a/search_commands.go b/search_commands.go index b31baaa760..a0c4c7d90f 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 } @@ -985,8 +999,19 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp if schema.FieldType != SearchFieldTypeVector { panic("FT.CREATE: SCHEMA FieldType VECTOR is required for VectorArgs") } - 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 { + panic("FT.CREATE: SCHEMA VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions") } if schema.VectorArgs.FlatOptions != nil { args = append(args, "FLAT") @@ -1035,6 +1060,40 @@ 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, "VAMANA") + if schema.VectorArgs.VamanaOptions.Type == "" || schema.VectorArgs.VamanaOptions.Dim == 0 || schema.VectorArgs.VamanaOptions.DistanceMetric == "" { + panic("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR VAMANA") + } + 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 { diff --git a/search_test.go b/search_test.go index fdcd0d24b7..aea68efaff 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 @@ -1745,6 +1756,632 @@ 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 + Expect(func() { + client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{ + Dim: 2, + DistanceMetric: "L2", + }}}) + }).To(Panic()) + + // Test missing Dim + Expect(func() { + client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{ + Type: "FLOAT32", + DistanceMetric: "L2", + }}}) + }).To(Panic()) + + // Test missing DistanceMetric + Expect(func() { + client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 2, + }}}) + }).To(Panic()) + }) + + It("should fail FTCreate VECTOR with multiple vector options", Label("search", "ftcreate"), func() { + // Test VAMANA + HNSW + Expect(func() { + 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"}, + }}) + }).To(Panic()) + + // Test VAMANA + FLAT + Expect(func() { + 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"}, + }}) + }).To(Panic()) + }) + + 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: "__v_score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "__v_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: "__v_score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "__v_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: "__v_score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "__v_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: 512, + ReduceDim: 4, + } + 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{ From 8dde00a8926209a03f4e52cb10b77a3e112ae579 Mon Sep 17 00:00:00 2001 From: ofekshenawa Date: Fri, 1 Aug 2025 13:13:07 +0300 Subject: [PATCH 2/5] Change to svs-vamana vector type && remove panics from search module --- search_commands.go | 66 +++++++++++++++++++++---------- search_test.go | 98 ++++++++++++++++++++++++---------------------- 2 files changed, 97 insertions(+), 67 deletions(-) diff --git a/search_commands.go b/search_commands.go index a0c4c7d90f..a533248067 100644 --- a/search_commands.go +++ b/search_commands.go @@ -512,7 +512,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 { @@ -528,7 +528,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", "*") @@ -584,7 +584,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") @@ -629,7 +629,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery queryArgs = append(queryArgs, "DIALECT", 2) } } - return queryArgs + return queryArgs, nil } func ProcessAggregateResult(data []interface{}) (*FTAggregateResult, error) { @@ -731,7 +731,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", "*") @@ -784,7 +786,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") @@ -932,7 +936,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)) @@ -983,12 +989,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 != "" { @@ -997,7 +1007,9 @@ 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 } // Check mutual exclusivity of vector options optionCount := 0 @@ -1011,12 +1023,16 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp optionCount++ } if optionCount != 1 { - panic("FT.CREATE: SCHEMA VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions") + 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, @@ -1035,7 +1051,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, @@ -1061,9 +1079,11 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp args = append(args, hnswArgs...) } if schema.VectorArgs.VamanaOptions != nil { - args = append(args, "VAMANA") + args = append(args, "SVS-VAMANA") if schema.VectorArgs.VamanaOptions.Type == "" || schema.VectorArgs.VamanaOptions.Dim == 0 || schema.VectorArgs.VamanaOptions.DistanceMetric == "" { - panic("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR VAMANA") + 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, @@ -1097,7 +1117,9 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp } 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) } @@ -1255,7 +1277,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) { @@ -1810,7 +1832,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 { @@ -1890,7 +1912,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") @@ -1918,7 +1940,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. @@ -2007,7 +2029,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 aea68efaff..733cdb1ea5 100644 --- a/search_test.go +++ b/search_test.go @@ -830,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"})) }) @@ -839,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")) @@ -850,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")) }) @@ -858,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("*")) }) @@ -870,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 @@ -883,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 { @@ -1826,56 +1832,56 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { It("should fail FTCreate VECTOR with VAMANA - missing required parameters", Label("search", "ftcreate"), func() { // Test missing Type - Expect(func() { - client.FTCreate(ctx, "idx1", - &redis.FTCreateOptions{}, - &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{ - Dim: 2, - DistanceMetric: "L2", - }}}) - }).To(Panic()) + 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 - Expect(func() { - client.FTCreate(ctx, "idx1", - &redis.FTCreateOptions{}, - &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{ - Type: "FLOAT32", - DistanceMetric: "L2", - }}}) - }).To(Panic()) + 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 - Expect(func() { - client.FTCreate(ctx, "idx1", - &redis.FTCreateOptions{}, - &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{ - Type: "FLOAT32", - Dim: 2, - }}}) - }).To(Panic()) + 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 - Expect(func() { - 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"}, - }}) - }).To(Panic()) + 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 - Expect(func() { - 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"}, - }}) - }).To(Panic()) + 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() { From 4ce2160c7898dfed378512764057096194e40308 Mon Sep 17 00:00:00 2001 From: ofekshenawa Date: Fri, 1 Aug 2025 13:33:31 +0300 Subject: [PATCH 3/5] fix tests --- search_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/search_test.go b/search_test.go index 733cdb1ea5..0df88dfe8a 100644 --- a/search_test.go +++ b/search_test.go @@ -1911,8 +1911,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { } searchOptions := &redis.FTSearchOptions{ - Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, - SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, + Return: []redis.FTSearchReturn{{FieldName: "score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "score", Asc: true}}, DialectVersion: 2, NoContent: true, Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, @@ -1949,8 +1949,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { } searchOptions := &redis.FTSearchOptions{ - Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, - SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, + Return: []redis.FTSearchReturn{{FieldName: "score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "score", Asc: true}}, DialectVersion: 2, NoContent: true, Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, @@ -1987,8 +1987,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { } searchOptions := &redis.FTSearchOptions{ - Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, - SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, + Return: []redis.FTSearchReturn{{FieldName: "score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "score", Asc: true}}, DialectVersion: 2, NoContent: true, Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, @@ -2357,7 +2357,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { GraphMaxDegree: 32, SearchWindowSize: 15, Epsilon: 0.01, - TrainingThreshold: 512, + TrainingThreshold: 1024, ReduceDim: 4, } val, err := client.FTCreate(ctx, "idx1", From c106abc5a3e60f0615fb0d28f13f43c9f4ce9e78 Mon Sep 17 00:00:00 2001 From: ofekshenawa Date: Fri, 1 Aug 2025 14:08:05 +0300 Subject: [PATCH 4/5] fix tests --- search_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/search_test.go b/search_test.go index 0df88dfe8a..d4a84d89f8 100644 --- a/search_test.go +++ b/search_test.go @@ -2352,7 +2352,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Type: "FLOAT32", Dim: 8, DistanceMetric: "L2", - Compression: "LVQ8", + Compression: "LeanVec", ConstructionWindowSize: 200, GraphMaxDegree: 32, SearchWindowSize: 15, From 4b23a992c6ddb926b5a30f6105c30aa56a3ba34c Mon Sep 17 00:00:00 2001 From: ofekshenawa Date: Fri, 1 Aug 2025 15:15:39 +0300 Subject: [PATCH 5/5] fix tests --- search_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/search_test.go b/search_test.go index d4a84d89f8..ede29c4613 100644 --- a/search_test.go +++ b/search_test.go @@ -2352,13 +2352,12 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Type: "FLOAT32", Dim: 8, DistanceMetric: "L2", - Compression: "LeanVec", + Compression: "LVQ8", ConstructionWindowSize: 200, GraphMaxDegree: 32, SearchWindowSize: 15, Epsilon: 0.01, TrainingThreshold: 1024, - ReduceDim: 4, } val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{},