Redis 允許您在哈希或JSON對象中索引向量字段(更多信息請參閱向量參考頁面)。向量字段可以存儲(chǔ)文本embedding等內(nèi)容,文本embedding是 AI 生成的向量表示,用于表示文本片段中的語義信息。兩個(gè)embedding之間的向量距離表明它們在語義上的相似程度。通過比較從查詢文本生成的embedding與存儲(chǔ)在哈?;?JSON 字段中的embedding的相似性,Redis 可以檢索與查詢的含義密切相關(guān)的文檔。
創(chuàng)建索引
func doCreateIndex() (err error) {
var (
RedisKeyPrefix = "doc:"
IndexName = "vactor_test"
Dimension = 2560 //| 實(shí)際字節(jié)數(shù) | 你傳進(jìn)來的 blob 長度 | 10240 字節(jié) | → 10240 ÷ 4 = 2560 維
)
cli := NewRedisStackClient("localhost:6379", "", 0)
ctx := context.Background()
// 確保在錯(cuò)誤時(shí)關(guān)閉連接
defer func() {
if err != nil {
cli.Client.Close()
}
}()
if err = cli.Client.Ping(ctx).Err(); err != nil {
return fmt.Errorf("failed to connect to Redis: %w", err)
}
indexName := fmt.Sprintf("%s%s", RedisKeyPrefix, IndexName)
// 檢查是否存在索引
exists, err := cli.Client.Do(ctx, "FT.INFO", indexName).Result()
if err != nil {
if !strings.Contains(err.Error(), "Unknown index name") {
return fmt.Errorf("failed to check if index exists: %w", err)
}
err = nil
} else if exists != nil {
return nil
}
// Create new index
createIndexArgs := []interface{}{
"FT.CREATE", indexName,
"ON", "HASH", //-- 數(shù)據(jù)載體:JSON 或 HASH
"PREFIX", "1", RedisKeyPrefix, // -- 只掃描以 doc: 開頭的鍵
"SCHEMA",
"content", "TEXT", //-- 業(yè)務(wù)字段示例content, 文本類型
"genre", "TAG", //-- 業(yè)務(wù)字段示例metadata, TAG類型
"embedding", "VECTOR", "FLAT", //-- 業(yè)務(wù)字段示例vector, 向量類型
"6", //-- 6 = 接下來 6 個(gè)參數(shù)
"TYPE", "FLOAT32", //向量元素類型
"DIM", Dimension, //向量維度,與模型一致
"DISTANCE_METRIC", "COSINE", //距離算法:COSINE / L2 / IP
}
if err = cli.Client.Do(ctx, createIndexArgs...).Err(); err != nil {
return fmt.Errorf("failed to create index: %w", err)
}
// 驗(yàn)證索引是否創(chuàng)建成功
if _, err = cli.Client.Do(ctx, "FT.INFO", indexName).Result(); err != nil {
return fmt.Errorf("failed to verify index creation: %w", err)
}
return nil
}
索引:doc:vactor_test只掃描以 doc: 開頭的鍵 , 包含三個(gè)字段:
- content: 內(nèi)容, TEXT文本類型
- genre: 類型, TAG標(biāo)簽類型
- embedding:向量, VECTOR 類型,F(xiàn)LAT標(biāo)識(shí)向量檢索的方式(HNSW 適合在線、高并發(fā)近似搜索;若數(shù)據(jù)量很小可用 FLAT(暴力線性掃描)), FLAT的原理: 不做任何近似或聚類(與 HNSW / IVF 不同)。每次 FT.SEARCH … KNN 都把查詢向量與索引中的 每一條向量 計(jì)算距離,再排序取 Top-k。因此 內(nèi)存占用高(需要完整存儲(chǔ)原始向量),但 召回率 100 %,也無額外調(diào)參。適用場景為數(shù)據(jù)量 ≤ 幾萬條或延遲要求不高。
DIM , 標(biāo)識(shí)向量維度, 必須與模型一直, 這里使用的是字節(jié)ARK的模型,維度為 2560, 在 給 RediSearch 建索引時(shí),你需把 DIM 設(shè)成實(shí)際要用的那個(gè)值, 在使用時(shí),因一開始不確定維度數(shù),設(shè)置的值384, 系統(tǒng)報(bào)以下錯(cuò)誤:
Could not add vector with blob size 10240 (expected size 1536)"
期望字節(jié)數(shù): DIM × 4, 即 384 × 4 = 1536 字節(jié), 但實(shí)際ARK返回的向量維度為10240 * 4 = 2560(除4是因?yàn)閒loat32 占4字節(jié)), 故刪除索引后重建索引, 修改DIM值為2560, 上述問題解決, 刪除索引:
func dropIndex() error {
cli := NewRedisStackClient("localhost:6379", "", 0)
ctx := context.Background()
return cli.Client.FTDropIndex(ctx, "doc:vactor_test").Err()
}
DISTANCE_METRIC 用來告訴 RediSearch 向量之間的距離如何計(jì)算。目前支持三種,選其一即可:
| 取值 | 全稱 | 公式(簡化) | 適用場景 |
|---|---|---|---|
| COSINE | Cosine Similarity | 1 ? cos(θ) | 文本 / 語義向量,長度已歸一化 |
| L2 | Euclidean Distance | √Σ(xi ? yi)2 | 通用數(shù)值向量,維度量綱一致 |
| IP | Inner Product | ? Σ(xi·yi) | 已歸一化且需要最大化內(nèi)積 |
基于字節(jié)ARK的embedding 模型對文本向量化,并存在在redis中
func addEmbeddins() (err error) {
cli := NewRedisStackClient("localhost:6379", "", 0)
ctx := context.Background()
// 確保在錯(cuò)誤時(shí)關(guān)閉連接
defer func() {
if err != nil {
cli.Client.Close()
}
}()
sentences := []string{
"That is a very happy person",
"That is a happy dog",
"Today is a sunny day",
}
tags := []string{
"persons", "pets", "weather",
}
config := &ark.EmbeddingConfig{
Model: os.Getenv("ARK_EMBEDDING_MODEL"),
APIKey: os.Getenv("ARK_API_KEY"),
}
eb, err := ark.NewEmbedder(ctx, config)
if err != nil {
return err
}
embeddings, err := eb.EmbedStrings(ctx, sentences)
if err != nil {
return err
}
for i, emb := range embeddings {
buffer := utils.Vector2Bytes(emb)
if err != nil {
return err
}
count, err := cli.Client.HSet(ctx,
fmt.Sprintf("doc:%v", i),
map[string]any{
"content": sentences[i],
"genre": tags[i],
"embedding": buffer,
},
).Result()
if err != nil {
return err
}
}
return nil
}
字節(jié)開源的大模型應(yīng)用框架eino-ext 已封裝Ark了對應(yīng)的Embedder組件, 用于文檔的向量化,需引入依賴: github.com/cloudwego/eino-ext/components/embedding/ark
Embedder組件的EmbedStrings方法,入?yún)閟tring類型的切片, 出參為一個(gè)float64類型的二維數(shù)組, 對應(yīng)每個(gè)字符串向量值, 存儲(chǔ)時(shí)需要將float64轉(zhuǎn)為float32 后再轉(zhuǎn)bytes 切片存儲(chǔ):
func Vector2Bytes(vector []float64) []byte {
float32Arr := make([]float32, len(vector))
for i, v := range vector {
float32Arr[i] = float32(v)
}
bytes := make([]byte, len(float32Arr)*4)
for i, v := range float32Arr {
binary.LittleEndian.PutUint32(bytes[i*4:], math.Float32bits(v))
}
return bytes
}
存入Redis 中的數(shù)據(jù)如下圖所示:

