From b00f7f9f4c159cd885305beb4b20ffa154560a73 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Sat, 10 May 2025 00:20:39 +0800 Subject: [PATCH 1/4] feat: support vectorset --- command.go | 55 +++++++ commands.go | 1 + vectorset_commands.go | 284 ++++++++++++++++++++++++++++++++ vectorset_commands_test.go | 322 +++++++++++++++++++++++++++++++++++++ 4 files changed, 662 insertions(+) create mode 100644 vectorset_commands.go create mode 100644 vectorset_commands_test.go diff --git a/command.go b/command.go index 3253af6cc..11da3791d 100644 --- a/command.go +++ b/command.go @@ -5615,3 +5615,58 @@ func (cmd *MonitorCmd) Stop() { defer cmd.mu.Unlock() cmd.status = monitorStatusStop } + +type MapStringFloatCmd struct { + baseCmd + + val map[string]float64 +} + +var _ Cmder = (*MapStringFloatCmd)(nil) + +func NewMapStringFloatCmd(ctx context.Context, args ...interface{}) *MapStringFloatCmd { + return &MapStringFloatCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *MapStringFloatCmd) SetVal(val map[string]float64) { + cmd.val = val +} + +func (cmd *MapStringFloatCmd) Val() map[string]float64 { + return cmd.val +} + +func (cmd *MapStringFloatCmd) Result() (map[string]float64, error) { + return cmd.val, cmd.err +} + +func (cmd *MapStringFloatCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *MapStringFloatCmd) readReply(rd *proto.Reader) error { + n, err := rd.ReadMapLen() + if err != nil { + return err + } + + cmd.val = make(map[string]float64, n) + for i := 0; i < n; i++ { + key, err := rd.ReadString() + if err != nil { + return err + } + + nn, err := rd.ReadFloat() + if err != nil { + return err + } + cmd.val[key] = nn + } + return nil +} diff --git a/commands.go b/commands.go index 271323242..c0358001d 100644 --- a/commands.go +++ b/commands.go @@ -234,6 +234,7 @@ type Cmdable interface { StreamCmdable TimeseriesCmdable JSONCmdable + VectorSetCmdable } type StatefulCmdable interface { diff --git a/vectorset_commands.go b/vectorset_commands.go new file mode 100644 index 000000000..06a88a10e --- /dev/null +++ b/vectorset_commands.go @@ -0,0 +1,284 @@ +package redis + +import ( + "context" + "strconv" +) + +type VectorSetCmdable interface { + VAdd(ctx context.Context, key, element string, val Vector) *BoolCmd + VAddArgs(ctx context.Context, key, element string, val Vector, addArgs VAddArgs) *BoolCmd + VCard(ctx context.Context, key string) *IntCmd + VDim(ctx context.Context, key string) *IntCmd + VEmb(ctx context.Context, key, element string, raw bool) *SliceCmd + VGetAttr(ctx context.Context, key, element string) *StringCmd + VInfo(ctx context.Context, key string) *MapStringInterfaceCmd + VLinks(ctx context.Context, key, element string) *StringSliceCmd + VLinksWithScores(ctx context.Context, key, element string) *MapStringFloatCmd + VRandMember(ctx context.Context, key string) *StringCmd + VRandMemberCount(ctx context.Context, key string, count int) *StringSliceCmd + VRem(ctx context.Context, key, element string) *BoolCmd + VSetAttr(ctx context.Context, key, element, attr string) *BoolCmd + VSim(ctx context.Context, key string, val Vector) *StringSliceCmd + VSimWithScores(ctx context.Context, key string, val Vector) *MapStringFloatCmd + VSimArgs(ctx context.Context, key string, val Vector, args VSimArgs) *StringSliceCmd + VSimArgsWithScores(ctx context.Context, key string, val Vector, args VSimArgs) *MapStringFloatCmd +} + +type Vector interface { + Value() []any +} + +const ( + vectorFormatFP32 string = "FP32" + vectorFormatValues string = "Values" +) + +type VectorFP32 struct { + Val []byte +} + +func (v *VectorFP32) Value() []any { + return []any{vectorFormatFP32, v.Val} +} + +var _ Vector = &VectorFP32{} + +type VectorValues struct { + Val []float64 +} + +func (v *VectorValues) Value() []any { + res := make([]any, 2+len(v.Val)) + res[0] = vectorFormatValues + res[1] = len(v.Val) + for i, v := range v.Val { + res[2+i] = v + } + return res +} + +var _ Vector = &VectorValues{} + +type VectorRef struct { + Name string // the name of the referent vector +} + +func (v *VectorRef) Value() []any { + return []any{"ele", v.Name} +} + +var _ Vector = &VectorRef{} + +// `VADD key (FP32 | VALUES num) vector element` +func (c cmdable) VAdd(ctx context.Context, key, element string, val Vector) *BoolCmd { + return c.VAddArgs(ctx, key, element, val, VAddArgs{}) +} + +type VAddArgs struct { + // the REDUCE option must be passed immediately after the key + Reduce int64 + Cas bool + + // The NoQuant, Q8 and Bin options are mutually exclusive. + NoQuant bool + Q8 bool + Bin bool + + EF int64 + SetAttr string + M int64 +} + +func (v VAddArgs) reduce() int64 { + return v.Reduce +} + +func (v VAddArgs) appendArgs(args []any) []any { + if v.Cas { + args = append(args, "cas") + } + + if v.NoQuant { + args = append(args, "noquant") + } else if v.Q8 { + args = append(args, "q8") + } else if v.Bin { + args = append(args, "bin") + } + + if v.EF > 0 { + args = append(args, "ef", strconv.FormatInt(v.EF, 10)) + } + if len(v.SetAttr) > 0 { + args = append(args, "setattr", v.SetAttr) + } + if v.M > 0 { + args = append(args, "m", strconv.FormatInt(v.M, 10)) + } + return args +} + +// `VADD key [REDUCE dim] (FP32 | VALUES num) vector element [CAS] [NOQUANT | Q8 | BIN] [EF build-exploration-factor] [SETATTR attributes] [M numlinks]` +func (c cmdable) VAddArgs(ctx context.Context, key, element string, val Vector, addArgs VAddArgs) *BoolCmd { + args := []any{"vadd", key} + if addArgs.reduce() > 0 { + args = append(args, "reduce", addArgs.reduce()) + } + args = append(args, val.Value()...) + args = append(args, element) + args = addArgs.appendArgs(args) + cmd := NewBoolCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// `VCARD key` +func (c cmdable) VCard(ctx context.Context, key string) *IntCmd { + cmd := NewIntCmd(ctx, "vcard", key) + _ = c(ctx, cmd) + return cmd +} + +// `VDIM key` +func (c cmdable) VDim(ctx context.Context, key string) *IntCmd { + cmd := NewIntCmd(ctx, "vdim", key) + _ = c(ctx, cmd) + return cmd +} + +// `VEMB key element [RAW]` +func (c cmdable) VEmb(ctx context.Context, key, element string, raw bool) *SliceCmd { + args := []any{"vemb", key, element} + if raw { + args = append(args, "raw") + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// `VGETATTR key element` +func (c cmdable) VGetAttr(ctx context.Context, key, element string) *StringCmd { + cmd := NewStringCmd(ctx, "vgetattr", key, element) + _ = c(ctx, cmd) + return cmd +} + +// `VINFO key` +func (c cmdable) VInfo(ctx context.Context, key string) *MapStringInterfaceCmd { + cmd := NewMapStringInterfaceCmd(ctx, "vinfo", key) + _ = c(ctx, cmd) + return cmd +} + +// `VLINKS key element` +func (c cmdable) VLinks(ctx context.Context, key, element string) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "vlinks", key, element) + _ = c(ctx, cmd) + return cmd +} + +// `VLINKS key element WITHSCORES` +func (c cmdable) VLinksWithScores(ctx context.Context, key, element string) *MapStringFloatCmd { + cmd := NewMapStringFloatCmd(ctx, "vlinks", key, element, "withscores") + _ = c(ctx, cmd) + return cmd +} + +// `VRANDMEMBER key` +func (c cmdable) VRandMember(ctx context.Context, key string) *StringCmd { + cmd := NewStringCmd(ctx, "vrandmember", key) + _ = c(ctx, cmd) + return cmd +} + +// `VRANDMEMBER key [count]` +func (c cmdable) VRandMemberCount(ctx context.Context, key string, count int) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "vrandmember", key, count) + _ = c(ctx, cmd) + return cmd +} + +// `VREM key element` +func (c cmdable) VRem(ctx context.Context, key, element string) *BoolCmd { + cmd := NewBoolCmd(ctx, "vrem", key, element) + _ = c(ctx, cmd) + return cmd +} + +// `VSETATTR key element "{ JSON obj }"` +func (c cmdable) VSetAttr(ctx context.Context, key, element, attr string) *BoolCmd { + cmd := NewBoolCmd(ctx, "vsetattr", key, element, attr) + _ = c(ctx, cmd) + return cmd +} + +// `VSIM key (ELE | FP32 | VALUES num) (vector | element)` +func (c cmdable) VSim(ctx context.Context, key string, val Vector) *StringSliceCmd { + return c.VSimArgs(ctx, key, val, VSimArgs{}) +} + +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) WITHSCORES` +func (c cmdable) VSimWithScores(ctx context.Context, key string, val Vector) *MapStringFloatCmd { + return c.VSimArgsWithScores(ctx, key, val, VSimArgs{}) +} + +type VSimArgs struct { + Count int64 + EF int64 + Filter string + FilterEF int64 + Truth bool + NoThread bool + // The `VSim` command in Redis has the option, by the doc in Redis.io don't have. + // Epsilon float64 +} + +func (v VSimArgs) appendArgs(args []any) []any { + if v.Count > 0 { + args = append(args, "count", v.Count) + } + if v.EF > 0 { + args = append(args, "ef", v.EF) + } + if len(v.Filter) > 0 { + args = append(args, "filter", v.Filter) + } + if v.FilterEF > 0 { + args = append(args, "filter-ef", v.FilterEF) + } + if v.Truth { + args = append(args, "truth") + } + if v.NoThread { + args = append(args, "nothread") + } + // if v.Epsilon > 0 { + // args = append(args, "Epsilon", v.Epsilon) + // } + return args +} + +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [COUNT num] +// [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` +func (c cmdable) VSimArgs(ctx context.Context, key string, val Vector, simArgs VSimArgs) *StringSliceCmd { + args := []any{"vsim", key} + args = append(args, val.Value()...) + args = simArgs.appendArgs(args) + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [COUNT num] +// [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` +func (c cmdable) VSimArgsWithScores(ctx context.Context, key string, val Vector, simArgs VSimArgs) *MapStringFloatCmd { + args := []any{"vsim", key} + args = append(args, val.Value()...) + args = append(args, "withscores") + args = simArgs.appendArgs(args) + cmd := NewMapStringFloatCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} diff --git a/vectorset_commands_test.go b/vectorset_commands_test.go new file mode 100644 index 000000000..79362ec67 --- /dev/null +++ b/vectorset_commands_test.go @@ -0,0 +1,322 @@ +package redis_test + +import ( + "context" + "fmt" + "math/rand" + "time" + + . "github.com/bsm/ginkgo/v2" + . "github.com/bsm/gomega" + "github.com/redis/go-redis/v9" + "github.com/redis/go-redis/v9/internal/proto" +) + +func expectNil(err error) { + Expect(err).NotTo(HaveOccurred()) +} + +func expectTrue(t bool) { + expectEqual(t, true) +} + +func expectEqual[T any, U any](a T, b U) { + Expect(a).To(BeEquivalentTo(b)) +} + +func generateRandomVector(dim int) redis.VectorValues { + rand.Seed(time.Now().UnixNano()) + v := make([]float64, dim) + for i := range v { + v[i] = float64(rand.Intn(1000)) + rand.Float64() + } + return redis.VectorValues{Val: v} +} + +var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { + ctx := context.TODO() + + setupRedisClient := func(protocolVersion int) *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + DB: 0, + Protocol: protocolVersion, + UnstableResp3: true, + }) + } + + protocols := []int{2, 3} + for _, protocol := range protocols { + protocol := protocol + + Context(fmt.Sprintf("with protocol version %d", protocol), func() { + var client *redis.Client + + BeforeEach(func() { + client = setupRedisClient(protocol) + Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + if client != nil { + client.FlushDB(ctx) + client.Close() + } + }) + + It("basic", func() { + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for ​​VectorSet​​.") + vecName := "basic" + val := &redis.VectorValues{ + Val: []float64{1.5, 2.4, 3.3, 4.2}, + } + ok, err := client.VAdd(ctx, vecName, "k1", val).Result() + expectNil(err) + expectTrue(ok) + + fp32 := "\x8f\xc2\xf9\x3e\xcb\xbe\xe9\xbe\xb0\x1e\xca\x3f\x5e\x06\x9e\x3f" + val2 := &redis.VectorFP32{ + Val: []byte(fp32), + } + ok, err = client.VAdd(ctx, vecName, "k2", val2).Result() + expectNil(err) + expectTrue(ok) + + dim, err := client.VDim(ctx, vecName).Result() + expectNil(err) + expectEqual(dim, 4) + + count, err := client.VCard(ctx, vecName).Result() + expectNil(err) + expectEqual(count, 2) + + ok, err = client.VRem(ctx, vecName, "k1").Result() + expectNil(err) + expectTrue(ok) + + count, err = client.VCard(ctx, vecName).Result() + expectNil(err) + expectEqual(count, 1) + }) + + It("basic similarity", func() { + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for ​​VectorSet​​.") + vecName := "basic_similarity" + + ok, err := client.VAdd(ctx, vecName, "k1", &redis.VectorValues{ + Val: []float64{1, 0, 0, 0}, + }).Result() + expectNil(err) + expectTrue(ok) + ok, err = client.VAdd(ctx, vecName, "k2", &redis.VectorValues{ + Val: []float64{0.99, 0.01, 0, 0}, + }).Result() + expectNil(err) + expectTrue(ok) + ok, err = client.VAdd(ctx, vecName, "k3", &redis.VectorValues{ + Val: []float64{0.1, 1, -1, 0.5}, + }).Result() + expectNil(err) + expectTrue(ok) + + sim, err := client.VSimWithScores(ctx, vecName, &redis.VectorValues{ + Val: []float64{1, 0, 0, 0}, + }).Result() + expectNil(err) + expectEqual(len(sim), 3) + expectTrue(sim["k1"] > 0.99) + expectTrue(sim["k2"] > 0.99) + expectTrue(sim["k3"] < 0.8) + }) + + It("dimension operation", func() { + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for ​​VectorSet​​.") + vecName := "dimension_op" + originalDim := 100 + reducedDim := 50 + + v1 := generateRandomVector(originalDim) + ok, err := client.VAddArgs(ctx, vecName, "k1", &v1, redis.VAddArgs{ + Reduce: int64(reducedDim), + }).Result() + expectNil(err) + expectTrue(ok) + + info, err := client.VInfo(ctx, vecName).Result() + expectNil(err) + dim := info["vector-dim"].(int64) + oriDim := info["projection-input-dim"].(int64) + expectEqual(dim, reducedDim) + expectEqual(oriDim, originalDim) + + wrongDim := 80 + wrongV := generateRandomVector(wrongDim) + _, err = client.VAddArgs(ctx, vecName, "kw", &wrongV, redis.VAddArgs{ + Reduce: int64(reducedDim), + }).Result() + expectTrue(err != nil) + + v2 := generateRandomVector(originalDim) + ok, err = client.VAddArgs(ctx, vecName, "k2", &v2, redis.VAddArgs{ + Reduce: int64(reducedDim), + }).Result() + expectNil(err) + expectTrue(ok) + }) + + It("remove", func() { + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for ​​VectorSet​​.") + vecName := "remove" + v1 := generateRandomVector(5) + ok, err := client.VAdd(ctx, vecName, "k1", &v1).Result() + expectNil(err) + expectTrue(ok) + + exist, err := client.Exists(ctx, vecName).Result() + expectNil(err) + expectEqual(exist, 1) + + ok, err = client.VRem(ctx, vecName, "k1").Result() + expectNil(err) + expectTrue(ok) + + exist, err = client.Exists(ctx, vecName).Result() + expectNil(err) + expectEqual(exist, 0) + }) + + It("all operations", func() { + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for ​​VectorSet​​.") + vecName := "commands" + vals := []struct { + name string + v redis.VectorValues + attr string + }{ + { + name: "k0", + v: redis.VectorValues{Val: []float64{1, 0, 0, 0}}, + attr: `{"age": 25, "name": "Alice", "active": true, "scores": [85, 90, 95], "city": "New York"}`, + }, + { + name: "k1", + v: redis.VectorValues{Val: []float64{0, 1, 0, 0}}, + attr: `{"age": 30, "name": "Bob", "active": false, "scores": [70, 75, 80], "city": "Boston"}`, + }, + { + name: "k2", + v: redis.VectorValues{Val: []float64{0, 0, 1, 0}}, + attr: `{"age": 35, "name": "Charlie", "scores": [60, 65, 70], "city": "Seattle"}`, + }, + { + name: "k3", + v: redis.VectorValues{Val: []float64{0, 0, 0, 1}}, + }, + { + name: "k4", + v: redis.VectorValues{Val: []float64{0.5, 0.5, 0, 0}}, + attr: `invalid json`, + }, + } + + // If the key doesn't exist, return null error + _, err := client.VRandMember(ctx, vecName).Result() + expectEqual(err.Error(), proto.Nil.Error()) + + // If the key doesn't exist, return an empty array + res, err := client.VRandMemberCount(ctx, vecName, 3).Result() + expectNil(err) + expectEqual(len(res), 0) + + for _, v := range vals { + ok, err := client.VAdd(ctx, vecName, v.name, &v.v).Result() + expectNil(err) + expectTrue(ok) + if len(v.attr) > 0 { + ok, err = client.VSetAttr(ctx, vecName, v.name, v.attr).Result() + expectNil(err) + expectTrue(ok) + } + } + + // VGetAttr + attr, err := client.VGetAttr(ctx, vecName, vals[1].name).Result() + expectNil(err) + expectEqual(attr, vals[1].attr) + + // VRandMember + _, err = client.VRandMember(ctx, vecName).Result() + expectNil(err) + + res, err = client.VRandMemberCount(ctx, vecName, 3).Result() + expectNil(err) + expectEqual(len(res), 3) + + res, err = client.VRandMemberCount(ctx, vecName, 10).Result() + expectNil(err) + expectEqual(len(res), len(vals)) + + // test equality + sim, err := client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + Filter: `.age == 25`, + }).Result() + expectNil(err) + expectEqual(len(sim), 1) + expectEqual(sim[0], vals[0].name) + + // test greater than + sim, err = client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + Filter: `.age > 25`, + }).Result() + expectNil(err) + expectEqual(len(sim), 2) + + // test less than or equal + sim, err = client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + Filter: `.age <= 30`, + }).Result() + expectNil(err) + expectEqual(len(sim), 2) + + // test string equality + sim, err = client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + Filter: `.name == "Alice"`, + }).Result() + expectNil(err) + expectEqual(len(sim), 1) + expectEqual(sim[0], vals[0].name) + + // test string inequality + sim, err = client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + Filter: `.name != "Alice"`, + }).Result() + expectNil(err) + expectEqual(len(sim), 2) + + // test bool + sim, err = client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + Filter: `.active`, + }).Result() + expectNil(err) + expectEqual(len(sim), 1) + expectEqual(sim[0], vals[0].name) + + // test logical add + sim, err = client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + Filter: `.age > 20 and .age < 30`, + }).Result() + expectNil(err) + expectEqual(len(sim), 1) + expectEqual(sim[0], vals[0].name) + + // test logical or + sim, err = client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + Filter: `.age < 30 or .age > 35`, + }).Result() + expectNil(err) + expectEqual(len(sim), 1) + expectEqual(sim[0], vals[0].name) + }) + }) + } +}) From 51a44fb2f56b14bf89d4a145b952732ab9e9dc67 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Sat, 10 May 2025 00:36:34 +0800 Subject: [PATCH 2/4] fix: char encoding error --- vectorset_commands_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vectorset_commands_test.go b/vectorset_commands_test.go index 79362ec67..edc0ea7ca 100644 --- a/vectorset_commands_test.go +++ b/vectorset_commands_test.go @@ -65,7 +65,7 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { }) It("basic", func() { - SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for ​​VectorSet​​.") + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") vecName := "basic" val := &redis.VectorValues{ Val: []float64{1.5, 2.4, 3.3, 4.2}, @@ -100,7 +100,7 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { }) It("basic similarity", func() { - SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for ​​VectorSet​​.") + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") vecName := "basic_similarity" ok, err := client.VAdd(ctx, vecName, "k1", &redis.VectorValues{ @@ -130,7 +130,7 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { }) It("dimension operation", func() { - SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for ​​VectorSet​​.") + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") vecName := "dimension_op" originalDim := 100 reducedDim := 50 @@ -165,7 +165,7 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { }) It("remove", func() { - SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for ​​VectorSet​​.") + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") vecName := "remove" v1 := generateRandomVector(5) ok, err := client.VAdd(ctx, vecName, "k1", &v1).Result() @@ -186,7 +186,7 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { }) It("all operations", func() { - SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for ​​VectorSet​​.") + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") vecName := "commands" vals := []struct { name string From af01b6d9874afede3a363eecd500f07f8afb0316 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Wed, 21 May 2025 20:42:14 +0800 Subject: [PATCH 3/4] use `any` instread of `interface{}` --- command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command.go b/command.go index 2dc362772..675388315 100644 --- a/command.go +++ b/command.go @@ -5629,7 +5629,7 @@ type MapStringFloatCmd struct { var _ Cmder = (*MapStringFloatCmd)(nil) -func NewMapStringFloatCmd(ctx context.Context, args ...interface{}) *MapStringFloatCmd { +func NewMapStringFloatCmd(ctx context.Context, args ...any) *MapStringFloatCmd { return &MapStringFloatCmd{ baseCmd: baseCmd{ ctx: ctx, From c7287907d2ec9fb4969b3c3b3744070aa300418b Mon Sep 17 00:00:00 2001 From: fukua95 Date: Wed, 21 May 2025 21:19:47 +0800 Subject: [PATCH 4/4] update vectorset API Signed-off-by: fukua95 --- vectorset_commands.go | 27 ++++++++++++++++++--------- vectorset_commands_test.go | 22 +++++++++++----------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/vectorset_commands.go b/vectorset_commands.go index 06a88a10e..bc2fb35ef 100644 --- a/vectorset_commands.go +++ b/vectorset_commands.go @@ -7,7 +7,7 @@ import ( type VectorSetCmdable interface { VAdd(ctx context.Context, key, element string, val Vector) *BoolCmd - VAddArgs(ctx context.Context, key, element string, val Vector, addArgs VAddArgs) *BoolCmd + VAddWithArgs(ctx context.Context, key, element string, val Vector, addArgs *VAddArgs) *BoolCmd VCard(ctx context.Context, key string) *IntCmd VDim(ctx context.Context, key string) *IntCmd VEmb(ctx context.Context, key, element string, raw bool) *SliceCmd @@ -21,8 +21,8 @@ type VectorSetCmdable interface { VSetAttr(ctx context.Context, key, element, attr string) *BoolCmd VSim(ctx context.Context, key string, val Vector) *StringSliceCmd VSimWithScores(ctx context.Context, key string, val Vector) *MapStringFloatCmd - VSimArgs(ctx context.Context, key string, val Vector, args VSimArgs) *StringSliceCmd - VSimArgsWithScores(ctx context.Context, key string, val Vector, args VSimArgs) *MapStringFloatCmd + VSimWithArgs(ctx context.Context, key string, val Vector, args *VSimArgs) *StringSliceCmd + VSimWithArgsWithScores(ctx context.Context, key string, val Vector, args *VSimArgs) *MapStringFloatCmd } type Vector interface { @@ -72,7 +72,7 @@ var _ Vector = &VectorRef{} // `VADD key (FP32 | VALUES num) vector element` func (c cmdable) VAdd(ctx context.Context, key, element string, val Vector) *BoolCmd { - return c.VAddArgs(ctx, key, element, val, VAddArgs{}) + return c.VAddWithArgs(ctx, key, element, val, &VAddArgs{}) } type VAddArgs struct { @@ -120,7 +120,10 @@ func (v VAddArgs) appendArgs(args []any) []any { } // `VADD key [REDUCE dim] (FP32 | VALUES num) vector element [CAS] [NOQUANT | Q8 | BIN] [EF build-exploration-factor] [SETATTR attributes] [M numlinks]` -func (c cmdable) VAddArgs(ctx context.Context, key, element string, val Vector, addArgs VAddArgs) *BoolCmd { +func (c cmdable) VAddWithArgs(ctx context.Context, key, element string, val Vector, addArgs *VAddArgs) *BoolCmd { + if addArgs == nil { + addArgs = &VAddArgs{} + } args := []any{"vadd", key} if addArgs.reduce() > 0 { args = append(args, "reduce", addArgs.reduce()) @@ -216,12 +219,12 @@ func (c cmdable) VSetAttr(ctx context.Context, key, element, attr string) *BoolC // `VSIM key (ELE | FP32 | VALUES num) (vector | element)` func (c cmdable) VSim(ctx context.Context, key string, val Vector) *StringSliceCmd { - return c.VSimArgs(ctx, key, val, VSimArgs{}) + return c.VSimWithArgs(ctx, key, val, &VSimArgs{}) } // `VSIM key (ELE | FP32 | VALUES num) (vector | element) WITHSCORES` func (c cmdable) VSimWithScores(ctx context.Context, key string, val Vector) *MapStringFloatCmd { - return c.VSimArgsWithScores(ctx, key, val, VSimArgs{}) + return c.VSimWithArgsWithScores(ctx, key, val, &VSimArgs{}) } type VSimArgs struct { @@ -262,7 +265,10 @@ func (v VSimArgs) appendArgs(args []any) []any { // `VSIM key (ELE | FP32 | VALUES num) (vector | element) [COUNT num] // [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` -func (c cmdable) VSimArgs(ctx context.Context, key string, val Vector, simArgs VSimArgs) *StringSliceCmd { +func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *StringSliceCmd { + if simArgs == nil { + simArgs = &VSimArgs{} + } args := []any{"vsim", key} args = append(args, val.Value()...) args = simArgs.appendArgs(args) @@ -273,7 +279,10 @@ func (c cmdable) VSimArgs(ctx context.Context, key string, val Vector, simArgs V // `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [COUNT num] // [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` -func (c cmdable) VSimArgsWithScores(ctx context.Context, key string, val Vector, simArgs VSimArgs) *MapStringFloatCmd { +func (c cmdable) VSimWithArgsWithScores(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *MapStringFloatCmd { + if simArgs == nil { + simArgs = &VSimArgs{} + } args := []any{"vsim", key} args = append(args, val.Value()...) args = append(args, "withscores") diff --git a/vectorset_commands_test.go b/vectorset_commands_test.go index edc0ea7ca..b3a7cea98 100644 --- a/vectorset_commands_test.go +++ b/vectorset_commands_test.go @@ -136,7 +136,7 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { reducedDim := 50 v1 := generateRandomVector(originalDim) - ok, err := client.VAddArgs(ctx, vecName, "k1", &v1, redis.VAddArgs{ + ok, err := client.VAddWithArgs(ctx, vecName, "k1", &v1, &redis.VAddArgs{ Reduce: int64(reducedDim), }).Result() expectNil(err) @@ -151,13 +151,13 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { wrongDim := 80 wrongV := generateRandomVector(wrongDim) - _, err = client.VAddArgs(ctx, vecName, "kw", &wrongV, redis.VAddArgs{ + _, err = client.VAddWithArgs(ctx, vecName, "kw", &wrongV, &redis.VAddArgs{ Reduce: int64(reducedDim), }).Result() expectTrue(err != nil) v2 := generateRandomVector(originalDim) - ok, err = client.VAddArgs(ctx, vecName, "k2", &v2, redis.VAddArgs{ + ok, err = client.VAddWithArgs(ctx, vecName, "k2", &v2, &redis.VAddArgs{ Reduce: int64(reducedDim), }).Result() expectNil(err) @@ -257,7 +257,7 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { expectEqual(len(res), len(vals)) // test equality - sim, err := client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + sim, err := client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ Filter: `.age == 25`, }).Result() expectNil(err) @@ -265,21 +265,21 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { expectEqual(sim[0], vals[0].name) // test greater than - sim, err = client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ Filter: `.age > 25`, }).Result() expectNil(err) expectEqual(len(sim), 2) // test less than or equal - sim, err = client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ Filter: `.age <= 30`, }).Result() expectNil(err) expectEqual(len(sim), 2) // test string equality - sim, err = client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ Filter: `.name == "Alice"`, }).Result() expectNil(err) @@ -287,14 +287,14 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { expectEqual(sim[0], vals[0].name) // test string inequality - sim, err = client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ Filter: `.name != "Alice"`, }).Result() expectNil(err) expectEqual(len(sim), 2) // test bool - sim, err = client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ Filter: `.active`, }).Result() expectNil(err) @@ -302,7 +302,7 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { expectEqual(sim[0], vals[0].name) // test logical add - sim, err = client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ Filter: `.age > 20 and .age < 30`, }).Result() expectNil(err) @@ -310,7 +310,7 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { expectEqual(sim[0], vals[0].name) // test logical or - sim, err = client.VSimArgs(ctx, vecName, &vals[0].v, redis.VSimArgs{ + sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ Filter: `.age < 30 or .age > 35`, }).Result() expectNil(err)