1
0
mirror of https://github.com/redis/go-redis.git synced 2025-07-18 00:20:57 +03:00

feat: support vectorset (#3375)

* feat: support vectorset

* fix: char encoding error

* use `any` instread of `interface{}`

* update vectorset API

Signed-off-by: fukua95 <fukua95@gmail.com>

* refact: MapStringFloat64Cmd -> VectorInfoSliceCmd

Signed-off-by: fukua95 <fukua95@gmail.com>

* update:

* the type of vector attribute: string -> VectorAttributeMarshaller
* Add a new API VRemAttr
* mark the APIs are experimental

Signed-off-by: fukua95 <fukua95@gmail.com>

* trigger CI again

Signed-off-by: fukua95 <fukua95@gmail.com>

* rename a API: VRemAttr -> VClearAttributes

Signed-off-by: fukua95 <fukua95@gmail.com>

* add test

Signed-off-by: fukua95 <fukua95@gmail.com>

* 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

---------

Signed-off-by: fukua95 <fukua95@gmail.com>
Co-authored-by: Nedyalko Dyakov <nedyalko.dyakov@gmail.com>
This commit is contained in:
fukua95
2025-06-05 16:35:45 +08:00
committed by GitHub
parent c609828c9b
commit 4e22885ca1
6 changed files with 1299 additions and 0 deletions

View File

@ -5620,3 +5620,59 @@ func (cmd *MonitorCmd) Stop() {
defer cmd.mu.Unlock()
cmd.status = monitorStatusStop
}
type VectorScoreSliceCmd struct {
baseCmd
val []VectorScore
}
var _ Cmder = (*VectorScoreSliceCmd)(nil)
func NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceCmd {
return &VectorScoreSliceCmd{
baseCmd: baseCmd{
ctx: ctx,
args: args,
},
}
}
func (cmd *VectorScoreSliceCmd) SetVal(val []VectorScore) {
cmd.val = val
}
func (cmd *VectorScoreSliceCmd) Val() []VectorScore {
return cmd.val
}
func (cmd *VectorScoreSliceCmd) Result() ([]VectorScore, error) {
return cmd.val, cmd.err
}
func (cmd *VectorScoreSliceCmd) String() string {
return cmdString(cmd, cmd.val)
}
func (cmd *VectorScoreSliceCmd) readReply(rd *proto.Reader) error {
n, err := rd.ReadMapLen()
if err != nil {
return err
}
cmd.val = make([]VectorScore, n)
for i := 0; i < n; i++ {
name, err := rd.ReadString()
if err != nil {
return err
}
cmd.val[i].Name = name
score, err := rd.ReadFloat()
if err != nil {
return err
}
cmd.val[i].Score = score
}
return nil
}

View File

@ -234,6 +234,7 @@ type Cmdable interface {
StreamCmdable
TimeseriesCmdable
JSONCmdable
VectorSetCmdable
}
type StatefulCmdable interface {

26
unit_test.go Normal file
View File

@ -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)
}
}

348
vectorset_commands.go Normal file
View File

@ -0,0 +1,348 @@
package redis
import (
"context"
"encoding/json"
"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
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) *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 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
VSimWithArgs(ctx context.Context, key string, val Vector, args *VSimArgs) *StringSliceCmd
VSimWithArgsWithScores(ctx context.Context, key string, val Vector, args *VSimArgs) *VectorScoreSliceCmd
}
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)(nil)
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)(nil)
type VectorRef struct {
Name string // the name of the referent vector
}
func (v *VectorRef) Value() []any {
return []any{"ele", v.Name}
}
var _ Vector = (*VectorRef)(nil)
type VectorScore struct {
Name string
Score float64
}
// `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{})
}
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]`
// 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{}
}
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`
// 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)
return cmd
}
// `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)
return cmd
}
// `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 {
args = append(args, "raw")
}
cmd := NewSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// `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)
return cmd
}
// `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)
return cmd
}
// `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)
return cmd
}
// `VLINKS key element WITHSCORES`
// 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)
return cmd
}
// `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)
return cmd
}
// `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)
return cmd
}
// `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 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
}
// `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) VClearAttributes(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`
// 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{})
}
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]`
// 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{}
}
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]`
// 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{}
}
args := []any{"vsim", key}
args = append(args, val.Value()...)
args = append(args, "withscores")
args = simArgs.appendArgs(args)
cmd := NewVectorInfoSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}

View File

@ -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)
})
})
}
})

542
vectorset_commands_test.go Normal file
View File

@ -0,0 +1,542 @@
package redis
import (
"context"
"encoding/json"
"reflect"
"testing"
)
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 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 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 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)
}
}
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)
}
}
}
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)
}
}
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)
}
}
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)
}
}
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)
}
}
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)
}
}
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)
}
}
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)
}
}
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)
}
}
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)
}
})
}
}