向量查詢
func doVectorQuery() (err error) {
var (
RedisKeyPrefix = "doc:"
IndexName = "vactor_test"
queryStr = []string{"That is a happy person"}
)
cli := NewRedisStackClient("localhost:6379", "", 0)
ctx := context.Background()
config := &ark.EmbeddingConfig{
Model: os.Getenv("ARK_EMBEDDING_MODEL"),
APIKey: os.Getenv("ARK_API_KEY"),
}
eb, err := ark.NewEmbedder(ctx, config)
if err != nil {
return err
}
embeddings, err := eb.EmbedStrings(ctx, queryStr)
if err != nil {
panic(err)
}
buffer := utils.FloatsToBytes(utils.ToFloat32(embeddings[0]))
if err != nil {
panic(err)
}
indexName := fmt.Sprintf("%s%s", RedisKeyPrefix, IndexName)
//*=>[ ... ]
//RediSearch 的 “向量查詢子句” 語法糖,* 代表“所有文檔”,=> 后面放向量運(yùn)算。
//KNN 3
//取 3 個(gè)最近鄰(k-nearest-neighbors)。
//@embedding
//指定 要比較的向量字段(索引里必須事先聲明為 VECTOR 類型)。
//$vec
//用戶傳入的查詢向量,需在 PARAMS 里綁定,例如 PARAMS 2 vec <base64或float數(shù)組>。
//AS vector_distance
//把計(jì)算出的距離(或相似度)作為 返回字段,后續(xù)可在 RETURN / SORTBY / DIALECT 里引用。
results, err := cli.Client.FTSearchWithArgs(ctx,
indexName,
"*=>[KNN 3 @embedding $vec AS vector_distance]",
&redis.FTSearchOptions{
Return: []redis.FTSearchReturn{
{FieldName: "vector_distance"},
{FieldName: "content"},
},
DialectVersion: 2,
Params: map[string]any{
"vec": buffer,
},
},
).Result()
if err != nil {
panic(err)
}
for _, doc := range results.Docs {
fmt.Printf(
"ID: %v, Distance:%v, Content:'%v'\n",
doc.ID, doc.Fields["vector_distance"], doc.Fields["content"],
)
}
return nil
}
向量查詢語句That is a happy person, 同樣需要使用的Ark的Embedder組件進(jìn)行向量化, 將Floate64類型轉(zhuǎn)為float32類型后轉(zhuǎn)bytes切片, 進(jìn)行匹配查詢:
results, err := cli.Client.FTSearchWithArgs(ctx,
indexName,
"*=>[KNN 3 @embedding $vec AS vector_distance]",
&redis.FTSearchOptions{
Return: []redis.FTSearchReturn{
{FieldName: "vector_distance"},
{FieldName: "content"},
},
DialectVersion: 2,
Params: map[string]any{
"vec": buffer,
},
},
).Result()
-
=>[ ... ]
RediSearch 的 “向量查詢子句” 語法糖, 代表“所有文檔”,=> 后面放向量運(yùn)算。 - KNN 3
取 3 個(gè)最近鄰(k-nearest-neighbors)。 - @embedding
指定 要比較的向量字段(索引里必須事先聲明為 VECTOR 類型)。 - $vec
用戶傳入的查詢向量,需在 Params里綁定, 統(tǒng)一放入到map里。 - AS vector_distance
把計(jì)算出的距離(或相似度)作為 返回字段,后續(xù)可在 RETURN / SORTBY / DIALECT 里引用。
上述查詢語句返回除ID之外聲明的vector_distance和content字段, DialectVersion(在 Redis/RediSearch 中常寫作 DIALECT)是 查詢語法版本號(hào),用來告訴 RediSearch 用哪一套解析器去解釋你的 FT.SEARCH、FT.AGGREGATE 等命令。向量檢索、參數(shù)綁定、JSON 多值字段 必須 DIALECT ≥ 2,否則會(huì)報(bào)錯(cuò) “syntax error”。
That is a happy person 語句的查詢返回輸出:
