索引和查询向量

了解如何使用 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

了解更多

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

评价此页面
返回顶部 ↑