1
0
mirror of https://github.com/redis/go-redis.git synced 2025-09-11 18:10:43 +03:00

feat(search): Add VAMANA vector type to RediSearch (#3449)

* Add VAMANA vector type to redisearch

* Change to svs-vamana vector type && remove panics from search module

* fix tests

* fix tests

* fix tests
This commit is contained in:
ofekshenawa
2025-08-01 15:41:40 +03:00
committed by GitHub
parent 9177a76d33
commit 3c85d090fe
2 changed files with 753 additions and 28 deletions

View File

@@ -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
}
@@ -499,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 {
@@ -515,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", "*")
@@ -571,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")
@@ -616,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) {
@@ -718,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", "*")
@@ -771,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")
@@ -919,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))
@@ -970,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 != "" {
@@ -984,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,
@@ -1011,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,
@@ -1036,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)
}
@@ -1197,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) {
@@ -1758,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 {
@@ -1838,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")
@@ -1866,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.
@@ -1955,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")

View File

@@ -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{