索引和查询向量

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

Redis Query Engine 允许您在哈希JSON 对象中索引向量字段(有关更多信息,请参阅向量参考页面)。向量字段除其他功能外,还可以存储文本嵌入,这些是 AI 生成的文本片段中语义信息的向量表示。两个嵌入之间的向量距离表示它们在语义上有多相似。通过比较某个查询文本生成的嵌入与存储在哈希或 JSON 字段中的嵌入的相似性,Redis 可以检索在含义上与查询密切匹配的文档。

在下面的示例中,我们使用Microsoft.ML 生成要使用 Redis Query Engine 存储和索引的向量嵌入。我们还展示了如何调整代码以使用Azure OpenAI 获取嵌入。首先展示了哈希文档的代码,然后单独解释了与 JSON 文档的区别

初始化

从一个新的控制台应用开始,可能最容易理解这个示例,您可以使用以下命令创建它

dotnet new console -n VecQueryExample

在应用的工程文件夹中,添加 NRedisStack

dotnet add package NRedisStack

然后,添加 Microsoft.ML 包。

dotnet add package Microsoft.ML

如果您想尝试下面描述的可选Azure 嵌入,您还应该添加 Azure.AI.OpenAI

dotnet add package Azure.AI.OpenAI --prerelease

导入依赖项

将以下导入添加到您的源文件中

// Redis connection and Query Engine.
using NRedisStack.RedisStackCommands;
using StackExchange.Redis;
using NRedisStack.Search;
using static NRedisStack.Search.Schema;
using NRedisStack.Search.Literals.Enums;

// Text embeddings.
using Microsoft.ML;
using Microsoft.ML.Transforms.Text;

如果您正在使用 Azure 嵌入,也请添加

// Azure embeddings.
using Azure;
using Azure.AI.OpenAI;

定义一个函数来获取嵌入模型

注意
如果您正在使用 Azure OpenAI 嵌入模型,请忽略此步骤。

初始化嵌入模型(在 Microsoft 术语中称为 PredictionEngine)需要几个步骤,因此我们声明一个函数来包含这些步骤。(有关 ApplyWordEmbedding 方法的更多信息,包括示例代码,请参阅 Microsoft.ML 文档。)

请注意,我们使用两个类 TextDataTransformedTextData 来指定 PredictionEngine 模型。C# 语法要求我们将这些类放在控制台应用源文件的主代码之后。下面的声明 TextDataTransformedTextData 部分展示了如何声明它们。

static PredictionEngine<TextData, TransformedTextData> GetPredictionEngine(){
    // Create a new ML context, for ML.NET operations. It can be used for
    // exception tracking and logging, as well as the source of randomness.
    var mlContext = new MLContext();

    // Create an empty list as the dataset
    var emptySamples = new List<TextData>();

    // Convert sample list to an empty IDataView.
    var emptyDataView = mlContext.Data.LoadFromEnumerable(emptySamples);

    // A pipeline for converting text into a 150-dimension embedding vector
    var textPipeline = mlContext.Transforms.Text.NormalizeText("Text")
        .Append(mlContext.Transforms.Text.TokenizeIntoWords("Tokens",
            "Text"))
        .Append(mlContext.Transforms.Text.ApplyWordEmbedding("Features",
            "Tokens", WordEmbeddingEstimator.PretrainedModelKind
            .SentimentSpecificWordEmbedding));

    // Fit to data.
    var textTransformer = textPipeline.Fit(emptyDataView);

    // Create the prediction engine to get the embedding vector from the input text/string.
    var predictionEngine = mlContext.Model.CreatePredictionEngine<TextData,
        TransformedTextData>(textTransformer);

    return predictionEngine;
}

定义一个函数来生成嵌入

注意
如果您正在使用 Azure OpenAI 嵌入模型,请忽略此步骤。

