From 8fe229b8df1ccf1a9a18124dc845d34b10093fa3 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Sat, 10 May 2025 00:20:39 +0800 Subject: [PATCH 01/10] 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 5fa347f43..2dc362772 100644 --- a/command.go +++ b/command.go @@ -5620,3 +5620,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 92b1abcfff86f0f97c7d5445d11735067332cf35 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Sat, 10 May 2025 00:36:34 +0800 Subject: [PATCH 02/10] 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 ca37c9bfdf5c5af0638c6e5cd70021f29c920ecf Mon Sep 17 00:00:00 2001 From: fukua95 Date: Wed, 21 May 2025 20:42:14 +0800 Subject: [PATCH 03/10] 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 1c3bf4cb650a71dee0eeb05d3fdc83a113278df7 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Wed, 21 May 2025 21:19:47 +0800 Subject: [PATCH 04/10] 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) From d2bb4dc8dcdf52d2cbdd9c7ccf0617110a085290 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Mon, 2 Jun 2025 23:26:16 +0800 Subject: [PATCH 05/10] refact: MapStringFloat64Cmd -> VectorInfoSliceCmd Signed-off-by: fukua95 --- command.go | 34 ++++++++++++++++++++-------------- vectorset_commands.go | 16 ++++++++-------- vectorset_commands_test.go | 10 +++++++--- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/command.go b/command.go index 675388315..543a12fd6 100644 --- a/command.go +++ b/command.go @@ -5621,16 +5621,21 @@ func (cmd *MonitorCmd) Stop() { cmd.status = monitorStatusStop } -type MapStringFloatCmd struct { +type VectorInfo struct { + Name string + Score float64 +} + +type VectorInfoSliceCmd struct { baseCmd - val map[string]float64 + val []VectorInfo } -var _ Cmder = (*MapStringFloatCmd)(nil) +var _ Cmder = (*VectorInfoSliceCmd)(nil) -func NewMapStringFloatCmd(ctx context.Context, args ...any) *MapStringFloatCmd { - return &MapStringFloatCmd{ +func NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorInfoSliceCmd { + return &VectorInfoSliceCmd{ baseCmd: baseCmd{ ctx: ctx, args: args, @@ -5638,40 +5643,41 @@ func NewMapStringFloatCmd(ctx context.Context, args ...any) *MapStringFloatCmd { } } -func (cmd *MapStringFloatCmd) SetVal(val map[string]float64) { +func (cmd *VectorInfoSliceCmd) SetVal(val []VectorInfo) { cmd.val = val } -func (cmd *MapStringFloatCmd) Val() map[string]float64 { +func (cmd *VectorInfoSliceCmd) Val() []VectorInfo { return cmd.val } -func (cmd *MapStringFloatCmd) Result() (map[string]float64, error) { +func (cmd *VectorInfoSliceCmd) Result() ([]VectorInfo, error) { return cmd.val, cmd.err } -func (cmd *MapStringFloatCmd) String() string { +func (cmd *VectorInfoSliceCmd) String() string { return cmdString(cmd, cmd.val) } -func (cmd *MapStringFloatCmd) readReply(rd *proto.Reader) error { +func (cmd *VectorInfoSliceCmd) readReply(rd *proto.Reader) error { n, err := rd.ReadMapLen() if err != nil { return err } - cmd.val = make(map[string]float64, n) + cmd.val = make([]VectorInfo, n) for i := 0; i < n; i++ { - key, err := rd.ReadString() + name, err := rd.ReadString() if err != nil { return err } + cmd.val[i].Name = name - nn, err := rd.ReadFloat() + score, err := rd.ReadFloat() if err != nil { return err } - cmd.val[key] = nn + cmd.val[i].Score = score } return nil } diff --git a/vectorset_commands.go b/vectorset_commands.go index bc2fb35ef..7dd030010 100644 --- a/vectorset_commands.go +++ b/vectorset_commands.go @@ -14,15 +14,15 @@ type VectorSetCmdable interface { 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 + VLinksWithScores(ctx context.Context, key, element string) *VectorInfoSliceCmd 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 + VSimWithScores(ctx context.Context, key string, val Vector) *VectorInfoSliceCmd VSimWithArgs(ctx context.Context, key string, val Vector, args *VSimArgs) *StringSliceCmd - VSimWithArgsWithScores(ctx context.Context, key string, val Vector, args *VSimArgs) *MapStringFloatCmd + VSimWithArgsWithScores(ctx context.Context, key string, val Vector, args *VSimArgs) *VectorInfoSliceCmd } type Vector interface { @@ -183,8 +183,8 @@ func (c cmdable) VLinks(ctx context.Context, key, element string) *StringSliceCm } // `VLINKS key element WITHSCORES` -func (c cmdable) VLinksWithScores(ctx context.Context, key, element string) *MapStringFloatCmd { - cmd := NewMapStringFloatCmd(ctx, "vlinks", key, element, "withscores") +func (c cmdable) VLinksWithScores(ctx context.Context, key, element string) *VectorInfoSliceCmd { + cmd := NewVectorInfoSliceCmd(ctx, "vlinks", key, element, "withscores") _ = c(ctx, cmd) return cmd } @@ -223,7 +223,7 @@ func (c cmdable) VSim(ctx context.Context, key string, val Vector) *StringSliceC } // `VSIM key (ELE | FP32 | VALUES num) (vector | element) WITHSCORES` -func (c cmdable) VSimWithScores(ctx context.Context, key string, val Vector) *MapStringFloatCmd { +func (c cmdable) VSimWithScores(ctx context.Context, key string, val Vector) *VectorInfoSliceCmd { return c.VSimWithArgsWithScores(ctx, key, val, &VSimArgs{}) } @@ -279,7 +279,7 @@ func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simAr // `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) VSimWithArgsWithScores(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *MapStringFloatCmd { +func (c cmdable) VSimWithArgsWithScores(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *VectorInfoSliceCmd { if simArgs == nil { simArgs = &VSimArgs{} } @@ -287,7 +287,7 @@ func (c cmdable) VSimWithArgsWithScores(ctx context.Context, key string, val Vec args = append(args, val.Value()...) args = append(args, "withscores") args = simArgs.appendArgs(args) - cmd := NewMapStringFloatCmd(ctx, args...) + cmd := NewVectorInfoSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd } diff --git a/vectorset_commands_test.go b/vectorset_commands_test.go index b3a7cea98..147fb84c5 100644 --- a/vectorset_commands_test.go +++ b/vectorset_commands_test.go @@ -124,9 +124,13 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { }).Result() expectNil(err) expectEqual(len(sim), 3) - expectTrue(sim["k1"] > 0.99) - expectTrue(sim["k2"] > 0.99) - expectTrue(sim["k3"] < 0.8) + simMap := make(map[string]float64) + for _, vi := range sim { + simMap[vi.Name] = vi.Score + } + expectTrue(simMap["k1"] > 0.99) + expectTrue(simMap["k2"] > 0.99) + expectTrue(simMap["k3"] < 0.8) }) It("dimension operation", func() { From 40c0ecfca7407301f8cd95518f236c9c803d633d Mon Sep 17 00:00:00 2001 From: fukua95 Date: Mon, 2 Jun 2025 23:35:11 +0800 Subject: [PATCH 06/10] update: * the type of vector attribute: string -> VectorAttributeMarshaller * Add a new API VRemAttr * mark the APIs are experimental Signed-off-by: fukua95 --- command.go | 27 ++++++--------- vectorset_commands.go | 69 +++++++++++++++++++++++++++++++------- vectorset_commands_test.go | 4 +-- 3 files changed, 70 insertions(+), 30 deletions(-) diff --git a/command.go b/command.go index 543a12fd6..56b225721 100644 --- a/command.go +++ b/command.go @@ -5621,21 +5621,16 @@ func (cmd *MonitorCmd) Stop() { cmd.status = monitorStatusStop } -type VectorInfo struct { - Name string - Score float64 -} - -type VectorInfoSliceCmd struct { +type VectorScoreSliceCmd struct { baseCmd - val []VectorInfo + val []VectorScore } -var _ Cmder = (*VectorInfoSliceCmd)(nil) +var _ Cmder = (*VectorScoreSliceCmd)(nil) -func NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorInfoSliceCmd { - return &VectorInfoSliceCmd{ +func NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceCmd { + return &VectorScoreSliceCmd{ baseCmd: baseCmd{ ctx: ctx, args: args, @@ -5643,29 +5638,29 @@ func NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorInfoSliceCmd } } -func (cmd *VectorInfoSliceCmd) SetVal(val []VectorInfo) { +func (cmd *VectorScoreSliceCmd) SetVal(val []VectorScore) { cmd.val = val } -func (cmd *VectorInfoSliceCmd) Val() []VectorInfo { +func (cmd *VectorScoreSliceCmd) Val() []VectorScore { return cmd.val } -func (cmd *VectorInfoSliceCmd) Result() ([]VectorInfo, error) { +func (cmd *VectorScoreSliceCmd) Result() ([]VectorScore, error) { return cmd.val, cmd.err } -func (cmd *VectorInfoSliceCmd) String() string { +func (cmd *VectorScoreSliceCmd) String() string { return cmdString(cmd, cmd.val) } -func (cmd *VectorInfoSliceCmd) readReply(rd *proto.Reader) error { +func (cmd *VectorScoreSliceCmd) readReply(rd *proto.Reader) error { n, err := rd.ReadMapLen() if err != nil { return err } - cmd.val = make([]VectorInfo, n) + cmd.val = make([]VectorScore, n) for i := 0; i < n; i++ { name, err := rd.ReadString() if err != nil { diff --git a/vectorset_commands.go b/vectorset_commands.go index 7dd030010..7924616d3 100644 --- a/vectorset_commands.go +++ b/vectorset_commands.go @@ -5,6 +5,7 @@ import ( "strconv" ) +// note: the APIs is experimental and may be subject to change. type VectorSetCmdable interface { VAdd(ctx context.Context, key, element string, val Vector) *BoolCmd VAddWithArgs(ctx context.Context, key, element string, val Vector, addArgs *VAddArgs) *BoolCmd @@ -14,15 +15,16 @@ type VectorSetCmdable interface { 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) *VectorInfoSliceCmd + VLinksWithScores(ctx context.Context, key, element string) *VectorScoreSliceCmd 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 + VSetAttr(ctx context.Context, key, element string, attr VectorAttributeMarshaller) *BoolCmd + VRemAttr(ctx context.Context, key, element string) *BoolCmd VSim(ctx context.Context, key string, val Vector) *StringSliceCmd - VSimWithScores(ctx context.Context, key string, val Vector) *VectorInfoSliceCmd + VSimWithScores(ctx context.Context, key string, val Vector) *VectorScoreSliceCmd VSimWithArgs(ctx context.Context, key string, val Vector, args *VSimArgs) *StringSliceCmd - VSimWithArgsWithScores(ctx context.Context, key string, val Vector, args *VSimArgs) *VectorInfoSliceCmd + VSimWithArgsWithScores(ctx context.Context, key string, val Vector, args *VSimArgs) *VectorScoreSliceCmd } type Vector interface { @@ -42,7 +44,7 @@ func (v *VectorFP32) Value() []any { return []any{vectorFormatFP32, v.Val} } -var _ Vector = &VectorFP32{} +var _ Vector = (*VectorFP32)(nil) type VectorValues struct { Val []float64 @@ -58,7 +60,7 @@ func (v *VectorValues) Value() []any { return res } -var _ Vector = &VectorValues{} +var _ Vector = (*VectorValues)(nil) type VectorRef struct { Name string // the name of the referent vector @@ -68,9 +70,27 @@ func (v *VectorRef) Value() []any { return []any{"ele", v.Name} } -var _ Vector = &VectorRef{} +var _ Vector = (*VectorRef)(nil) + +type VectorScore struct { + Name string + Score float64 +} + +type VectorAttributeMarshaller interface { + Marshall() string +} + +type VectorAttributeRawString string + +func (a *VectorAttributeRawString) Marshall() string { + return string(*a) +} + +var _ VectorAttributeMarshaller = (*VectorAttributeRawString)(nil) // `VADD key (FP32 | VALUES num) vector element` +// note: the API is experimental and may be subject to change. func (c cmdable) VAdd(ctx context.Context, key, element string, val Vector) *BoolCmd { return c.VAddWithArgs(ctx, key, element, val, &VAddArgs{}) } @@ -120,6 +140,7 @@ 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]` +// note: the API is experimental and may be subject to change. func (c cmdable) VAddWithArgs(ctx context.Context, key, element string, val Vector, addArgs *VAddArgs) *BoolCmd { if addArgs == nil { addArgs = &VAddArgs{} @@ -137,6 +158,7 @@ func (c cmdable) VAddWithArgs(ctx context.Context, key, element string, val Vect } // `VCARD key` +// note: the API is experimental and may be subject to change. func (c cmdable) VCard(ctx context.Context, key string) *IntCmd { cmd := NewIntCmd(ctx, "vcard", key) _ = c(ctx, cmd) @@ -144,6 +166,7 @@ func (c cmdable) VCard(ctx context.Context, key string) *IntCmd { } // `VDIM key` +// note: the API is experimental and may be subject to change. func (c cmdable) VDim(ctx context.Context, key string) *IntCmd { cmd := NewIntCmd(ctx, "vdim", key) _ = c(ctx, cmd) @@ -151,6 +174,7 @@ func (c cmdable) VDim(ctx context.Context, key string) *IntCmd { } // `VEMB key element [RAW]` +// note: the API is experimental and may be subject to change. func (c cmdable) VEmb(ctx context.Context, key, element string, raw bool) *SliceCmd { args := []any{"vemb", key, element} if raw { @@ -162,6 +186,7 @@ func (c cmdable) VEmb(ctx context.Context, key, element string, raw bool) *Slice } // `VGETATTR key element` +// note: the API is experimental and may be subject to change. func (c cmdable) VGetAttr(ctx context.Context, key, element string) *StringCmd { cmd := NewStringCmd(ctx, "vgetattr", key, element) _ = c(ctx, cmd) @@ -169,6 +194,7 @@ func (c cmdable) VGetAttr(ctx context.Context, key, element string) *StringCmd { } // `VINFO key` +// note: the API is experimental and may be subject to change. func (c cmdable) VInfo(ctx context.Context, key string) *MapStringInterfaceCmd { cmd := NewMapStringInterfaceCmd(ctx, "vinfo", key) _ = c(ctx, cmd) @@ -176,6 +202,7 @@ func (c cmdable) VInfo(ctx context.Context, key string) *MapStringInterfaceCmd { } // `VLINKS key element` +// note: the API is experimental and may be subject to change. func (c cmdable) VLinks(ctx context.Context, key, element string) *StringSliceCmd { cmd := NewStringSliceCmd(ctx, "vlinks", key, element) _ = c(ctx, cmd) @@ -183,13 +210,15 @@ func (c cmdable) VLinks(ctx context.Context, key, element string) *StringSliceCm } // `VLINKS key element WITHSCORES` -func (c cmdable) VLinksWithScores(ctx context.Context, key, element string) *VectorInfoSliceCmd { +// note: the API is experimental and may be subject to change. +func (c cmdable) VLinksWithScores(ctx context.Context, key, element string) *VectorScoreSliceCmd { cmd := NewVectorInfoSliceCmd(ctx, "vlinks", key, element, "withscores") _ = c(ctx, cmd) return cmd } // `VRANDMEMBER key` +// note: the API is experimental and may be subject to change. func (c cmdable) VRandMember(ctx context.Context, key string) *StringCmd { cmd := NewStringCmd(ctx, "vrandmember", key) _ = c(ctx, cmd) @@ -197,6 +226,7 @@ func (c cmdable) VRandMember(ctx context.Context, key string) *StringCmd { } // `VRANDMEMBER key [count]` +// note: the API is experimental and may be subject to change. func (c cmdable) VRandMemberCount(ctx context.Context, key string, count int) *StringSliceCmd { cmd := NewStringSliceCmd(ctx, "vrandmember", key, count) _ = c(ctx, cmd) @@ -204,6 +234,7 @@ func (c cmdable) VRandMemberCount(ctx context.Context, key string, count int) *S } // `VREM key element` +// note: the API is experimental and may be subject to change. func (c cmdable) VRem(ctx context.Context, key, element string) *BoolCmd { cmd := NewBoolCmd(ctx, "vrem", key, element) _ = c(ctx, cmd) @@ -211,19 +242,31 @@ func (c cmdable) VRem(ctx context.Context, key, element string) *BoolCmd { } // `VSETATTR key element "{ JSON obj }"` -func (c cmdable) VSetAttr(ctx context.Context, key, element, attr string) *BoolCmd { - cmd := NewBoolCmd(ctx, "vsetattr", key, element, attr) +// note: the API is experimental and may be subject to change. +func (c cmdable) VSetAttr(ctx context.Context, key, element string, attr VectorAttributeMarshaller) *BoolCmd { + cmd := NewBoolCmd(ctx, "vsetattr", key, element, attr.Marshall()) + _ = c(ctx, cmd) + return cmd +} + +// `VREMATTR` removes attributes on a vector set element. +// is equal to `VSETATTR key element ""` +// note: the API is experimental and may be subject to change. +func (c cmdable) VRemAttr(ctx context.Context, key, element string) *BoolCmd { + cmd := NewBoolCmd(ctx, "vsetattr", key, element, "") _ = c(ctx, cmd) return cmd } // `VSIM key (ELE | FP32 | VALUES num) (vector | element)` +// note: the API is experimental and may be subject to change. func (c cmdable) VSim(ctx context.Context, key string, val Vector) *StringSliceCmd { 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) *VectorInfoSliceCmd { +// note: the API is experimental and may be subject to change. +func (c cmdable) VSimWithScores(ctx context.Context, key string, val Vector) *VectorScoreSliceCmd { return c.VSimWithArgsWithScores(ctx, key, val, &VSimArgs{}) } @@ -265,6 +308,7 @@ 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]` +// 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 { if simArgs == nil { simArgs = &VSimArgs{} @@ -279,7 +323,8 @@ func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simAr // `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) VSimWithArgsWithScores(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *VectorInfoSliceCmd { +// 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 { if simArgs == nil { simArgs = &VSimArgs{} } diff --git a/vectorset_commands_test.go b/vectorset_commands_test.go index 147fb84c5..30a6ce360 100644 --- a/vectorset_commands_test.go +++ b/vectorset_commands_test.go @@ -195,7 +195,7 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { vals := []struct { name string v redis.VectorValues - attr string + attr redis.VectorAttributeRawString }{ { name: "k0", @@ -237,7 +237,7 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { expectNil(err) expectTrue(ok) if len(v.attr) > 0 { - ok, err = client.VSetAttr(ctx, vecName, v.name, v.attr).Result() + ok, err = client.VSetAttr(ctx, vecName, v.name, &v.attr).Result() expectNil(err) expectTrue(ok) } From e2c26d39f1263b5fe1d6026fc3d5079c6d0b523e Mon Sep 17 00:00:00 2001 From: fukua95 Date: Mon, 2 Jun 2025 23:49:23 +0800 Subject: [PATCH 07/10] trigger CI again Signed-off-by: fukua95 From bd30653968597bcb37743008e4e34c9cfe161c6b Mon Sep 17 00:00:00 2001 From: fukua95 Date: Tue, 3 Jun 2025 21:43:17 +0800 Subject: [PATCH 08/10] rename a API: VRemAttr -> VClearAttributes Signed-off-by: fukua95 --- vectorset_commands.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vectorset_commands.go b/vectorset_commands.go index 7924616d3..31aa7c680 100644 --- a/vectorset_commands.go +++ b/vectorset_commands.go @@ -20,7 +20,7 @@ type VectorSetCmdable interface { VRandMemberCount(ctx context.Context, key string, count int) *StringSliceCmd VRem(ctx context.Context, key, element string) *BoolCmd VSetAttr(ctx context.Context, key, element string, attr VectorAttributeMarshaller) *BoolCmd - VRemAttr(ctx context.Context, key, element string) *BoolCmd + VClearAttributes(ctx context.Context, key, element string) *BoolCmd VSim(ctx context.Context, key string, val Vector) *StringSliceCmd VSimWithScores(ctx context.Context, key string, val Vector) *VectorScoreSliceCmd VSimWithArgs(ctx context.Context, key string, val Vector, args *VSimArgs) *StringSliceCmd @@ -249,10 +249,10 @@ func (c cmdable) VSetAttr(ctx context.Context, key, element string, attr VectorA return cmd } -// `VREMATTR` removes attributes on a vector set element. -// is equal to `VSETATTR key element ""` +// `VClearAttributes` clear attributes on a vector set element. +// The implementation of `VClearAttributes` is execute command `VSETATTR key element ""`. // note: the API is experimental and may be subject to change. -func (c cmdable) VRemAttr(ctx context.Context, key, element string) *BoolCmd { +func (c cmdable) VClearAttributes(ctx context.Context, key, element string) *BoolCmd { cmd := NewBoolCmd(ctx, "vsetattr", key, element, "") _ = c(ctx, cmd) return cmd From 0c8cafb156cacfcdc23cedbdc9aa8c4120545a2f Mon Sep 17 00:00:00 2001 From: fukua95 Date: Tue, 3 Jun 2025 22:10:08 +0800 Subject: [PATCH 09/10] add test Signed-off-by: fukua95 --- vectorset_commands_test.go | 89 +++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/vectorset_commands_test.go b/vectorset_commands_test.go index 30a6ce360..b856e4a29 100644 --- a/vectorset_commands_test.go +++ b/vectorset_commands_test.go @@ -66,11 +66,11 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { It("basic", func() { SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") - vecName := "basic" + vecSetName := "basic" val := &redis.VectorValues{ Val: []float64{1.5, 2.4, 3.3, 4.2}, } - ok, err := client.VAdd(ctx, vecName, "k1", val).Result() + ok, err := client.VAdd(ctx, vecSetName, "k1", val).Result() expectNil(err) expectTrue(ok) @@ -78,48 +78,48 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { val2 := &redis.VectorFP32{ Val: []byte(fp32), } - ok, err = client.VAdd(ctx, vecName, "k2", val2).Result() + ok, err = client.VAdd(ctx, vecSetName, "k2", val2).Result() expectNil(err) expectTrue(ok) - dim, err := client.VDim(ctx, vecName).Result() + dim, err := client.VDim(ctx, vecSetName).Result() expectNil(err) expectEqual(dim, 4) - count, err := client.VCard(ctx, vecName).Result() + count, err := client.VCard(ctx, vecSetName).Result() expectNil(err) expectEqual(count, 2) - ok, err = client.VRem(ctx, vecName, "k1").Result() + ok, err = client.VRem(ctx, vecSetName, "k1").Result() expectNil(err) expectTrue(ok) - count, err = client.VCard(ctx, vecName).Result() + count, err = client.VCard(ctx, vecSetName).Result() expectNil(err) expectEqual(count, 1) }) It("basic similarity", func() { SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") - vecName := "basic_similarity" + vecSetName := "basic_similarity" - ok, err := client.VAdd(ctx, vecName, "k1", &redis.VectorValues{ + ok, err := client.VAdd(ctx, vecSetName, "k1", &redis.VectorValues{ Val: []float64{1, 0, 0, 0}, }).Result() expectNil(err) expectTrue(ok) - ok, err = client.VAdd(ctx, vecName, "k2", &redis.VectorValues{ + ok, err = client.VAdd(ctx, vecSetName, "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{ + ok, err = client.VAdd(ctx, vecSetName, "k3", &redis.VectorValues{ Val: []float64{0.1, 1, -1, 0.5}, }).Result() expectNil(err) expectTrue(ok) - sim, err := client.VSimWithScores(ctx, vecName, &redis.VectorValues{ + sim, err := client.VSimWithScores(ctx, vecSetName, &redis.VectorValues{ Val: []float64{1, 0, 0, 0}, }).Result() expectNil(err) @@ -135,18 +135,18 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { It("dimension operation", func() { SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") - vecName := "dimension_op" + vecSetName := "dimension_op" originalDim := 100 reducedDim := 50 v1 := generateRandomVector(originalDim) - ok, err := client.VAddWithArgs(ctx, vecName, "k1", &v1, &redis.VAddArgs{ + ok, err := client.VAddWithArgs(ctx, vecSetName, "k1", &v1, &redis.VAddArgs{ Reduce: int64(reducedDim), }).Result() expectNil(err) expectTrue(ok) - info, err := client.VInfo(ctx, vecName).Result() + info, err := client.VInfo(ctx, vecSetName).Result() expectNil(err) dim := info["vector-dim"].(int64) oriDim := info["projection-input-dim"].(int64) @@ -155,13 +155,13 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { wrongDim := 80 wrongV := generateRandomVector(wrongDim) - _, err = client.VAddWithArgs(ctx, vecName, "kw", &wrongV, &redis.VAddArgs{ + _, err = client.VAddWithArgs(ctx, vecSetName, "kw", &wrongV, &redis.VAddArgs{ Reduce: int64(reducedDim), }).Result() expectTrue(err != nil) v2 := generateRandomVector(originalDim) - ok, err = client.VAddWithArgs(ctx, vecName, "k2", &v2, &redis.VAddArgs{ + ok, err = client.VAddWithArgs(ctx, vecSetName, "k2", &v2, &redis.VAddArgs{ Reduce: int64(reducedDim), }).Result() expectNil(err) @@ -170,28 +170,28 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { It("remove", func() { SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") - vecName := "remove" + vecSetName := "remove" v1 := generateRandomVector(5) - ok, err := client.VAdd(ctx, vecName, "k1", &v1).Result() + ok, err := client.VAdd(ctx, vecSetName, "k1", &v1).Result() expectNil(err) expectTrue(ok) - exist, err := client.Exists(ctx, vecName).Result() + exist, err := client.Exists(ctx, vecSetName).Result() expectNil(err) expectEqual(exist, 1) - ok, err = client.VRem(ctx, vecName, "k1").Result() + ok, err = client.VRem(ctx, vecSetName, "k1").Result() expectNil(err) expectTrue(ok) - exist, err = client.Exists(ctx, vecName).Result() + exist, err = client.Exists(ctx, vecSetName).Result() expectNil(err) expectEqual(exist, 0) }) It("all operations", func() { SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") - vecName := "commands" + vecSetName := "commands" vals := []struct { name string v redis.VectorValues @@ -224,44 +224,44 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { } // If the key doesn't exist, return null error - _, err := client.VRandMember(ctx, vecName).Result() + _, err := client.VRandMember(ctx, vecSetName).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() + res, err := client.VRandMemberCount(ctx, vecSetName, 3).Result() expectNil(err) expectEqual(len(res), 0) for _, v := range vals { - ok, err := client.VAdd(ctx, vecName, v.name, &v.v).Result() + ok, err := client.VAdd(ctx, vecSetName, 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() + ok, err = client.VSetAttr(ctx, vecSetName, v.name, &v.attr).Result() expectNil(err) expectTrue(ok) } } // VGetAttr - attr, err := client.VGetAttr(ctx, vecName, vals[1].name).Result() + attr, err := client.VGetAttr(ctx, vecSetName, vals[1].name).Result() expectNil(err) - expectEqual(attr, vals[1].attr) + expectEqual(attr, vals[1].attr.Marshall()) // VRandMember - _, err = client.VRandMember(ctx, vecName).Result() + _, err = client.VRandMember(ctx, vecSetName).Result() expectNil(err) - res, err = client.VRandMemberCount(ctx, vecName, 3).Result() + res, err = client.VRandMemberCount(ctx, vecSetName, 3).Result() expectNil(err) expectEqual(len(res), 3) - res, err = client.VRandMemberCount(ctx, vecName, 10).Result() + res, err = client.VRandMemberCount(ctx, vecSetName, 10).Result() expectNil(err) expectEqual(len(res), len(vals)) // test equality - sim, err := client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + sim, err := client.VSimWithArgs(ctx, vecSetName, &vals[0].v, &redis.VSimArgs{ Filter: `.age == 25`, }).Result() expectNil(err) @@ -269,21 +269,21 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { expectEqual(sim[0], vals[0].name) // test greater than - sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + sim, err = client.VSimWithArgs(ctx, vecSetName, &vals[0].v, &redis.VSimArgs{ Filter: `.age > 25`, }).Result() expectNil(err) expectEqual(len(sim), 2) // test less than or equal - sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + sim, err = client.VSimWithArgs(ctx, vecSetName, &vals[0].v, &redis.VSimArgs{ Filter: `.age <= 30`, }).Result() expectNil(err) expectEqual(len(sim), 2) // test string equality - sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + sim, err = client.VSimWithArgs(ctx, vecSetName, &vals[0].v, &redis.VSimArgs{ Filter: `.name == "Alice"`, }).Result() expectNil(err) @@ -291,14 +291,14 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { expectEqual(sim[0], vals[0].name) // test string inequality - sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + sim, err = client.VSimWithArgs(ctx, vecSetName, &vals[0].v, &redis.VSimArgs{ Filter: `.name != "Alice"`, }).Result() expectNil(err) expectEqual(len(sim), 2) // test bool - sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + sim, err = client.VSimWithArgs(ctx, vecSetName, &vals[0].v, &redis.VSimArgs{ Filter: `.active`, }).Result() expectNil(err) @@ -306,7 +306,7 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { expectEqual(sim[0], vals[0].name) // test logical add - sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + sim, err = client.VSimWithArgs(ctx, vecSetName, &vals[0].v, &redis.VSimArgs{ Filter: `.age > 20 and .age < 30`, }).Result() expectNil(err) @@ -314,12 +314,21 @@ var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { expectEqual(sim[0], vals[0].name) // test logical or - sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + sim, err = client.VSimWithArgs(ctx, vecSetName, &vals[0].v, &redis.VSimArgs{ Filter: `.age < 30 or .age > 35`, }).Result() expectNil(err) expectEqual(len(sim), 1) expectEqual(sim[0], vals[0].name) + + // VClearAttributes + ok, err := client.VClearAttributes(ctx, vecSetName, vals[1].name).Result() + expectNil(err) + expectTrue(ok) + + // VGetAttr + _, err = client.VGetAttr(ctx, vecSetName, vals[1].name).Result() + expectEqual(err, proto.Nil) }) }) } From cb291da9f037d476537b59612b4becaaac2f6122 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Wed, 4 Jun 2025 14:01:27 +0300 Subject: [PATCH 10/10] feat(vectorset): improve VSetAttr API and add comprehensive test suite - Simplify VSetAttr to accept interface{} with automatic JSON marshalling - Remove VectorAttributeMarshaller interface for simpler API - Add comprehensive unit tests for all vectorset commands --- unit_test.go | 26 + vectorset_commands.go | 40 +- vectorset_commands_integration_test.go | 326 ++++++++++ vectorset_commands_test.go | 825 ++++++++++++++++--------- 4 files changed, 893 insertions(+), 324 deletions(-) create mode 100644 unit_test.go create mode 100644 vectorset_commands_integration_test.go diff --git a/unit_test.go b/unit_test.go new file mode 100644 index 000000000..e4d0e7b57 --- /dev/null +++ b/unit_test.go @@ -0,0 +1,26 @@ +package redis + +import ( + "context" +) + +// mockCmdable is a mock implementation of cmdable that records the last command. +// This is used for unit testing command construction without requiring a Redis server. +type mockCmdable struct { + lastCmd Cmder + returnErr error +} + +func (m *mockCmdable) call(ctx context.Context, cmd Cmder) error { + m.lastCmd = cmd + if m.returnErr != nil { + cmd.SetErr(m.returnErr) + } + return m.returnErr +} + +func (m *mockCmdable) asCmdable() cmdable { + return func(ctx context.Context, cmd Cmder) error { + return m.call(ctx, cmd) + } +} diff --git a/vectorset_commands.go b/vectorset_commands.go index 31aa7c680..2bd9e2216 100644 --- a/vectorset_commands.go +++ b/vectorset_commands.go @@ -2,6 +2,7 @@ package redis import ( "context" + "encoding/json" "strconv" ) @@ -19,7 +20,7 @@ type VectorSetCmdable interface { 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 string, attr VectorAttributeMarshaller) *BoolCmd + VSetAttr(ctx context.Context, key, element string, attr interface{}) *BoolCmd VClearAttributes(ctx context.Context, key, element string) *BoolCmd VSim(ctx context.Context, key string, val Vector) *StringSliceCmd VSimWithScores(ctx context.Context, key string, val Vector) *VectorScoreSliceCmd @@ -77,18 +78,6 @@ type VectorScore struct { Score float64 } -type VectorAttributeMarshaller interface { - Marshall() string -} - -type VectorAttributeRawString string - -func (a *VectorAttributeRawString) Marshall() string { - return string(*a) -} - -var _ VectorAttributeMarshaller = (*VectorAttributeRawString)(nil) - // `VADD key (FP32 | VALUES num) vector element` // note: the API is experimental and may be subject to change. func (c cmdable) VAdd(ctx context.Context, key, element string, val Vector) *BoolCmd { @@ -242,9 +231,30 @@ func (c cmdable) VRem(ctx context.Context, key, element string) *BoolCmd { } // `VSETATTR key element "{ JSON obj }"` +// The `attr` must be something that can be marshaled to JSON (using encoding/JSON) unless +// the argument is a string or []byte when we assume that it can be passed directly as JSON. +// // note: the API is experimental and may be subject to change. -func (c cmdable) VSetAttr(ctx context.Context, key, element string, attr VectorAttributeMarshaller) *BoolCmd { - cmd := NewBoolCmd(ctx, "vsetattr", key, element, attr.Marshall()) +func (c cmdable) VSetAttr(ctx context.Context, key, element string, attr interface{}) *BoolCmd { + var attrStr string + var err error + switch v := attr.(type) { + case string: + attrStr = v + case []byte: + attrStr = string(v) + default: + var bytes []byte + bytes, err = json.Marshal(v) + if err != nil { + // If marshalling fails, create the command and set the error; this command won't be executed. + cmd := NewBoolCmd(ctx, "vsetattr", key, element, "") + cmd.SetErr(err) + return cmd + } + attrStr = string(bytes) + } + cmd := NewBoolCmd(ctx, "vsetattr", key, element, attrStr) _ = c(ctx, cmd) return cmd } diff --git a/vectorset_commands_integration_test.go b/vectorset_commands_integration_test.go new file mode 100644 index 000000000..147fb84c5 --- /dev/null +++ b/vectorset_commands_integration_test.go @@ -0,0 +1,326 @@ +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) + simMap := make(map[string]float64) + for _, vi := range sim { + simMap[vi.Name] = vi.Score + } + expectTrue(simMap["k1"] > 0.99) + expectTrue(simMap["k2"] > 0.99) + expectTrue(simMap["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.VAddWithArgs(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.VAddWithArgs(ctx, vecName, "kw", &wrongV, &redis.VAddArgs{ + Reduce: int64(reducedDim), + }).Result() + expectTrue(err != nil) + + v2 := generateRandomVector(originalDim) + ok, err = client.VAddWithArgs(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.VSimWithArgs(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.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.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + Filter: `.age <= 30`, + }).Result() + expectNil(err) + expectEqual(len(sim), 2) + + // test string equality + sim, err = client.VSimWithArgs(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.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + Filter: `.name != "Alice"`, + }).Result() + expectNil(err) + expectEqual(len(sim), 2) + + // test bool + sim, err = client.VSimWithArgs(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.VSimWithArgs(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.VSimWithArgs(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) + }) + }) + } +}) diff --git a/vectorset_commands_test.go b/vectorset_commands_test.go index b856e4a29..9dbc8a78f 100644 --- a/vectorset_commands_test.go +++ b/vectorset_commands_test.go @@ -1,335 +1,542 @@ -package redis_test +package redis 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" + "encoding/json" + "reflect" + "testing" ) -func expectNil(err error) { - Expect(err).NotTo(HaveOccurred()) +func TestVectorFP32_Value(t *testing.T) { + v := &VectorFP32{Val: []byte{1, 2, 3}} + got := v.Value() + want := []any{"FP32", []byte{1, 2, 3}} + if !reflect.DeepEqual(got, want) { + t.Errorf("VectorFP32.Value() = %v, want %v", got, want) + } } -func expectTrue(t bool) { - expectEqual(t, true) +func TestVectorValues_Value(t *testing.T) { + v := &VectorValues{Val: []float64{1.1, 2.2}} + got := v.Value() + want := []any{"Values", 2, 1.1, 2.2} + if !reflect.DeepEqual(got, want) { + t.Errorf("VectorValues.Value() = %v, want %v", got, want) + } } -func expectEqual[T any, U any](a T, b U) { - Expect(a).To(BeEquivalentTo(b)) +func TestVectorRef_Value(t *testing.T) { + v := &VectorRef{Name: "foo"} + got := v.Value() + want := []any{"ele", "foo"} + if !reflect.DeepEqual(got, want) { + t.Errorf("VectorRef.Value() = %v, want %v", got, want) + } } -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() +func TestVAdd(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + c.VAdd(context.Background(), "k", "e", vec) + cmd, ok := m.lastCmd.(*BoolCmd) + if !ok { + t.Fatalf("expected BoolCmd, got %T", m.lastCmd) + } + if cmd.args[0] != "vadd" || cmd.args[1] != "k" || cmd.args[len(cmd.args)-1] != "e" { + t.Errorf("unexpected args: %v", cmd.args) } - return redis.VectorValues{Val: v} } -var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { - ctx := context.TODO() +func TestVAddWithArgs_AllOptions(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + args := &VAddArgs{Reduce: 3, Cas: true, NoQuant: true, EF: 5, SetAttr: "attr", M: 2} + c.VAddWithArgs(context.Background(), "k", "e", vec, args) + cmd := m.lastCmd.(*BoolCmd) + found := map[string]bool{} + for _, a := range cmd.args { + if s, ok := a.(string); ok { + found[s] = true + } + } + for _, want := range []string{"reduce", "cas", "noquant", "ef", "setattr", "m"} { + if !found[want] { + t.Errorf("missing arg: %s", want) + } + } +} - setupRedisClient := func(protocolVersion int) *redis.Client { - return redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - DB: 0, - Protocol: protocolVersion, - UnstableResp3: true, - }) +func TestVCard(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VCard(context.Background(), "k") + cmd := m.lastCmd.(*IntCmd) + if cmd.args[0] != "vcard" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) } +} - protocols := []int{2, 3} - for _, protocol := range protocols { - protocol := protocol +func TestVDim(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VDim(context.Background(), "k") + cmd := m.lastCmd.(*IntCmd) + if cmd.args[0] != "vdim" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } +} - Context(fmt.Sprintf("with protocol version %d", protocol), func() { - var client *redis.Client +func TestVEmb(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VEmb(context.Background(), "k", "e", true) + cmd := m.lastCmd.(*SliceCmd) + if cmd.args[0] != "vemb" || cmd.args[1] != "k" || cmd.args[2] != "e" || cmd.args[3] != "raw" { + t.Errorf("unexpected args: %v", cmd.args) + } +} - BeforeEach(func() { - client = setupRedisClient(protocol) - Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred()) - }) +func TestVGetAttr(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VGetAttr(context.Background(), "k", "e") + cmd := m.lastCmd.(*StringCmd) + if cmd.args[0] != "vgetattr" || cmd.args[1] != "k" || cmd.args[2] != "e" { + t.Errorf("unexpected args: %v", cmd.args) + } +} - AfterEach(func() { - if client != nil { - client.FlushDB(ctx) - client.Close() - } - }) +func TestVInfo(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VInfo(context.Background(), "k") + cmd := m.lastCmd.(*MapStringInterfaceCmd) + if cmd.args[0] != "vinfo" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } +} - It("basic", func() { - SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") - vecSetName := "basic" - val := &redis.VectorValues{ - Val: []float64{1.5, 2.4, 3.3, 4.2}, - } - ok, err := client.VAdd(ctx, vecSetName, "k1", val).Result() - expectNil(err) - expectTrue(ok) +func TestVLinks(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VLinks(context.Background(), "k", "e") + cmd := m.lastCmd.(*StringSliceCmd) + if cmd.args[0] != "vlinks" || cmd.args[1] != "k" || cmd.args[2] != "e" { + t.Errorf("unexpected args: %v", cmd.args) + } +} - 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, vecSetName, "k2", val2).Result() - expectNil(err) - expectTrue(ok) - - dim, err := client.VDim(ctx, vecSetName).Result() - expectNil(err) - expectEqual(dim, 4) - - count, err := client.VCard(ctx, vecSetName).Result() - expectNil(err) - expectEqual(count, 2) - - ok, err = client.VRem(ctx, vecSetName, "k1").Result() - expectNil(err) - expectTrue(ok) - - count, err = client.VCard(ctx, vecSetName).Result() - expectNil(err) - expectEqual(count, 1) - }) - - It("basic similarity", func() { - SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") - vecSetName := "basic_similarity" - - ok, err := client.VAdd(ctx, vecSetName, "k1", &redis.VectorValues{ - Val: []float64{1, 0, 0, 0}, - }).Result() - expectNil(err) - expectTrue(ok) - ok, err = client.VAdd(ctx, vecSetName, "k2", &redis.VectorValues{ - Val: []float64{0.99, 0.01, 0, 0}, - }).Result() - expectNil(err) - expectTrue(ok) - ok, err = client.VAdd(ctx, vecSetName, "k3", &redis.VectorValues{ - Val: []float64{0.1, 1, -1, 0.5}, - }).Result() - expectNil(err) - expectTrue(ok) - - sim, err := client.VSimWithScores(ctx, vecSetName, &redis.VectorValues{ - Val: []float64{1, 0, 0, 0}, - }).Result() - expectNil(err) - expectEqual(len(sim), 3) - simMap := make(map[string]float64) - for _, vi := range sim { - simMap[vi.Name] = vi.Score - } - expectTrue(simMap["k1"] > 0.99) - expectTrue(simMap["k2"] > 0.99) - expectTrue(simMap["k3"] < 0.8) - }) - - It("dimension operation", func() { - SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") - vecSetName := "dimension_op" - originalDim := 100 - reducedDim := 50 - - v1 := generateRandomVector(originalDim) - ok, err := client.VAddWithArgs(ctx, vecSetName, "k1", &v1, &redis.VAddArgs{ - Reduce: int64(reducedDim), - }).Result() - expectNil(err) - expectTrue(ok) - - info, err := client.VInfo(ctx, vecSetName).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.VAddWithArgs(ctx, vecSetName, "kw", &wrongV, &redis.VAddArgs{ - Reduce: int64(reducedDim), - }).Result() - expectTrue(err != nil) - - v2 := generateRandomVector(originalDim) - ok, err = client.VAddWithArgs(ctx, vecSetName, "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") - vecSetName := "remove" - v1 := generateRandomVector(5) - ok, err := client.VAdd(ctx, vecSetName, "k1", &v1).Result() - expectNil(err) - expectTrue(ok) - - exist, err := client.Exists(ctx, vecSetName).Result() - expectNil(err) - expectEqual(exist, 1) - - ok, err = client.VRem(ctx, vecSetName, "k1").Result() - expectNil(err) - expectTrue(ok) - - exist, err = client.Exists(ctx, vecSetName).Result() - expectNil(err) - expectEqual(exist, 0) - }) - - It("all operations", func() { - SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") - vecSetName := "commands" - vals := []struct { - name string - v redis.VectorValues - attr redis.VectorAttributeRawString - }{ - { - 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`, - }, - } +func TestVLinksWithScores(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VLinksWithScores(context.Background(), "k", "e") + cmd := m.lastCmd.(*VectorScoreSliceCmd) + if cmd.args[0] != "vlinks" || cmd.args[1] != "k" || cmd.args[2] != "e" || cmd.args[3] != "withscores" { + t.Errorf("unexpected args: %v", cmd.args) + } +} - // If the key doesn't exist, return null error - _, err := client.VRandMember(ctx, vecSetName).Result() - expectEqual(err.Error(), proto.Nil.Error()) - - // If the key doesn't exist, return an empty array - res, err := client.VRandMemberCount(ctx, vecSetName, 3).Result() - expectNil(err) - expectEqual(len(res), 0) - - for _, v := range vals { - ok, err := client.VAdd(ctx, vecSetName, v.name, &v.v).Result() - expectNil(err) - expectTrue(ok) - if len(v.attr) > 0 { - ok, err = client.VSetAttr(ctx, vecSetName, v.name, &v.attr).Result() - expectNil(err) - expectTrue(ok) - } - } +func TestVRandMember(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VRandMember(context.Background(), "k") + cmd := m.lastCmd.(*StringCmd) + if cmd.args[0] != "vrandmember" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } +} - // VGetAttr - attr, err := client.VGetAttr(ctx, vecSetName, vals[1].name).Result() - expectNil(err) - expectEqual(attr, vals[1].attr.Marshall()) - - // VRandMember - _, err = client.VRandMember(ctx, vecSetName).Result() - expectNil(err) - - res, err = client.VRandMemberCount(ctx, vecSetName, 3).Result() - expectNil(err) - expectEqual(len(res), 3) - - res, err = client.VRandMemberCount(ctx, vecSetName, 10).Result() - expectNil(err) - expectEqual(len(res), len(vals)) - - // test equality - sim, err := client.VSimWithArgs(ctx, vecSetName, &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.VSimWithArgs(ctx, vecSetName, &vals[0].v, &redis.VSimArgs{ - Filter: `.age > 25`, - }).Result() - expectNil(err) - expectEqual(len(sim), 2) - - // test less than or equal - sim, err = client.VSimWithArgs(ctx, vecSetName, &vals[0].v, &redis.VSimArgs{ - Filter: `.age <= 30`, - }).Result() - expectNil(err) - expectEqual(len(sim), 2) - - // test string equality - sim, err = client.VSimWithArgs(ctx, vecSetName, &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.VSimWithArgs(ctx, vecSetName, &vals[0].v, &redis.VSimArgs{ - Filter: `.name != "Alice"`, - }).Result() - expectNil(err) - expectEqual(len(sim), 2) - - // test bool - sim, err = client.VSimWithArgs(ctx, vecSetName, &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.VSimWithArgs(ctx, vecSetName, &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.VSimWithArgs(ctx, vecSetName, &vals[0].v, &redis.VSimArgs{ - Filter: `.age < 30 or .age > 35`, - }).Result() - expectNil(err) - expectEqual(len(sim), 1) - expectEqual(sim[0], vals[0].name) - - // VClearAttributes - ok, err := client.VClearAttributes(ctx, vecSetName, vals[1].name).Result() - expectNil(err) - expectTrue(ok) - - // VGetAttr - _, err = client.VGetAttr(ctx, vecSetName, vals[1].name).Result() - expectEqual(err, proto.Nil) - }) +func TestVRandMemberCount(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VRandMemberCount(context.Background(), "k", 5) + cmd := m.lastCmd.(*StringSliceCmd) + if cmd.args[0] != "vrandmember" || cmd.args[1] != "k" || cmd.args[2] != 5 { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVRem(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VRem(context.Background(), "k", "e") + cmd := m.lastCmd.(*BoolCmd) + if cmd.args[0] != "vrem" || cmd.args[1] != "k" || cmd.args[2] != "e" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVSetAttr_String(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VSetAttr(context.Background(), "k", "e", "foo") + cmd := m.lastCmd.(*BoolCmd) + if cmd.args[0] != "vsetattr" || cmd.args[1] != "k" || cmd.args[2] != "e" || cmd.args[3] != "foo" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVSetAttr_Bytes(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VSetAttr(context.Background(), "k", "e", []byte("bar")) + cmd := m.lastCmd.(*BoolCmd) + if cmd.args[3] != "bar" { + t.Errorf("expected 'bar', got %v", cmd.args[3]) + } +} + +func TestVSetAttr_MarshalStruct(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + val := struct{ A int }{A: 1} + c.VSetAttr(context.Background(), "k", "e", val) + cmd := m.lastCmd.(*BoolCmd) + want, _ := json.Marshal(val) + if cmd.args[3] != string(want) { + t.Errorf("expected marshalled struct, got %v", cmd.args[3]) + } +} + +func TestVSetAttr_MarshalError(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + bad := func() {} + cmd := c.VSetAttr(context.Background(), "k", "e", bad) + if cmd.Err() == nil { + t.Error("expected error for non-marshallable value") + } +} + +func TestVClearAttributes(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VClearAttributes(context.Background(), "k", "e") + cmd := m.lastCmd.(*BoolCmd) + if cmd.args[0] != "vsetattr" || cmd.args[3] != "" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVSim(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + c.VSim(context.Background(), "k", vec) + cmd := m.lastCmd.(*StringSliceCmd) + if cmd.args[0] != "vsim" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVSimWithScores(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + c.VSimWithScores(context.Background(), "k", vec) + cmd := m.lastCmd.(*VectorScoreSliceCmd) + if cmd.args[0] != "vsim" || cmd.args[1] != "k" || cmd.args[len(cmd.args)-1] != "withscores" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVSimWithArgs_AllOptions(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + args := &VSimArgs{Count: 2, EF: 3, Filter: "f", FilterEF: 4, Truth: true, NoThread: true} + c.VSimWithArgs(context.Background(), "k", vec, args) + cmd := m.lastCmd.(*StringSliceCmd) + found := map[string]bool{} + for _, a := range cmd.args { + if s, ok := a.(string); ok { + found[s] = true + } + } + for _, want := range []string{"count", "ef", "filter", "filter-ef", "truth", "nothread"} { + if !found[want] { + t.Errorf("missing arg: %s", want) + } + } +} + +func TestVSimWithArgsWithScores_AllOptions(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + args := &VSimArgs{Count: 2, EF: 3, Filter: "f", FilterEF: 4, Truth: true, NoThread: true} + c.VSimWithArgsWithScores(context.Background(), "k", vec, args) + cmd := m.lastCmd.(*VectorScoreSliceCmd) + found := map[string]bool{} + for _, a := range cmd.args { + if s, ok := a.(string); ok { + found[s] = true + } + } + for _, want := range []string{"count", "ef", "filter", "filter-ef", "truth", "nothread", "withscores"} { + if !found[want] { + t.Errorf("missing arg: %s", want) + } + } +} + +// Additional tests for missing coverage + +func TestVectorValues_EmptySlice(t *testing.T) { + v := &VectorValues{Val: []float64{}} + got := v.Value() + want := []any{"Values", 0} + if !reflect.DeepEqual(got, want) { + t.Errorf("VectorValues.Value() with empty slice = %v, want %v", got, want) + } +} + +func TestVEmb_WithoutRaw(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VEmb(context.Background(), "k", "e", false) + cmd := m.lastCmd.(*SliceCmd) + if cmd.args[0] != "vemb" || cmd.args[1] != "k" || cmd.args[2] != "e" { + t.Errorf("unexpected args: %v", cmd.args) + } + if len(cmd.args) != 3 { + t.Errorf("expected 3 args when raw=false, got %d", len(cmd.args)) + } +} + +func TestVAddWithArgs_Q8Option(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + args := &VAddArgs{Q8: true} + c.VAddWithArgs(context.Background(), "k", "e", vec, args) + cmd := m.lastCmd.(*BoolCmd) + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == "q8" { + found = true + break + } + } + if !found { + t.Error("missing q8 arg") + } +} + +func TestVAddWithArgs_BinOption(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + args := &VAddArgs{Bin: true} + c.VAddWithArgs(context.Background(), "k", "e", vec, args) + cmd := m.lastCmd.(*BoolCmd) + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == "bin" { + found = true + break + } + } + if !found { + t.Error("missing bin arg") + } +} + +func TestVAddWithArgs_NilArgs(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + c.VAddWithArgs(context.Background(), "k", "e", vec, nil) + cmd := m.lastCmd.(*BoolCmd) + if cmd.args[0] != "vadd" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVSimWithArgs_NilArgs(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + c.VSimWithArgs(context.Background(), "k", vec, nil) + cmd := m.lastCmd.(*StringSliceCmd) + if cmd.args[0] != "vsim" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVSimWithArgsWithScores_NilArgs(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + c.VSimWithArgsWithScores(context.Background(), "k", vec, nil) + cmd := m.lastCmd.(*VectorScoreSliceCmd) + if cmd.args[0] != "vsim" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } + // Should still have withscores + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == "withscores" { + found = true + break + } + } + if !found { + t.Error("missing withscores arg") + } +} + +func TestVAdd_WithVectorFP32(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorFP32{Val: []byte{1, 2, 3, 4}} + c.VAdd(context.Background(), "k", "e", vec) + cmd := m.lastCmd.(*BoolCmd) + if cmd.args[0] != "vadd" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } + // Check that FP32 format is used + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == "FP32" { + found = true + break + } + } + if !found { + t.Error("missing FP32 format in args") + } +} + +func TestVAdd_WithVectorRef(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorRef{Name: "ref-vector"} + c.VAdd(context.Background(), "k", "e", vec) + cmd := m.lastCmd.(*BoolCmd) + if cmd.args[0] != "vadd" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } + // Check that ele format is used + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == "ele" { + found = true + break + } + } + if !found { + t.Error("missing ele format in args") + } +} + +func TestVSim_WithVectorFP32(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorFP32{Val: []byte{1, 2, 3, 4}} + c.VSim(context.Background(), "k", vec) + cmd := m.lastCmd.(*StringSliceCmd) + if cmd.args[0] != "vsim" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } + // Check that FP32 format is used + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == "FP32" { + found = true + break + } + } + if !found { + t.Error("missing FP32 format in args") + } +} + +func TestVSim_WithVectorRef(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorRef{Name: "ref-vector"} + c.VSim(context.Background(), "k", vec) + cmd := m.lastCmd.(*StringSliceCmd) + if cmd.args[0] != "vsim" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } + // Check that ele format is used + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == "ele" { + found = true + break + } + } + if !found { + t.Error("missing ele format in args") + } +} + +func TestVAddWithArgs_ReduceOption(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + args := &VAddArgs{Reduce: 128} + c.VAddWithArgs(context.Background(), "k", "e", vec, args) + cmd := m.lastCmd.(*BoolCmd) + // Check that reduce appears early in args (after key) + if cmd.args[0] != "vadd" || cmd.args[1] != "k" || cmd.args[2] != "reduce" { + t.Errorf("unexpected args order: %v", cmd.args) + } +} + +func TestVAddWithArgs_ZeroValues(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + args := &VAddArgs{Reduce: 0, EF: 0, M: 0} // Zero values should not appear in args + c.VAddWithArgs(context.Background(), "k", "e", vec, args) + cmd := m.lastCmd.(*BoolCmd) + // Check that zero values don't appear + for _, a := range cmd.args { + if s, ok := a.(string); ok { + if s == "reduce" || s == "ef" || s == "m" { + t.Errorf("zero value option should not appear in args: %s", s) + } + } + } +} + +func TestVSimArgs_IndividualOptions(t *testing.T) { + tests := []struct { + name string + args *VSimArgs + want string + }{ + {"Count", &VSimArgs{Count: 5}, "count"}, + {"EF", &VSimArgs{EF: 10}, "ef"}, + {"Filter", &VSimArgs{Filter: "test"}, "filter"}, + {"FilterEF", &VSimArgs{FilterEF: 15}, "filter-ef"}, + {"Truth", &VSimArgs{Truth: true}, "truth"}, + {"NoThread", &VSimArgs{NoThread: true}, "nothread"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + c.VSimWithArgs(context.Background(), "k", vec, tt.args) + cmd := m.lastCmd.(*StringSliceCmd) + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == tt.want { + found = true + break + } + } + if !found { + t.Errorf("missing arg: %s", tt.want) + } }) } -}) +}