Skip to content

Add VAMANA vector type to RediSearch #3449

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 105 additions & 22 deletions search_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ type FieldSchema struct {
}

type FTVectorArgs struct {
FlatOptions *FTFlatOptions
HNSWOptions *FTHNSWOptions
FlatOptions *FTFlatOptions
HNSWOptions *FTHNSWOptions
VamanaOptions *FTVamanaOptions
}

type FTFlatOptions struct {
Expand All @@ -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
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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", "*")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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", "*")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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 != "" {
Expand All @@ -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
}
// 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 schema.VectorArgs.FlatOptions != nil && schema.VectorArgs.HNSWOptions != nil {
panic("FT.CREATE: SCHEMA VectorArgs FlatOptions and HNSWOptions are mutually exclusive")
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,
Expand All @@ -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,
Expand All @@ -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)
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Expand Down
Loading
Loading