我们的嵌入模型将向量表示为 float 值数组,但当您将向量存储在 Redis 哈希对象中时,必须将向量数组编码为 byte 字符串。为了简化此操作,我们声明了一个 GetEmbedding() 函数,该函数应用上述描述的 PredictionEngine 模型,然后将返回的 float 数组编码为 byte 字符串。如果您将文档存储为 JSON 对象而不是哈希,那么您应该直接使用 float 数组作为嵌入,而无需先将其转换为 byte 字符串(请参阅下面的与 JSON 文档的区别)。

static byte[] GetEmbedding(
    PredictionEngine<TextData, TransformedTextData> model, string sentence
)
{
    // Call the prediction API to convert the text into embedding vector.
    var data = new TextData()
    {
        Text = sentence
    };

    var prediction = model.Predict(data);

    // Convert prediction.Features to a binary blob
    float[] floatArray = Array.ConvertAll(prediction.Features, x => (float)x);
    byte[] byteArray = new byte[floatArray.Length * sizeof(float)];
    Buffer.BlockCopy(floatArray, 0, byteArray, 0, byteArray.Length);

    return byteArray;
}

从 Azure OpenAI 生成嵌入

注意
如果您正在使用 Microsoft.ML 嵌入模型,请忽略此步骤。

Azure OpenAI 是一种便捷的方式来访问嵌入模型,因为您无需自己管理和扩展服务器基础设施。

您可以创建 Azure OpenAI 服务和部署来提供您所需的任何类型的嵌入。选择您的区域,记下服务终结点和密钥,并将它们添加到下面函数中看到占位符的位置。有关更多信息,请参阅了解如何使用 Azure OpenAI 生成嵌入

