索引和查询向量
了解如何使用 Redis 对向量嵌入进行索引和查询
Redis Query Engine 允许您对 hash 或 JSON 对象中的向量字段进行索引(有关更多信息,请参阅向量参考页面)。向量字段除其他用途外,还可以存储文本嵌入,即由 AI 生成的文本片段语义信息的向量表示。两个嵌入之间的向量距离表明它们在语义上的相似程度。通过比较由某些查询文本生成的嵌入与存储在 hash 或 JSON 字段中的嵌入之间的相似性,Redis 可以检索在含义上与查询密切匹配的文档。
下面的示例使用 sentence-transformers
库生成向量嵌入,以便使用 Redis Query Engine 进行存储和索引。代码首先演示了如何用于 hash 文档,并另设一个部分解释与 JSON 文档的区别。
初始化
如果尚未安装,请安装 redis-py
。此外,使用以下命令安装 sentence-transformers
pip install sentence-transformers
在一个新的 Python 源文件中,首先导入所需的类
from sentence_transformers import SentenceTransformer
from redis.commands.search.query import Query
from redis.commands.search.field import TextField, TagField, VectorField
from redis.commands.search.indexDefinition import IndexDefinition, IndexType
from redis.commands.json.path import Path
import numpy as np
import redis
第一个导入是 SentenceTransformer
类,它从一段文本生成嵌入。在这里,我们创建了一个使用 all-MiniLM-L6-v2
模型生成嵌入的 SentenceTransformer
实例。此模型生成 384 维的向量,与输入文本的长度无关,但请注意输入被截断为 256 个 token(有关 token 与原始文本关系的更多信息,请参阅 Hugging Face 文档中的Word piece tokenization)。
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
创建索引
连接到 Redis 并删除之前使用名称 vector_idx
创建的任何索引。(如果索引不存在,dropindex()
调用会抛出异常,因此您需要 try: except:
块。)
r = redis.Redis(decode_responses=True)
try:
r.ft("vector_idx").dropindex(True)
except redis.exceptions.ResponseError:
pass
接下来,创建索引。以下示例中的 schema 指定使用 hash 对象存储,并包含三个字段:要索引的文本内容、表示文本“体裁”的tag字段,以及从原始文本内容生成的嵌入向量。embedding
字段指定 HNSW 索引、L2 向量距离度量、表示向量分量的 Float32
值以及 384 维,这些都是 all-MiniLM-L6-v2
嵌入模型所要求的。
schema = (
TextField("content"),
TagField("genre"),
VectorField("embedding", "HNSW", {
"TYPE": "FLOAT32",
"DIM": 384,
"DISTANCE_METRIC":"L2"
})
)
r.ft("vector_idx").create_index(
schema,
definition=IndexDefinition(
prefix=["doc:"], index_type=IndexType.HASH
)
)
添加数据
现在您可以提供数据对象,只要使用索引定义中指定的 doc:
前缀,使用 hset()
添加它们时就会自动进行索引。
如下所示,使用 SentenceTransformer
的 model.encode()
方法创建表示 content
字段的嵌入。紧随 model.encode()
调用之后的 astype()
选项指定我们想要一个 float32
值的向量。tobytes()
选项将向量分量一起编码为单个二进制字符串。当您索引 hash 或运行查询时,使用二进制字符串表示(但对于 JSON 文档,请使用 float
列表)。
content = "That is a very happy person"
r.hset("doc:0", mapping={
"content": content,
"genre": "persons",
"embedding": model.encode(content).astype(np.float32).tobytes(),
})
content = "That is a happy dog"
r.hset("doc:1", mapping={
"content": content,
"genre": "pets",
"embedding": model.encode(content).astype(np.float32).tobytes(),
})
content = "Today is a sunny day"
r.hset("doc:2", mapping={
"content": content,
"genre": "weather",
"embedding": model.encode(content).astype(np.float32).tobytes(),
})
运行查询
创建索引并添加数据后,就可以运行查询了。为此,您必须从您选择的查询文本创建另一个嵌入向量。Redis 在运行查询时计算查询向量与索引中每个嵌入向量之间的相似性。然后,它会按照这个数值相似性值对结果进行排序。
下面的代码使用 model.encode()
创建查询嵌入,与索引时相同,并在查询执行时将其作为参数传递(有关如何将查询参数与嵌入一起使用的更多信息,请参阅向量搜索)。
q = Query(
"*=>[KNN 3 @embedding $vec AS vector_distance]"
).return_field("score").dialect(2)
query_text = "That is a happy person"
res = r.ft("vector_idx").search(
q, query_params={
"vec": model.encode(query_text).astype(np.float32).tobytes()
}
)
print(res)
代码现在可以运行了,但请注意,首次运行时可能需要一段时间才能完成(这是因为 RedisVL 必须先下载 all-MiniLM-L6-v2
模型数据,然后才能生成嵌入)。当您运行代码时,它会输出以下结果对象(此处已稍作格式化以提高清晰度)
Result{
3 total,
docs: [
Document {
'id': 'doc:0',
'payload': None,
'vector_distance': '0.114169985056',
'content': 'That is a very happy person'
},
Document {
'id': 'doc:1',
'payload': None,
'vector_distance': '0.610845386982',
'content': 'That is a happy dog'
},
Document {
'id': 'doc:2',
'payload': None,
'vector_distance': '1.48624813557',
'content': 'Today is a sunny day'
}
]
}
请注意,结果是根据 vector_distance
字段的值排序的,距离值最低表示与查询的相似度最高。正如您所料,内容文本为 "That is a very happy person" 的 doc:0
的结果与查询文本 "That is a happy person" 在含义上最相似。
与 JSON 文档的区别
索引 JSON 文档类似于索引 hash,但有一些重要区别。JSON 允许使用嵌套字段进行更丰富的数据建模,因此您必须在 schema 中提供一个路径来标识要索引的每个字段。但是,您可以使用 as_name
关键字参数为每个路径声明一个短别名,以避免在每次查询时都输入完整路径。此外,创建索引时必须指定 IndexType.JSON
。
下面的代码展示了这些区别,但除此之外,索引与之前为 hash 创建的索引非常相似
schema = (
TextField("$.content", as_name="content"),
TagField("$.genre", as_name="genre"),
VectorField(
"$.embedding", "HNSW", {
"TYPE": "FLOAT32",
"DIM": 384,
"DISTANCE_METRIC": "L2"
},
as_name="embedding"
)
)
r.ft("vector_json_idx").create_index(
schema,
definition=IndexDefinition(
prefix=["jdoc:"], index_type=IndexType.JSON
)
)
使用 json().set()
添加数据,而不是 hset()
。指定字段的字典结构与用于 hset()
的字典相同,但 json().set()
以位置参数而非 mapping
关键字参数接收它们。
JSON 索引的一个重要区别是向量使用列表而非二进制字符串指定。请使用 tolist()
方法生成列表,而不是像 hash 那样使用 tobytes()
。
content = "That is a very happy person"
r.json().set("jdoc:0", Path.root_path(), {
"content": content,
"genre": "persons",
"embedding": model.encode(content).astype(np.float32).tolist(),
})
content = "That is a happy dog"
r.json().set("jdoc:1", Path.root_path(), {
"content": content,
"genre": "pets",
"embedding": model.encode(content).astype(np.float32).tolist(),
})
content = "Today is a sunny day"
r.json().set("jdoc:2", Path.root_path(), {
"content": content,
"genre": "weather",
"embedding": model.encode(content).astype(np.float32).tolist(),
})
查询几乎与 hash 文档的查询相同。这表明为 JSON 路径选择正确的别名可以避免编写复杂的查询。需要注意的一点是,即使 JSON 的 embedding
字段的数据被指定为列表,查询的向量参数仍被指定为二进制字符串(使用 tobytes()
方法)。
q = Query(
"*=>[KNN 3 @embedding $vec AS vector_distance]"
).return_field("vector_distance").return_field("content").dialect(2)
query_text = "That is a happy person"
res = r.ft("vector_json_idx").search(
q, query_params={
"vec": model.encode(query_text).astype(np.float32).tobytes()
}
)
除了键的 jdoc:
前缀外,JSON 查询的结果与 hash 的结果相同
Result{
3 total,
docs: [
Document {
'id': 'jdoc:0',
'payload': None,
'vector_distance': '0.114169985056',
'content': 'That is a very happy person'
},
.
.
.
了解更多
有关向量的索引选项、距离度量和查询格式的更多信息,请参阅向量搜索。