索引和查询向量
了解如何使用 Redis 索引和查询向量嵌入
Redis 查询引擎 允许您在 哈希 (hash) 或 JSON 对象中索引向量字段(更多信息请参见 向量 参考页面)。除其他外,向量字段可以存储 文本嵌入,即 AI 生成的文本片段 语义信息 的向量表示。两个嵌入之间的 向量距离 表明它们在 语义上 的相似程度。通过比较由某些查询文本生成的嵌入与存储在哈希或 JSON 字段中的嵌入的相似性,Redis 可以检索在含义上与查询紧密匹配的文档。
下面的示例使用 HuggingFace 模型 all-MiniLM-L6-v2
生成向量嵌入,以便使用 Redis 查询引擎 进行存储和索引。代码首先演示了哈希文档的情况,并有一个单独的部分解释与 JSON 文档的区别。
初始化
您可以使用 TransformersPHP 库创建向量嵌入。使用以下命令安装库
composer require codewithkyrian/transformers
导入依赖项
在源文件中导入以下类和函数
<?php
require 'vendor/autoload.php';
// TransformersPHP
use function Codewithkyrian\Transformers\Pipelines\pipeline;
// Redis client and query engine classes.
use Predis\Client;
use Predis\Command\Argument\Search\CreateArguments;
use Predis\Command\Argument\Search\SearchArguments;
use Predis\Command\Argument\Search\SchemaFields\TextField;
use Predis\Command\Argument\Search\SchemaFields\TagField;
use Predis\Command\Argument\Search\SchemaFields\VectorField;
创建分词器实例
以下代码展示了如何使用 all-MiniLM-L6-v2
分词器生成嵌入。表示嵌入的向量具有 384 维,与输入文本的长度无关。在此,pipeline()
调用创建了从文本生成嵌入的 $extractor
函数。
$extractor = pipeline('embeddings', 'Xenova/all-MiniLM-L6-v2');
创建索引
连接到 Redis 并删除之前创建的名为 vector_idx
的任何索引。(如果索引不存在,ftdropindex()
调用会抛出异常,因此需要 try...catch
块。)
$client = new Predis\Client([
'host' => 'localhost',
'port' => 6379,
]);
try {
$client->ftdropindex("vector_idx");
} catch (Exception $e){}
接下来,创建索引。下面的示例中的 Schema(模式)包含三个字段:要索引的文本内容,表示文本“体裁”的 tag(标签)字段,以及从原始文本内容生成的嵌入向量。embedding
字段指定了 HNSW 索引、L2 向量距离度量、表示向量分量的 Float32
值以及 384 维,这些都是 all-MiniLM-L6-v2
嵌入模型所要求的。
ftcreate()
的 CreateArguments
参数指定了用于存储的哈希对象和标识要索引的哈希对象的前缀 doc:
。
$schema = [
new TextField("content"),
new TagField("genre"),
new VectorField(
"embedding",
"HNSW",
[
"TYPE", "FLOAT32",
"DIM", 384,
"DISTANCE_METRIC", "L2"
]
)
];
$client->ftcreate("vector_idx", $schema,
(new CreateArguments())
->on('HASH')
->prefix(["doc:"])
);
添加数据
您现在可以提供数据对象,只要您使用索引定义中指定的前缀 doc:
,在使用 hmset()
添加它们时就会自动进行索引。
如下所示使用 $extractor()
函数创建表示 content
字段的嵌入。请注意,$extractor()
可以一次从多个字符串参数生成多个嵌入,因此它返回一个嵌入向量数组。在这里,返回的数组中只有一个嵌入。normalize:
和 pooling:
命名参数与嵌入模型的细节有关(更多信息请参见 all-MiniLM-L6-v2
页面)。
要将嵌入添加为哈希对象的字段,必须将向量数组编码为二进制字符串。内置的 pack()
函数是 PHP 中执行此操作的便捷方法,使用 g*
格式说明符表示一个由 float
值打包而成的数组。请注意,如果您使用 JSON 对象而不是哈希来存储文档,则应直接存储 float
数组,无需先将其转换为二进制字符串(请参见下面的 与 JSON 文档的区别)。
$content = "That is a very happy person";
$emb = $extractor($content, normalize: true, pooling: 'mean');
$client->hmset("doc:0",[
"content" => $content,
"genre" => "persons",
"embedding" => pack('g*', ...$emb[0])
]);
$content = "That is a happy dog";
$emb = $extractor($content, normalize: true, pooling: 'mean');
$client->hmset("doc:1",[
"content" => $content,
"genre" => "pets",
"embedding" => pack('g*', ...$emb[0])
]);
$content = "Today is a sunny day";
$emb = $extractor($content, normalize: true, pooling: 'mean');
$client->hmset("doc:2",[
"content" => $content,
"genre" => "weather",
"embedding" => pack('g*', ...$emb[0])
]);
运行查询
创建索引并添加数据后,即可运行查询。为此,您必须从您选择的查询文本创建另一个嵌入向量。Redis 在运行查询时计算查询向量与索引中每个嵌入向量之间的向量距离。您可以请求对结果进行排序,以便按升序距离对它们进行排名。
下面的代码使用 $extractor()
函数创建查询嵌入(就像索引一样),并在查询执行时将其作为参数传递(有关将查询参数与嵌入一起使用的更多信息,请参见向量搜索)。该查询是一个 K 最近邻 (KNN) 搜索,它按与查询向量的向量距离对结果进行排序。
结果以数组形式返回,第一个元素是结果的数量。其余元素是交替的对,首先是返回文档的键(例如 doc:0
),后跟一个数组,其中包含您请求的字段(同样以交替的键值对形式)。
$queryText = "That is a happy person";
$queryEmb = $extractor($queryText, normalize: true, pooling: 'mean');
$result = $client->ftsearch(
"vector_idx",
'*=>[KNN 3 @embedding $vec AS vector_distance]',
new SearchArguments()
->addReturn(1, "vector_distance")
->dialect("2")
->params([
"vec", pack('g*', ...$queryEmb[0])
])
->sortBy("vector_distance")
);
$numResults = $result[0];
echo "Number of results: $numResults" . PHP_EOL;
// >>> Number of results: 3
for ($i = 1; $i < ($numResults * 2 + 1); $i += 2) {
$key = $result[$i];
echo "Key: $key" . PHP_EOL;
$fields = $result[$i + 1];
echo "Field: {$fields[0]}, Value: {$fields[1]}" . PHP_EOL;
}
// >>> Key: doc:0
// >>> Field: vector_distance, Value: 3.76152896881
// >>> Key: doc:1
// >>> Field: vector_distance, Value: 18.6544265747
// >>> Key: doc:2
// >>> Field: vector_distance, Value: 44.6189727783
假设您已将上述步骤中的代码添加到源文件中,现在可以运行了,但请注意,首次运行时可能需要一些时间才能完成(这是因为分词器必须下载 all-MiniLM-L6-v2
模型数据才能生成嵌入)。运行代码时,它会输出以下结果文本
Number of results: 3
Key: doc:0
Field: vector_distance, Value: 3.76152896881
Key: doc:1
Field: vector_distance, Value: 18.6544265747
Key: doc:2
Field: vector_distance, Value: 44.6189727783
请注意,结果按 distance
字段的值排序,距离越低表示与查询的相似度越高。正如您所料,文本“那是一个非常快乐的人”(来自 doc:0
文档)被认为在含义上与查询文本“那是一个快乐的人”最相似。
与 JSON 文档的区别
索引 JSON 文档与索引哈希类似,但存在一些重要的区别。JSON 允许使用嵌套字段进行更丰富的数据建模,因此您必须在 Schema(模式)中提供一个 路径 来标识您要索引的每个字段。但是,您可以为每个路径声明一个短别名,以避免在每次查询时完整输入它。此外,创建索引时必须使用 on()
选项指定 JSON
。
以下代码展示了这些区别,但除此以外,索引与之前为哈希创建的索引非常相似
$jsonSchema = [
new TextField("$.content", "content"),
new TagField("$.genre", "genre"),
new VectorField(
"$.embedding",
"HNSW",
[
"TYPE", "FLOAT32",
"DIM", 384,
"DISTANCE_METRIC", "L2"
],
"embedding",
)
];
$client->ftcreate("vector_json_idx", $jsonSchema,
(new CreateArguments())
->on('JSON')
->prefix(["jdoc:"])
);
使用 jsonset()
添加数据,而不是 hmset()
。指定字段的数组结构与用于 hmset()
的数组结构大致相同,但您应该使用标准库函数 json_encode()
生成数组的 JSON 字符串表示。
JSON 索引的一个重要区别在于,向量使用数组而不是二进制字符串指定。只需将嵌入添加为数组字段,无需使用 pack()
函数,就像您处理哈希时那样。
$content = "That is a very happy person";
$emb = $extractor($content, normalize: true, pooling: 'mean');
$client->jsonset("jdoc:0", "$",
json_encode(
[
"content" => $content,
"genre" => "persons",
"embedding" => $emb[0]
],
JSON_THROW_ON_ERROR
)
);
$content = "That is a happy dog";
$emb = $extractor($content, normalize: true, pooling: 'mean');
$client->jsonset("jdoc:1","$",
json_encode(
[
"content" => $content,
"genre" => "pets",
"embedding" => $emb[0]
],
JSON_THROW_ON_ERROR
)
);
$content = "Today is a sunny day";
$emb = $extractor($content, normalize: true, pooling: 'mean');
$client->jsonset("jdoc:2", "$",
json_encode(
[
"content" => $content,
"genre" => "weather",
"embedding" => $emb[0]
],
JSON_THROW_ON_ERROR
)
);
查询与哈希文档的查询几乎相同。这表明为 JSON 路径选择正确的别名可以帮助您避免编写复杂的查询。需要注意的重要一点是,即使 JSON 中 embedding
字段的数据被指定为数组,查询的向量参数仍指定为二进制字符串(使用 pack()
函数)。
$queryText = "That is a happy person";
$queryEmb = $extractor($queryText, normalize: true, pooling: 'mean');
$result = $client->ftsearch(
"vector_json_idx",
'*=>[KNN 3 @embedding $vec AS vector_distance]',
new SearchArguments()
->addReturn(1, "vector_distance")
->dialect("2")
->params([
"vec", pack('g*', ...$queryEmb[0])
])
->sortBy("vector_distance")
);
除了键的前缀是 jdoc:
之外,JSON 查询的结果与哈希查询的结果相同
Number of results: 3
Key: jdoc:0
Field: vector_distance, Value: 3.76152896881
Key: jdoc:1
Field: vector_distance, Value: 18.6544265747
Key: jdoc:2
Field: vector_distance, Value: 44.6189727783
了解更多
有关向量的索引选项、距离度量和查询格式的更多信息,请参见向量搜索。