索引和查询向量
了解如何使用 Redis 索引和查询向量嵌入
Redis Query Engine 允许您在哈希或JSON对象中索引向量字段(更多信息请参阅向量参考页面)。向量字段可以存储文本嵌入等内容,文本嵌入是 AI 生成的向量表示,用于表示文本片段中的语义信息。两个嵌入之间的向量距离表明它们在语义上的相似程度。通过比较从查询文本生成的嵌入与存储在哈希或 JSON 字段中的嵌入的相似性,Redis 可以检索与查询的含义密切相关的文档。
在下面的示例中,我们使用 huggingfaceembedder
包(来自 LinGoose
框架)生成向量嵌入,以便通过 Redis Query Engine 进行存储和索引。代码首先演示了哈希文档的使用,并有一个单独的部分解释与 JSON 文档的区别。
初始化
使用以下命令启动一个新的 Go 模块
go mod init vecexample
然后,在您的模块文件夹中,安装go-redis
和huggingfaceembedder
包
go get github.com/redis/go-redis/v9
go get github.com/henomis/lingoose/embedder/huggingface
将以下导入添加到模块的主程序文件中
package main
import (
"context"
"encoding/binary"
"fmt"
"math"
huggingfaceembedder "github.com/henomis/lingoose/embedder/huggingface"
"github.com/redis/go-redis/v9"
)
您还必须创建一个HuggingFace 账户并添加新的访问令牌才能使用嵌入模型。请参阅HuggingFace文档了解如何创建和管理访问令牌。请注意,我们将用于生成此示例嵌入的账户和 all-MiniLM-L6-v2
模型都是免费提供的。
添加一个辅助函数
huggingfaceembedder
模型将嵌入输出为 []float32
数组。如果您将文档存储为哈希对象,则在将其作为哈希字段添加之前,必须将此数组转换为 byte
字符串。下面所示的函数使用 Go 的binary
包生成 byte
字符串
func floatsToBytes(fs []float32) []byte {
buf := make([]byte, len(fs)*4)
for i, f := range fs {
u := math.Float32bits(f)
binary.NativeEndian.PutUint32(buf[i*4:], u)
}
return buf
}
请注意,如果您使用JSON对象而不是哈希来存储文档,则应直接存储 []float32
数组,而无需先将其转换为 byte
字符串(请参阅下面的与 JSON 文档的区别)。
创建索引
在 main()
函数中,连接到 Redis 并删除之前使用名称 vector_idx
创建的任何索引
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
Protocol: 2,
})
rdb.FTDropIndexWithArgs(ctx,
"vector_idx",
&redis.FTDropIndexOptions{
DeleteDocs: true,
},
)
接下来,创建索引。下面的示例中的 schema 指定使用哈希对象进行存储,并包含三个字段:要索引的文本内容,一个表示文本“类别”的标签字段,以及从原始文本内容生成的嵌入向量。embedding
字段指定HNSW索引、L2向量距离度量、用于表示向量分量的 Float32
值,以及 384 维,这是 all-MiniLM-L6-v2
嵌入模型的要求。
_, err := rdb.FTCreate(ctx,
"vector_idx",
&redis.FTCreateOptions{
OnHash: true,
Prefix: []any{"doc:"},
},
&redis.FieldSchema{
FieldName: "content",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "genre",
FieldType: redis.SearchFieldTypeTag,
},
&redis.FieldSchema{
FieldName: "embedding",
FieldType: redis.SearchFieldTypeVector,
VectorArgs: &redis.FTVectorArgs{
HNSWOptions: &redis.FTHNSWOptions{
Dim: 384,
DistanceMetric: "L2",
Type: "FLOAT32",
},
},
},
).Result()
if err != nil {
panic(err)
}
创建一个嵌入器实例
您需要一个 huggingfaceembedder
类的实例来生成嵌入。使用以下代码创建一个使用 sentence-transformers/all-MiniLM-L6-v2
模型的实例,并将您的 HuggingFace 访问令牌传递给 WithToken()
方法。
hf := huggingfaceembedder.New().
WithToken("<your-access-token>").
WithModel("sentence-transformers/all-MiniLM-L6-v2")
添加数据
您现在可以提供数据对象,只要您使用索引定义中指定的 doc:
前缀,使用HSet()
添加它们时就会自动索引。
如下所示使用 huggingfacetransformer
的 Embed()
方法创建表示 content
字段的嵌入。此方法接受一个字符串数组,并输出相应的 Embedding
对象数组。使用 Embedding
的 ToFloat32()
方法生成我们需要的浮点值数组,并使用我们上面定义的 floatsToBytes()
函数将此数组转换为 byte
字符串。
sentences := []string{
"That is a very happy person",
"That is a happy dog",
"Today is a sunny day",
}
tags := []string{
"persons", "pets", "weather",
}
embeddings, err := hf.Embed(ctx, sentences)
if err != nil {
panic(err)
}
for i, emb := range embeddings {
buffer := floatsToBytes(emb.ToFloat32())
if err != nil {
panic(err)
}
_, err = rdb.HSet(ctx,
fmt.Sprintf("doc:%v", i),
map[string]any{
"content": sentences[i],
"genre": tags[i],
"embedding": buffer,
},
).Result()
if err != nil {
panic(err)
}
}
运行查询
创建索引并添加数据后,即可运行查询。为此,您必须从所选的查询文本创建另一个嵌入向量。Redis 在运行查询时会计算查询向量与索引中每个嵌入向量之间的相似度。然后,它会按照此数值相似度值对结果进行排名。
下面的代码使用 Embed()
创建查询嵌入,就像索引一样,并在查询执行时将其作为参数传递(有关使用查询参数进行嵌入的更多信息,请参阅向量搜索)。
queryEmbedding, err := hf.Embed(ctx, []string{
"That is a happy person",
})
if err != nil {
panic(err)
}
buffer := floatsToBytes(queryEmbedding[0].ToFloat32())
if err != nil {
panic(err)
}
results, err := rdb.FTSearchWithArgs(ctx,
"vector_idx",
"*=>[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"],
)
}
代码现在可以运行了,但请注意,首次运行时可能需要一些时间才能完成(这是因为 huggingfacetransformer
在生成嵌入之前必须下载 all-MiniLM-L6-v2
模型数据)。运行代码时,它会输出以下文本
ID: doc:0, Distance:0.114169843495, Content:'That is a very happy person'
ID: doc:1, Distance:0.610845327377, Content:'That is a happy dog'
ID: doc:2, Distance:1.48624765873, Content:'Today is a sunny day'
结果按照 vector_distance
字段的值排序,距离最低表示与查询的相似度最高。正如您所料,内容文本为 "That is a very happy person" 的 doc:0
的结果与查询文本 "That is a happy person" 在含义上最为相似。
与 JSON 文档的区别
索引 JSON 文档类似于哈希索引,但有一些重要的区别。JSON 允许使用嵌套字段进行更丰富的数据建模,因此您必须在 schema 中提供一个路径来标识您想要索引的每个字段。但是,您可以为这些路径中的每一个声明一个短别名(使用 As
选项),以避免在每次查询时完整输入。此外,在创建索引时必须将 OnJSON
设置为 true
。
下面的代码展示了这些区别,但索引与之前为哈希创建的索引非常相似
_, err = rdb.FTCreate(ctx,
"vector_json_idx",
&redis.FTCreateOptions{
OnJSON: true,
Prefix: []any{"jdoc:"},
},
&redis.FieldSchema{
FieldName: "$.content",
As: "content",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.genre",
As: "genre",
FieldType: redis.SearchFieldTypeTag,
},
&redis.FieldSchema{
FieldName: "$.embedding",
As: "embedding",
FieldType: redis.SearchFieldTypeVector,
VectorArgs: &redis.FTVectorArgs{
HNSWOptions: &redis.FTHNSWOptions{
Dim: 384,
DistanceMetric: "L2",
Type: "FLOAT32",
},
},
},
).Result()
使用JSONSet()
添加数据,而不是HSet()
。指定字段的 map 具有与用于 HSet()
的 map 相同的结构。
JSON 索引的一个重要区别在于,向量使用列表而不是二进制字符串指定。下面的循环类似于之前用于添加哈希数据的循环,但它不使用 floatsToBytes()
函数来编码 float32
数组。
for i, emb := range embeddings {
_, err = rdb.JSONSet(ctx,
fmt.Sprintf("jdoc:%v", i),
"$",
map[string]any{
"content": sentences[i],
"genre": tags[i],
"embedding": emb.ToFloat32(),
},
).Result()
if err != nil {
panic(err)
}
}
查询与哈希文档的查询几乎相同。这表明为 JSON 路径选择正确的别名可以避免编写复杂的查询。需要注意的一点是,尽管 JSON 的 embedding
字段的数据被指定为数组,但查询的向量参数仍然指定为二进制字符串(使用 floatsToBytes()
方法)。
jsonQueryEmbedding, err := hf.Embed(ctx, []string{
"That is a happy person",
})
if err != nil {
panic(err)
}
jsonBuffer := floatsToBytes(jsonQueryEmbedding[0].ToFloat32())
jsonResults, err := rdb.FTSearchWithArgs(ctx,
"vector_json_idx",
"*=>[KNN 3 @embedding $vec AS vector_distance]",
&redis.FTSearchOptions{
Return: []redis.FTSearchReturn{
{FieldName: "vector_distance"},
{FieldName: "content"},
},
DialectVersion: 2,
Params: map[string]any{
"vec": jsonBuffer,
},
},
).Result()
除了键的 jdoc:
前缀外,JSON 查询的结果与哈希查询相同
ID: jdoc:0, Distance:0.114169843495, Content:'That is a very happy person'
ID: jdoc:1, Distance:0.610845327377, Content:'That is a happy dog'
ID: jdoc:2, Distance:1.48624765873, Content:'Today is a sunny day'
了解更多
请参阅向量搜索,了解有关向量的索引选项、距离度量和查询格式的更多信息。