mirror of
https://github.com/redis/go-redis.git
synced 2025-09-05 20:24:00 +03:00
Merge branch 'master' of https://github.com/redis/go-redis into add-configurable-conn-buffer-sizes
This commit is contained in:
2
.github/actions/run-tests/action.yml
vendored
2
.github/actions/run-tests/action.yml
vendored
@@ -25,7 +25,7 @@ runs:
|
|||||||
|
|
||||||
# Mapping of redis version to redis testing containers
|
# Mapping of redis version to redis testing containers
|
||||||
declare -A redis_version_mapping=(
|
declare -A redis_version_mapping=(
|
||||||
["8.2.x"]="8.2-RC1-pre"
|
["8.2.x"]="8.2-rc2-pre"
|
||||||
["8.0.x"]="8.0.2"
|
["8.0.x"]="8.0.2"
|
||||||
["7.4.x"]="rs-7.4.0-v5"
|
["7.4.x"]="rs-7.4.0-v5"
|
||||||
["7.2.x"]="rs-7.2.0-v17"
|
["7.2.x"]="rs-7.2.0-v17"
|
||||||
|
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -2,9 +2,9 @@ name: Go
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master, v9, v9.7, v9.8]
|
branches: [master, v9, v9.7, v9.8, 'ndyakov/*', 'ofekshenawa/*', 'htemelski-redis/*', 'ce/*']
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, v9, v9.7, v9.8]
|
branches: [master, v9, v9.7, v9.8, 'ndyakov/*', 'ofekshenawa/*', 'htemelski-redis/*', 'ce/*']
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
|
|
||||||
# Mapping of redis version to redis testing containers
|
# Mapping of redis version to redis testing containers
|
||||||
declare -A redis_version_mapping=(
|
declare -A redis_version_mapping=(
|
||||||
["8.2.x"]="8.2-RC1-pre"
|
["8.2.x"]="8.2-rc2-pre"
|
||||||
["8.0.x"]="8.0.2"
|
["8.0.x"]="8.0.2"
|
||||||
["7.4.x"]="rs-7.4.0-v5"
|
["7.4.x"]="rs-7.4.0-v5"
|
||||||
)
|
)
|
||||||
|
@@ -6169,6 +6169,34 @@ var _ = Describe("Commands", func() {
|
|||||||
Expect(n).To(Equal(int64(3)))
|
Expect(n).To(Equal(int64(3)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("should XTrimMaxLenMode", func() {
|
||||||
|
SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images")
|
||||||
|
n, err := client.XTrimMaxLenMode(ctx, "stream", 0, "KEEPREF").Result()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(n).To(BeNumerically(">=", 0))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should XTrimMaxLenApproxMode", func() {
|
||||||
|
SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images")
|
||||||
|
n, err := client.XTrimMaxLenApproxMode(ctx, "stream", 0, 0, "KEEPREF").Result()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(n).To(BeNumerically(">=", 0))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should XTrimMinIDMode", func() {
|
||||||
|
SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images")
|
||||||
|
n, err := client.XTrimMinIDMode(ctx, "stream", "4-0", "KEEPREF").Result()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(n).To(BeNumerically(">=", 0))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should XTrimMinIDApproxMode", func() {
|
||||||
|
SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images")
|
||||||
|
n, err := client.XTrimMinIDApproxMode(ctx, "stream", "4-0", 0, "KEEPREF").Result()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(n).To(BeNumerically(">=", 0))
|
||||||
|
})
|
||||||
|
|
||||||
It("should XAdd", func() {
|
It("should XAdd", func() {
|
||||||
id, err := client.XAdd(ctx, &redis.XAddArgs{
|
id, err := client.XAdd(ctx, &redis.XAddArgs{
|
||||||
Stream: "stream",
|
Stream: "stream",
|
||||||
@@ -6222,6 +6250,37 @@ var _ = Describe("Commands", func() {
|
|||||||
Expect(n).To(Equal(int64(3)))
|
Expect(n).To(Equal(int64(3)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("should XAckDel", func() {
|
||||||
|
SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images")
|
||||||
|
// First, create a consumer group
|
||||||
|
err := client.XGroupCreate(ctx, "stream", "testgroup", "0").Err()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Read messages to create pending entries
|
||||||
|
_, err = client.XReadGroup(ctx, &redis.XReadGroupArgs{
|
||||||
|
Group: "testgroup",
|
||||||
|
Consumer: "testconsumer",
|
||||||
|
Streams: []string{"stream", ">"},
|
||||||
|
}).Result()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Test XAckDel with KEEPREF mode
|
||||||
|
n, err := client.XAckDel(ctx, "stream", "testgroup", "KEEPREF", "1-0", "2-0").Result()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(n).To(HaveLen(2))
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
client.XGroupDestroy(ctx, "stream", "testgroup")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should XDelEx", func() {
|
||||||
|
SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images")
|
||||||
|
// Test XDelEx with KEEPREF mode
|
||||||
|
n, err := client.XDelEx(ctx, "stream", "KEEPREF", "1-0", "2-0").Result()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(n).To(HaveLen(2))
|
||||||
|
})
|
||||||
|
|
||||||
It("should XLen", func() {
|
It("should XLen", func() {
|
||||||
n, err := client.XLen(ctx, "stream").Result()
|
n, err := client.XLen(ctx, "stream").Result()
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
@@ -199,11 +199,11 @@ func ExampleClient_geoindex() {
|
|||||||
// OK
|
// OK
|
||||||
// OK
|
// OK
|
||||||
// OK
|
// OK
|
||||||
// {1 [{product:46885 <nil> <nil> <nil> map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}]}]}
|
// {1 [{product:46885 <nil> <nil> <nil> map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}] <nil>}]}
|
||||||
// OK
|
// OK
|
||||||
// OK
|
// OK
|
||||||
// OK
|
// OK
|
||||||
// OK
|
// OK
|
||||||
// OK
|
// OK
|
||||||
// {1 [{shape:4 <nil> <nil> <nil> map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]]}]}
|
// {1 [{shape:4 <nil> <nil> <nil> map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]] <nil>}]}
|
||||||
}
|
}
|
||||||
|
@@ -219,7 +219,7 @@ func ExampleClient_search_json() {
|
|||||||
// STEP_END
|
// STEP_END
|
||||||
|
|
||||||
// Output:
|
// Output:
|
||||||
// {1 [{user:3 <nil> <nil> <nil> map[$:{"age":35,"city":"Tel Aviv","email":"paul.zamir@example.com","name":"Paul Zamir"}]}]}
|
// {1 [{user:3 <nil> <nil> <nil> map[$:{"age":35,"city":"Tel Aviv","email":"paul.zamir@example.com","name":"Paul Zamir"}] <nil>}]}
|
||||||
// London
|
// London
|
||||||
// Tel Aviv
|
// Tel Aviv
|
||||||
// 0
|
// 0
|
||||||
@@ -329,5 +329,5 @@ func ExampleClient_search_hash() {
|
|||||||
// STEP_END
|
// STEP_END
|
||||||
|
|
||||||
// Output:
|
// Output:
|
||||||
// {1 [{huser:3 <nil> <nil> <nil> map[age:35 city:Tel Aviv email:paul.zamir@example.com name:Paul Zamir]}]}
|
// {1 [{huser:3 <nil> <nil> <nil> map[age:35 city:Tel Aviv email:paul.zamir@example.com name:Paul Zamir] <nil>}]}
|
||||||
}
|
}
|
||||||
|
@@ -257,6 +257,6 @@ func ExampleClient_search_qs() {
|
|||||||
|
|
||||||
// Output:
|
// Output:
|
||||||
// Documents found: 10
|
// Documents found: 10
|
||||||
// {1 [{bicycle:0 <nil> <nil> <nil> map[$:{"brand":"Velorim","condition":"new","description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","model":"Jigger","price":270}]}]}
|
// {1 [{bicycle:0 <nil> <nil> <nil> map[$:{"brand":"Velorim","condition":"new","description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","model":"Jigger","price":270}] <nil>}]}
|
||||||
// {1 [{bicycle:4 <nil> <nil> <nil> map[$:{"brand":"Noka Bikes","condition":"used","description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","model":"Kahuna","price":3200}]}]}
|
// {1 [{bicycle:4 <nil> <nil> <nil> map[$:{"brand":"Noka Bikes","condition":"used","description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","model":"Kahuna","price":3200}] <nil>}]}
|
||||||
}
|
}
|
||||||
|
@@ -82,6 +82,7 @@ type FieldSchema struct {
|
|||||||
type FTVectorArgs struct {
|
type FTVectorArgs struct {
|
||||||
FlatOptions *FTFlatOptions
|
FlatOptions *FTFlatOptions
|
||||||
HNSWOptions *FTHNSWOptions
|
HNSWOptions *FTHNSWOptions
|
||||||
|
VamanaOptions *FTVamanaOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
type FTFlatOptions struct {
|
type FTFlatOptions struct {
|
||||||
@@ -103,6 +104,19 @@ type FTHNSWOptions struct {
|
|||||||
Epsilon float64
|
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 {
|
type FTDropIndexOptions struct {
|
||||||
DeleteDocs bool
|
DeleteDocs bool
|
||||||
}
|
}
|
||||||
@@ -474,6 +488,7 @@ type Document struct {
|
|||||||
Payload *string
|
Payload *string
|
||||||
SortKey *string
|
SortKey *string
|
||||||
Fields map[string]string
|
Fields map[string]string
|
||||||
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
type AggregateQuery []interface{}
|
type AggregateQuery []interface{}
|
||||||
@@ -498,7 +513,7 @@ func (c cmdable) FTAggregate(ctx context.Context, index string, query string) *M
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery {
|
func FTAggregateQuery(query string, options *FTAggregateOptions) (AggregateQuery, error) {
|
||||||
queryArgs := []interface{}{query}
|
queryArgs := []interface{}{query}
|
||||||
if options != nil {
|
if options != nil {
|
||||||
if options.Verbatim {
|
if options.Verbatim {
|
||||||
@@ -514,7 +529,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery
|
|||||||
}
|
}
|
||||||
|
|
||||||
if options.LoadAll && options.Load != nil {
|
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 {
|
if options.LoadAll {
|
||||||
queryArgs = append(queryArgs, "LOAD", "*")
|
queryArgs = append(queryArgs, "LOAD", "*")
|
||||||
@@ -570,7 +585,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery
|
|||||||
for _, sortBy := range options.SortBy {
|
for _, sortBy := range options.SortBy {
|
||||||
sortByOptions = append(sortByOptions, sortBy.FieldName)
|
sortByOptions = append(sortByOptions, sortBy.FieldName)
|
||||||
if sortBy.Asc && sortBy.Desc {
|
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 {
|
if sortBy.Asc {
|
||||||
sortByOptions = append(sortByOptions, "ASC")
|
sortByOptions = append(sortByOptions, "ASC")
|
||||||
@@ -615,7 +630,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery
|
|||||||
queryArgs = append(queryArgs, "DIALECT", 2)
|
queryArgs = append(queryArgs, "DIALECT", 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return queryArgs
|
return queryArgs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProcessAggregateResult(data []interface{}) (*FTAggregateResult, error) {
|
func ProcessAggregateResult(data []interface{}) (*FTAggregateResult, error) {
|
||||||
@@ -717,7 +732,9 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st
|
|||||||
args = append(args, "ADDSCORES")
|
args = append(args, "ADDSCORES")
|
||||||
}
|
}
|
||||||
if options.LoadAll && options.Load != nil {
|
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 {
|
if options.LoadAll {
|
||||||
args = append(args, "LOAD", "*")
|
args = append(args, "LOAD", "*")
|
||||||
@@ -770,7 +787,9 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st
|
|||||||
for _, sortBy := range options.SortBy {
|
for _, sortBy := range options.SortBy {
|
||||||
sortByOptions = append(sortByOptions, sortBy.FieldName)
|
sortByOptions = append(sortByOptions, sortBy.FieldName)
|
||||||
if sortBy.Asc && sortBy.Desc {
|
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 {
|
if sortBy.Asc {
|
||||||
sortByOptions = append(sortByOptions, "ASC")
|
sortByOptions = append(sortByOptions, "ASC")
|
||||||
@@ -918,7 +937,9 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp
|
|||||||
args = append(args, "ON", "JSON")
|
args = append(args, "ON", "JSON")
|
||||||
}
|
}
|
||||||
if options.OnHash && options.OnJSON {
|
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 {
|
if options.Prefix != nil {
|
||||||
args = append(args, "PREFIX", len(options.Prefix))
|
args = append(args, "PREFIX", len(options.Prefix))
|
||||||
@@ -969,12 +990,16 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if schema == nil {
|
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")
|
args = append(args, "SCHEMA")
|
||||||
for _, schema := range schema {
|
for _, schema := range schema {
|
||||||
if schema.FieldName == "" || schema.FieldType == SearchFieldTypeInvalid {
|
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)
|
args = append(args, schema.FieldName)
|
||||||
if schema.As != "" {
|
if schema.As != "" {
|
||||||
@@ -983,15 +1008,32 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp
|
|||||||
args = append(args, schema.FieldType.String())
|
args = append(args, schema.FieldType.String())
|
||||||
if schema.VectorArgs != nil {
|
if schema.VectorArgs != nil {
|
||||||
if schema.FieldType != SearchFieldTypeVector {
|
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 {
|
// Check mutual exclusivity of vector options
|
||||||
panic("FT.CREATE: SCHEMA VectorArgs FlatOptions and HNSWOptions are mutually exclusive")
|
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 {
|
if schema.VectorArgs.FlatOptions != nil {
|
||||||
args = append(args, "FLAT")
|
args = append(args, "FLAT")
|
||||||
if schema.VectorArgs.FlatOptions.Type == "" || schema.VectorArgs.FlatOptions.Dim == 0 || schema.VectorArgs.FlatOptions.DistanceMetric == "" {
|
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{}{
|
flatArgs := []interface{}{
|
||||||
"TYPE", schema.VectorArgs.FlatOptions.Type,
|
"TYPE", schema.VectorArgs.FlatOptions.Type,
|
||||||
@@ -1010,7 +1052,9 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp
|
|||||||
if schema.VectorArgs.HNSWOptions != nil {
|
if schema.VectorArgs.HNSWOptions != nil {
|
||||||
args = append(args, "HNSW")
|
args = append(args, "HNSW")
|
||||||
if schema.VectorArgs.HNSWOptions.Type == "" || schema.VectorArgs.HNSWOptions.Dim == 0 || schema.VectorArgs.HNSWOptions.DistanceMetric == "" {
|
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{}{
|
hnswArgs := []interface{}{
|
||||||
"TYPE", schema.VectorArgs.HNSWOptions.Type,
|
"TYPE", schema.VectorArgs.HNSWOptions.Type,
|
||||||
@@ -1035,10 +1079,48 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp
|
|||||||
args = append(args, len(hnswArgs))
|
args = append(args, len(hnswArgs))
|
||||||
args = append(args, 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.GeoShapeFieldType != "" {
|
||||||
if schema.FieldType != SearchFieldTypeGeoShape {
|
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)
|
args = append(args, schema.GeoShapeFieldType)
|
||||||
}
|
}
|
||||||
@@ -1196,7 +1278,7 @@ func (c cmdable) FTExplainWithArgs(ctx context.Context, index string, query stri
|
|||||||
// FTExplainCli - Returns the execution plan for a complex query. [Not Implemented]
|
// FTExplainCli - Returns the execution plan for a complex query. [Not Implemented]
|
||||||
// For more information, see https://redis.io/commands/ft.explaincli/
|
// For more information, see https://redis.io/commands/ft.explaincli/
|
||||||
func (c cmdable) FTExplainCli(ctx context.Context, key, path string) error {
|
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) {
|
func parseFTInfo(data map[string]interface{}) (FTInfoResult, error) {
|
||||||
@@ -1654,8 +1736,14 @@ func parseFTSearch(data []interface{}, noContent, withScores, withPayloads, with
|
|||||||
if i < len(data) {
|
if i < len(data) {
|
||||||
fields, ok := data[i].([]interface{})
|
fields, ok := data[i].([]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
|
if data[i] == proto.Nil || data[i] == nil {
|
||||||
|
doc.Error = proto.Nil
|
||||||
|
doc.Fields = map[string]string{}
|
||||||
|
fields = []interface{}{}
|
||||||
|
} else {
|
||||||
return FTSearchResult{}, fmt.Errorf("invalid document fields format")
|
return FTSearchResult{}, fmt.Errorf("invalid document fields format")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for j := 0; j < len(fields); j += 2 {
|
for j := 0; j < len(fields); j += 2 {
|
||||||
key, ok := fields[j].(string)
|
key, ok := fields[j].(string)
|
||||||
@@ -1751,7 +1839,7 @@ type SearchQuery []interface{}
|
|||||||
// For more information, please refer to the Redis documentation about [FT.SEARCH].
|
// For more information, please refer to the Redis documentation about [FT.SEARCH].
|
||||||
//
|
//
|
||||||
// [FT.SEARCH]: (https://redis.io/commands/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}
|
queryArgs := []interface{}{query}
|
||||||
if options != nil {
|
if options != nil {
|
||||||
if options.NoContent {
|
if options.NoContent {
|
||||||
@@ -1831,7 +1919,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery {
|
|||||||
for _, sortBy := range options.SortBy {
|
for _, sortBy := range options.SortBy {
|
||||||
queryArgs = append(queryArgs, sortBy.FieldName)
|
queryArgs = append(queryArgs, sortBy.FieldName)
|
||||||
if sortBy.Asc && sortBy.Desc {
|
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 {
|
if sortBy.Asc {
|
||||||
queryArgs = append(queryArgs, "ASC")
|
queryArgs = append(queryArgs, "ASC")
|
||||||
@@ -1859,7 +1947,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery {
|
|||||||
queryArgs = append(queryArgs, "DIALECT", 2)
|
queryArgs = append(queryArgs, "DIALECT", 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return queryArgs
|
return queryArgs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FTSearchWithArgs - Executes a search query on an index with additional options.
|
// FTSearchWithArgs - Executes a search query on an index with additional options.
|
||||||
@@ -1948,7 +2036,9 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin
|
|||||||
for _, sortBy := range options.SortBy {
|
for _, sortBy := range options.SortBy {
|
||||||
args = append(args, sortBy.FieldName)
|
args = append(args, sortBy.FieldName)
|
||||||
if sortBy.Asc && sortBy.Desc {
|
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 {
|
if sortBy.Asc {
|
||||||
args = append(args, "ASC")
|
args = append(args, "ASC")
|
||||||
|
654
search_test.go
654
search_test.go
@@ -38,6 +38,17 @@ func encodeFloat32Vector(vec []float32) []byte {
|
|||||||
return buf.Bytes()
|
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() {
|
var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
var client *redis.Client
|
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() {
|
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"}))
|
Expect(args).To(Equal(redis.AggregateQuery{"testQuery"}))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -828,7 +840,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
|
|||||||
Verbatim: true,
|
Verbatim: true,
|
||||||
Scorer: "BM25",
|
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[0]).To(Equal("testQuery"))
|
||||||
Expect(args).To(ContainElement("VERBATIM"))
|
Expect(args).To(ContainElement("VERBATIM"))
|
||||||
Expect(args).To(ContainElement("SCORER"))
|
Expect(args).To(ContainElement("SCORER"))
|
||||||
@@ -839,7 +852,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
|
|||||||
options := &redis.FTAggregateOptions{
|
options := &redis.FTAggregateOptions{
|
||||||
AddScores: true,
|
AddScores: true,
|
||||||
}
|
}
|
||||||
args := redis.FTAggregateQuery("q", options)
|
args, err := redis.FTAggregateQuery("q", options)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(args).To(ContainElement("ADDSCORES"))
|
Expect(args).To(ContainElement("ADDSCORES"))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -847,7 +861,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
|
|||||||
options := &redis.FTAggregateOptions{
|
options := &redis.FTAggregateOptions{
|
||||||
LoadAll: true,
|
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("LOAD"))
|
||||||
Expect(args).To(ContainElement("*"))
|
Expect(args).To(ContainElement("*"))
|
||||||
})
|
})
|
||||||
@@ -859,7 +874,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
|
|||||||
{Field: "field2"},
|
{Field: "field2"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
args := redis.FTAggregateQuery("q", options)
|
args, err := redis.FTAggregateQuery("q", options)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
// Verify LOAD options related arguments
|
// Verify LOAD options related arguments
|
||||||
Expect(args).To(ContainElement("LOAD"))
|
Expect(args).To(ContainElement("LOAD"))
|
||||||
// Check that field names and aliases are present
|
// Check that field names and aliases are present
|
||||||
@@ -872,7 +888,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
|
|||||||
options := &redis.FTAggregateOptions{
|
options := &redis.FTAggregateOptions{
|
||||||
Timeout: 500,
|
Timeout: 500,
|
||||||
}
|
}
|
||||||
args := redis.FTAggregateQuery("q", options)
|
args, err := redis.FTAggregateQuery("q", options)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(args).To(ContainElement("TIMEOUT"))
|
Expect(args).To(ContainElement("TIMEOUT"))
|
||||||
found := false
|
found := false
|
||||||
for i, a := range args {
|
for i, a := range args {
|
||||||
@@ -1745,6 +1762,631 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
|
|||||||
Expect(nanCount).To(Equal(2))
|
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() {
|
It("should fail when using a non-zero offset with a zero limit", Label("search", "ftsearch"), func() {
|
||||||
SkipBeforeRedisVersion(7.9, "requires Redis 8.x")
|
SkipBeforeRedisVersion(7.9, "requires Redis 8.x")
|
||||||
val, err := client.FTCreate(ctx, "testIdx", &redis.FTCreateOptions{}, &redis.FieldSchema{
|
val, err := client.FTCreate(ctx, "testIdx", &redis.FTCreateOptions{}, &redis.FieldSchema{
|
||||||
|
@@ -7,7 +7,9 @@ import (
|
|||||||
|
|
||||||
type StreamCmdable interface {
|
type StreamCmdable interface {
|
||||||
XAdd(ctx context.Context, a *XAddArgs) *StringCmd
|
XAdd(ctx context.Context, a *XAddArgs) *StringCmd
|
||||||
|
XAckDel(ctx context.Context, stream string, group string, mode string, ids ...string) *SliceCmd
|
||||||
XDel(ctx context.Context, stream string, ids ...string) *IntCmd
|
XDel(ctx context.Context, stream string, ids ...string) *IntCmd
|
||||||
|
XDelEx(ctx context.Context, stream string, mode string, ids ...string) *SliceCmd
|
||||||
XLen(ctx context.Context, stream string) *IntCmd
|
XLen(ctx context.Context, stream string) *IntCmd
|
||||||
XRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd
|
XRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd
|
||||||
XRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd
|
XRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd
|
||||||
@@ -31,8 +33,12 @@ type StreamCmdable interface {
|
|||||||
XAutoClaimJustID(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimJustIDCmd
|
XAutoClaimJustID(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimJustIDCmd
|
||||||
XTrimMaxLen(ctx context.Context, key string, maxLen int64) *IntCmd
|
XTrimMaxLen(ctx context.Context, key string, maxLen int64) *IntCmd
|
||||||
XTrimMaxLenApprox(ctx context.Context, key string, maxLen, limit int64) *IntCmd
|
XTrimMaxLenApprox(ctx context.Context, key string, maxLen, limit int64) *IntCmd
|
||||||
|
XTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *IntCmd
|
||||||
|
XTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *IntCmd
|
||||||
XTrimMinID(ctx context.Context, key string, minID string) *IntCmd
|
XTrimMinID(ctx context.Context, key string, minID string) *IntCmd
|
||||||
XTrimMinIDApprox(ctx context.Context, key string, minID string, limit int64) *IntCmd
|
XTrimMinIDApprox(ctx context.Context, key string, minID string, limit int64) *IntCmd
|
||||||
|
XTrimMinIDMode(ctx context.Context, key string, minID string, mode string) *IntCmd
|
||||||
|
XTrimMinIDApproxMode(ctx context.Context, key string, minID string, limit int64, mode string) *IntCmd
|
||||||
XInfoGroups(ctx context.Context, key string) *XInfoGroupsCmd
|
XInfoGroups(ctx context.Context, key string) *XInfoGroupsCmd
|
||||||
XInfoStream(ctx context.Context, key string) *XInfoStreamCmd
|
XInfoStream(ctx context.Context, key string) *XInfoStreamCmd
|
||||||
XInfoStreamFull(ctx context.Context, key string, count int) *XInfoStreamFullCmd
|
XInfoStreamFull(ctx context.Context, key string, count int) *XInfoStreamFullCmd
|
||||||
@@ -54,6 +60,7 @@ type XAddArgs struct {
|
|||||||
// Approx causes MaxLen and MinID to use "~" matcher (instead of "=").
|
// Approx causes MaxLen and MinID to use "~" matcher (instead of "=").
|
||||||
Approx bool
|
Approx bool
|
||||||
Limit int64
|
Limit int64
|
||||||
|
Mode string
|
||||||
ID string
|
ID string
|
||||||
Values interface{}
|
Values interface{}
|
||||||
}
|
}
|
||||||
@@ -81,6 +88,11 @@ func (c cmdable) XAdd(ctx context.Context, a *XAddArgs) *StringCmd {
|
|||||||
if a.Limit > 0 {
|
if a.Limit > 0 {
|
||||||
args = append(args, "limit", a.Limit)
|
args = append(args, "limit", a.Limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.Mode != "" {
|
||||||
|
args = append(args, a.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
if a.ID != "" {
|
if a.ID != "" {
|
||||||
args = append(args, a.ID)
|
args = append(args, a.ID)
|
||||||
} else {
|
} else {
|
||||||
@@ -93,6 +105,16 @@ func (c cmdable) XAdd(ctx context.Context, a *XAddArgs) *StringCmd {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c cmdable) XAckDel(ctx context.Context, stream string, group string, mode string, ids ...string) *SliceCmd {
|
||||||
|
args := []interface{}{"xackdel", stream, group, mode, "ids", len(ids)}
|
||||||
|
for _, id := range ids {
|
||||||
|
args = append(args, id)
|
||||||
|
}
|
||||||
|
cmd := NewSliceCmd(ctx, args...)
|
||||||
|
_ = c(ctx, cmd)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
func (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd {
|
func (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd {
|
||||||
args := []interface{}{"xdel", stream}
|
args := []interface{}{"xdel", stream}
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
@@ -103,6 +125,16 @@ func (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c cmdable) XDelEx(ctx context.Context, stream string, mode string, ids ...string) *SliceCmd {
|
||||||
|
args := []interface{}{"xdelex", stream, mode, "ids", len(ids)}
|
||||||
|
for _, id := range ids {
|
||||||
|
args = append(args, id)
|
||||||
|
}
|
||||||
|
cmd := NewSliceCmd(ctx, args...)
|
||||||
|
_ = c(ctx, cmd)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
func (c cmdable) XLen(ctx context.Context, stream string) *IntCmd {
|
func (c cmdable) XLen(ctx context.Context, stream string) *IntCmd {
|
||||||
cmd := NewIntCmd(ctx, "xlen", stream)
|
cmd := NewIntCmd(ctx, "xlen", stream)
|
||||||
_ = c(ctx, cmd)
|
_ = c(ctx, cmd)
|
||||||
@@ -375,6 +407,8 @@ func xClaimArgs(a *XClaimArgs) []interface{} {
|
|||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: refactor xTrim, xTrimMode and the wrappers over the functions
|
||||||
|
|
||||||
// xTrim If approx is true, add the "~" parameter, otherwise it is the default "=" (redis default).
|
// xTrim If approx is true, add the "~" parameter, otherwise it is the default "=" (redis default).
|
||||||
// example:
|
// example:
|
||||||
//
|
//
|
||||||
@@ -418,6 +452,42 @@ func (c cmdable) XTrimMinIDApprox(ctx context.Context, key string, minID string,
|
|||||||
return c.xTrim(ctx, key, "minid", true, minID, limit)
|
return c.xTrim(ctx, key, "minid", true, minID, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c cmdable) xTrimMode(
|
||||||
|
ctx context.Context, key, strategy string,
|
||||||
|
approx bool, threshold interface{}, limit int64,
|
||||||
|
mode string,
|
||||||
|
) *IntCmd {
|
||||||
|
args := make([]interface{}, 0, 7)
|
||||||
|
args = append(args, "xtrim", key, strategy)
|
||||||
|
if approx {
|
||||||
|
args = append(args, "~")
|
||||||
|
}
|
||||||
|
args = append(args, threshold)
|
||||||
|
if limit > 0 {
|
||||||
|
args = append(args, "limit", limit)
|
||||||
|
}
|
||||||
|
args = append(args, mode)
|
||||||
|
cmd := NewIntCmd(ctx, args...)
|
||||||
|
_ = c(ctx, cmd)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cmdable) XTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *IntCmd {
|
||||||
|
return c.xTrimMode(ctx, key, "maxlen", false, maxLen, 0, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cmdable) XTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *IntCmd {
|
||||||
|
return c.xTrimMode(ctx, key, "maxlen", true, maxLen, limit, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cmdable) XTrimMinIDMode(ctx context.Context, key string, minID string, mode string) *IntCmd {
|
||||||
|
return c.xTrimMode(ctx, key, "minid", false, minID, 0, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cmdable) XTrimMinIDApproxMode(ctx context.Context, key string, minID string, limit int64, mode string) *IntCmd {
|
||||||
|
return c.xTrimMode(ctx, key, "minid", true, minID, limit, mode)
|
||||||
|
}
|
||||||
|
|
||||||
func (c cmdable) XInfoConsumers(ctx context.Context, key string, group string) *XInfoConsumersCmd {
|
func (c cmdable) XInfoConsumers(ctx context.Context, key string, group string) *XInfoConsumersCmd {
|
||||||
cmd := NewXInfoConsumersCmd(ctx, key, group)
|
cmd := NewXInfoConsumersCmd(ctx, key, group)
|
||||||
_ = c(ctx, cmd)
|
_ = c(ctx, cmd)
|
||||||
|
@@ -287,8 +287,7 @@ type VSimArgs struct {
|
|||||||
FilterEF int64
|
FilterEF int64
|
||||||
Truth bool
|
Truth bool
|
||||||
NoThread bool
|
NoThread bool
|
||||||
// The `VSim` command in Redis has the option, by the doc in Redis.io don't have.
|
Epsilon float64
|
||||||
// Epsilon float64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v VSimArgs) appendArgs(args []any) []any {
|
func (v VSimArgs) appendArgs(args []any) []any {
|
||||||
@@ -310,13 +309,13 @@ func (v VSimArgs) appendArgs(args []any) []any {
|
|||||||
if v.NoThread {
|
if v.NoThread {
|
||||||
args = append(args, "nothread")
|
args = append(args, "nothread")
|
||||||
}
|
}
|
||||||
// if v.Epsilon > 0 {
|
if v.Epsilon > 0 {
|
||||||
// args = append(args, "Epsilon", v.Epsilon)
|
args = append(args, "Epsilon", v.Epsilon)
|
||||||
// }
|
}
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [COUNT num]
|
// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [COUNT num] [EPSILON delta]
|
||||||
// [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]`
|
// [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]`
|
||||||
// note: the API is experimental and may be subject to change.
|
// note: the API is experimental and may be subject to change.
|
||||||
func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *StringSliceCmd {
|
func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *StringSliceCmd {
|
||||||
@@ -331,7 +330,7 @@ func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simAr
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [COUNT num]
|
// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [COUNT num] [EPSILON delta]
|
||||||
// [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]`
|
// [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]`
|
||||||
// note: the API is experimental and may be subject to change.
|
// note: the API is experimental and may be subject to change.
|
||||||
func (c cmdable) VSimWithArgsWithScores(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *VectorScoreSliceCmd {
|
func (c cmdable) VSimWithArgsWithScores(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *VectorScoreSliceCmd {
|
||||||
|
Reference in New Issue
Block a user