private static byte[] GetEmbeddingFromAzure(string sentence){
	Uri oaiEndpoint = new ("your-azure-openai-endpoint”);
	string oaiKey = "your-openai-key";

	AzureKeyCredential credentials = new (oaiKey);
	OpenAIClient openAIClient = new (oaiEndpoint, credentials);

	EmbeddingsOptions embeddingOptions = new() {
    	     DeploymentName = "your-deployment-name",
    	     Input = { sentence },
	};

	// Generate the vector embedding
	var returnValue = openAIClient.GetEmbeddings(embeddingOptions);

	// Convert the array of floats to binary blob
	float[] floatArray = Array.ConvertAll(returnValue.Value.Data[0].Embedding.ToArray(), x => (float)x);
	byte[] byteArray = new byte[floatArray.Length * sizeof(float)];
	Buffer.BlockCopy(floatArray, 0, byteArray, 0, byteArray.Length);
	return byteArray;
}

创建索引

连接到 Redis 并删除任何以前创建的名为 vector_idx 的索引。(如果索引不存在,DropIndex() 调用会抛出异常,这就是为什么您需要 try...catch 块。)

var muxer = ConnectionMultiplexer.Connect("localhost:6379");
var db = muxer.GetDatabase();

try { db.FT().DropIndex("vector_idx");} catch {}

接下来,创建索引。下面示例中的模式包含三个字段:要索引的文本内容,一个表示文本“流派”的标签字段,以及从原始文本内容生成的嵌入向量。embedding 字段指定了HNSW 索引、L2 向量距离度量、表示向量分量的 Float32 值以及模型所需的 150 维。

FTCreateParams 对象指定用于存储的哈希对象和一个前缀 doc:,用于标识我们要索引的哈希对象。

var schema = new Schema()
    .AddTextField(new FieldName("content", "content"))
    .AddTagField(new FieldName("genre", "genre"))
    .AddVectorField("embedding", VectorField.VectorAlgo.HNSW,
        new Dictionary<string, object>()
        {
            ["TYPE"] = "FLOAT32",
            ["DIM"] = "150",
            ["DISTANCE_METRIC"] = "L2"
        }
    );

db.FT().Create(
    "vector_idx",
    new FTCreateParams()
        .On(IndexDataType.HASH)
        .Prefix("doc:"),
    schema
);

添加数据

现在您可以提供数据对象,只要您使用索引定义中指定的 doc: 前缀,当您使用HashSet() 添加它们时,它们将自动被索引。

首先,使用我们的 GetPredictionEngine() 函数创建一个 PredictionEngine 模型实例。然后,您可以将其传递给 GetEmbedding() 函数来创建表示 content 字段的嵌入,如下所示。

(如果您使用 Azure OpenAI 模型进行嵌入,则使用 GetEmbeddingFromAzure() 而不是 GetEmbedding(),并注意 PredictionModel 由服务器管理,因此您不需要自己创建实例。)

var predEngine = GetPredictionEngine();

var sentence1 = "That is a very happy person";

HashEntry[] doc1 = {
    new("content", sentence1),
    new("genre", "persons"),
    new("embedding", GetEmbedding(predEngine, sentence1))
};

db.HashSet("doc:1", doc1);

var sentence2 = "That is a happy dog";

HashEntry[] doc2 = {
    new("content", sentence2),
    new("genre", "pets"),
    new("embedding", GetEmbedding(predEngine, sentence2))
};

db.HashSet("doc:2", doc2);

var sentence3 = "Today is a sunny day";

HashEntry[] doc3 = {
    new("content", sentence3),
    new("genre", "weather"),
    new("embedding", GetEmbedding(predEngine, sentence3))
};

db.HashSet("doc:3", doc3);

运行查询

创建索引并添加数据后,您就可以运行查询了。为此,您必须从您选择的查询文本中创建另一个嵌入向量。Redis 在运行查询时会计算查询向量与索引中每个嵌入向量之间的向量距离。我们可以请求对结果进行排序,以便按升序距离排列。

下面的代码使用 GetEmbedding() 方法创建查询嵌入,就像索引一样,并在查询执行时将其作为参数传递(有关使用嵌入进行查询参数的更多信息,请参阅向量搜索)。该查询是K 最近邻 (KNN) 搜索,按距离查询向量的向量距离对结果进行排序。

(如前所述,如果您正在使用 Azure OpenAI,请将 GetEmbedding() 替换为 GetEmbeddingFromAzure()。)

var res = db.FT().Search("vector_idx",
    new Query("*=>[KNN 3 @embedding $query_vec AS score]")
    .AddParam("query_vec", GetEmbedding(predEngine, "That is a happy person"))
    .ReturnFields(
        new FieldName("content", "content"),
        new FieldName("score", "score")
    )
    .SetSortBy("score")
    .Dialect(2));

foreach (var doc in res.Documents) {
    var props = doc.GetProperties();
    var propText = string.Join(
        ", ",
        props.Select(p => $"{p.Key}: '{p.Value}'")
    );

    Console.WriteLine(
        $"ID: {doc.Id}, Properties: [\n  {propText}\n]"
    );
}

声明 TextDataTransformedTextData

注意
如果您正在使用 Azure OpenAI 嵌入模型,请忽略此步骤。

正如我们在上面关于嵌入模型的部分中提到的,我们必须在源文件末尾声明两个非常简单的类。这是必需的,因为生成模型的 API 要求类具有输入 string 和输出 float 数组的命名字段。

class TextData
{
    public string Text { get; set; }
}

class TransformedTextData : TextData
{
    public float[] Features { get; set; }
}

运行代码

假设您已将上述步骤中的代码添加到您的源文件中,它现在已准备好运行,但请注意,当您第一次运行它时可能需要一些时间才能完成(这是因为分词器必须下载嵌入模型数据后才能生成嵌入)。当您运行代码时,它会输出以下结果文本

ID: doc:1, Properties: [
  score: '4.30777168274', content: 'That is a very happy person'
]
ID: doc:2, Properties: [
  score: '25.9752807617', content: 'That is a happy dog'
]
ID: doc:3, Properties: [
  score: '68.8638000488', content: 'Today is a sunny day'
]

结果根据 score 字段的值进行排序,这里表示向量距离。最低距离表示与查询最相似。正如您所料,对于内容文本为“That is a very happy person”的 doc:1 的结果,在含义上与查询文本“That is a happy person”最相似。

与 JSON 文档的区别

索引 JSON 文档与哈希索引类似,但存在一些重要区别。JSON 允许使用嵌套字段进行更丰富的数据建模,因此您必须在模式中提供一个路径来标识要索引的每个字段。但是,您可以为每个这些路径声明一个简短的别名,以避免在每次查询时都完整输入。此外,您必须在创建索引时使用 On() 选项指定 IndexType.JSON

下面的代码展示了这些区别,但索引与之前为哈希创建的索引非常相似

var jsonSchema = new Schema()
    .AddTextField(new FieldName("$.content", "content"))
    .AddTagField(new FieldName("$.genre", "genre"))
    .AddVectorField(
        new FieldName("$.embedding", "embedding"),
        VectorField.VectorAlgo.HNSW,
        new Dictionary<string, object>()
        {
            ["TYPE"] = "FLOAT32",
            ["DIM"] = "150",
            ["DISTANCE_METRIC"] = "L2"
        }
    );


db.FT().Create(
    "vector_json_idx",
    new FTCreateParams()
        .On(IndexDataType.JSON)
        .Prefix("jdoc:"),
    jsonSchema
);

JSON 索引的一个重要区别是使用 float 数组而不是二进制字符串来指定向量。这需要修改定义一个函数来生成嵌入中声明的 GetEmbedding() 函数

static float[] GetFloatEmbedding(
    PredictionEngine<TextData, TransformedTextData> model, string sentence
)
{
    // Call the prediction API to convert the text into embedding vector.
    var data = new TextData()
    {
        Text = sentence
    };

    var prediction = model.Predict(data);

    float[] floatArray = Array.ConvertAll(prediction.Features, x => (float)x);
    return floatArray;
}

如果您将 Azure OpenAI 与 JSON 一起使用,也应该对 GetEmbeddingFromAzure() 函数进行类似的修改。

使用 JSON().set() 添加数据而不是 HashSet()

var jSentence1 = "That is a very happy person";

var jdoc1 = new {
    content = jSentence1,
    genre = "persons",
    embedding = GetFloatEmbedding(predEngine, jSentence1),
};

db.JSON().Set("jdoc:1", "$", jdoc1);

var jSentence2 = "That is a happy dog";

var jdoc2 = new {
    content = jSentence2,
    genre = "pets",
    embedding = GetFloatEmbedding(predEngine, jSentence2),
};

db.JSON().Set("jdoc:2", "$", jdoc2);

var jSentence3 = "Today is a sunny day";

var jdoc3 = new {
    content = jSentence3,
    genre = "weather",
    embedding = GetFloatEmbedding(predEngine, jSentence3),
};

db.JSON().Set("jdoc:3", "$", jdoc3);

查询与哈希文档的查询几乎相同。这表明为 JSON 路径选择正确的别名可以为您省去编写复杂查询的麻烦。唯一重要的区别是为 ReturnFields() 选项创建的 FieldName 对象必须包含字段的 JSON 路径。

需要注意的一个重要事项是,即使 JSON 的 embedding 字段的数据指定为 float 数组,查询的向量参数仍然被指定为二进制字符串(使用 GetEmbedding() 方法)。

var jRes = db.FT().Search("vector_json_idx",
    new Query("*=>[KNN 3 @embedding $query_vec AS score]")
    .AddParam("query_vec", GetEmbedding(predEngine, "That is a happy person"))
    .ReturnFields(
        new FieldName("$.content", "content"),
        new FieldName("$.score", "score")
    )
    .SetSortBy("score")
    .Dialect(2));

foreach (var doc in jRes.Documents) {
    var props = doc.GetProperties();
    var propText = string.Join(
        ", ",
        props.Select(p => $"{p.Key}: '{p.Value}'")
    );

    Console.WriteLine(
        $"ID: {doc.Id}, Properties: [\n  {propText}\n]"
    );
}

除了键的 jdoc: 前缀外,JSON 查询的结果与哈希查询的结果相同

ID: jdoc:1, Properties: [
  score: '4.30777168274', content: 'That is a very happy person'
]
ID: jdoc:2, Properties: [
  score: '25.9752807617', content: 'That is a happy dog'
]
ID: jdoc:3, Properties: [
  score: '68.8638000488', content: 'Today is a sunny day'
]

了解更多

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

评价本页
返回顶部 ↑