索引和查询向量

了解如何使用 Redis 索引和查询向量嵌入

Redis Query Engine 允许您在哈希JSON对象中索引向量字段(更多信息请参阅向量参考页面)。向量字段可以存储文本嵌入等内容,文本嵌入是 AI 生成的向量表示,用于表示文本片段中的语义信息。两个嵌入之间的向量距离表明它们在语义上的相似程度。通过比较从查询文本生成的嵌入与存储在哈希或 JSON 字段中的嵌入的相似性,Redis 可以检索与查询的含义密切相关的文档。

在下面的示例中,我们使用 huggingfaceembedder 包(来自 LinGoose 框架)生成向量嵌入,以便通过 Redis Query Engine 进行存储和索引。代码首先演示了哈希文档的使用,并有一个单独的部分解释与 JSON 文档的区别

初始化

使用以下命令启动一个新的 Go 模块

go mod init vecexample

然后,在您的模块文件夹中,安装go-redishuggingfaceembedder

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()添加它们时就会自动索引。

如下所示使用 huggingfacetransformerEmbed() 方法创建表示 content 字段的嵌入。此方法接受一个字符串数组,并输出相应的 Embedding 对象数组。使用 EmbeddingToFloat32() 方法生成我们需要的浮点值数组,并使用我们上面定义的 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'

了解更多

请参阅向量搜索,了解有关向量的索引选项、距离度量和查询格式的更多信息。

评价此页
返回顶部 ↑