.NET 向量存储、查询及实例

使用说明

依赖包引入

// 核心抽象包(定义数据模型、特性、接口等)
// NuGet: Microsoft.Extensions.VectorData.Abstractions
using Microsoft.Extensions.VectorData;

// 具体数据库驱动包(根据业务选用,例如 Qdrant)
// NuGet: Microsoft.SemanticKernel.Connectors.Qdrant
using Microsoft.SemanticKernel.Connectors.Qdrant;

1. 定义数据类型 (Model-First)

💡 提示:框架也支持动态生成/流式定义(Fluent API)以及通用泛型模型(无需显式定义类),但推荐使用如下“模型优先”的强类型类定义。

public class Hotel
{
    // 1. 记录唯一主键
    // StorageName 参数(可选)可用于指定该字段在数据库中的实际名称(别名映射)
    [VectorStoreKey]
    public ulong HotelId { get; set; }

    // 2. 常规数据(支持过滤筛选)
    // IsIndexed 默认 false。设为 true 时指示数据库为该属性创建索引,以支持高性能过滤查询(Filtering)。
    // 规范:只要该字段要出现在 LINQ 表达式的 Filter 中,建议显式标上。
    [VectorStoreData(IsIndexed = true, IsFullTextIndexed = false)]
    public string HotelName { get; set; }

    // 3. 长文本数据(支持关键词/全文搜索)
    // IsFullTextIndexed 默认 false。设为 true 时指定数据库创建倒排索引,用于传统关键词检索和混合搜索。
    [VectorStoreData(IsFullTextIndexed = true)]
    public string Description { get; set; }

    // 4.1 向量数据(传统的明确向量类型)
    // Dimensions: 向量维度(必填),需与嵌入模型(如 OpenAI text-embedding-3-small 的 1536 维)一致。
    // DistanceFunction: 计算相似度的距离函数(如 CosineSimilarity, DotProduct, EuclideanDistance)。
    // IndexKind: 向量索引算法类型(如 Hnsw, Flat)。
    [VectorStoreVector(Dimensions: 1536, DistanceFunction = DistanceFunction.CosineSimilarity, IndexKind = IndexKind.Hnsw)]
    public ReadOnlyMemory<float>? DescriptionEmbedding { get; set; }

    // 4.2 向量数据(自动向量化 - 高级特性)
    // 当属性类型为 string 且声明了 [VectorStoreVector] 特性,
    // 并且在初始化向量存储器时注入了 IEmbeddingGenerator 实例时,
    // 框架在 Upsert 时会自动将 string 文本转换为向量并存储。
    // 注意:此机制仅支持写入时自动生成,无法直接用于逆向文本检索。
    [VectorStoreVector(Dimensions: 1536, DistanceFunction = DistanceFunction.CosineSimilarity, IndexKind = IndexKind.Hnsw)]
    public string DescriptionText { get; set; }

    // 5. 常规数据(数组/标签类型,支持筛选)
    [VectorStoreData(IsIndexed = true)]
    public string[] Tags { get; set; }
}

2. 标准向量搜索 (Vector Search)

通过配置 VectorSearchOptions<T>,可以将元数据筛选与向量相似度检索相结合。

var searchOptions = new VectorSearchOptions<Hotel>
{
    Top = 5,       // 返回前 N 条结果
    Skip = 0,      // 跳过前 N 条结果(用于分页)
    IncludeVectors = false, // 是否返回结果中的向量数据,默认 false,设为 false 可显著节省网络带宽

    // 核心:指定目标向量属性
    // 如果数据模型中只包含一个向量属性,此项可省略;若存在多向量场景,必须显式指定。
    VectorProperty = h => h.DescriptionEmbedding, 

    // 核心:预过滤(Pre-filtering)
    // 现代用法:直接使用标准的 C# LINQ 表达式。连接器会在运行时将其翻译为底层向量数据库的 native 过滤语句。
    // 字段必须提前配置为 IsIndexed = true。
    Filter = h => h.HotelName == "Tokyo" && h.Tags.Contains("度假")
};

// 1. 执行向量检索(假设 searchVector 已由外部嵌入模型生成)
var searchResults = await collection.SearchAsync(searchVector, searchOptions);

// 2. 遍历解析搜索结果
await foreach (var result in searchResults)
{
    Console.WriteLine($"酒店名称: {result.Record.HotelName}");
    Console.WriteLine($"匹配得分: {result.Score}"); // 距离函数计算出的相关性得分
}

LINQ 表达式支持的常用过滤计算符

⚠️ 注意:并非所有向量数据库都完美支持全部操作符,底层的 LINQ 翻译器不支持复杂的本地方法调用(如自定义 C# 函数或正则),若超出数据库限制会在运行时抛出异常。

C# 类型 示例过滤条件 (LINQ) 说明
string m => m.Title == "Inception" 字符串精确匹配
int / long m => m.Year >= 2015 数值范围/大小比较
float / double m => m.Rating > 4.5 浮点数比较
bool m => m.IsActive == true 布尔值精确匹配
DateTime m => m.CreatedAt > DateTime.UtcNow.AddDays(-7) 日期与时间跨度比较
Guid m => m.CategoryId == myGuid GUID 唯一标识匹配
string[] / List<string> m => m.Genres.Contains("Sci-Fi") 标签/集合数组中包含特定元素
int[] / List<int> m => m.Tags.Any(t => t > 100) 数值数组的任意项条件过滤

3. 混合搜索 (Hybrid Search)

混合搜索(Hybrid Search)会将关键字/全文检索向量语义检索并行执行,并将两者的结果集取并集通过融合算法(如 RRF)打分返回。

  • 💡 前提条件:数据库和当前的连接器必须实现 IKeywordHybridSearchable<TRecord> 接口。
  • 🔍 模型映射:参与文本检索的字段必须设置 IsFullTextIndexed = true
// 1. 获取显式支持混合搜索的集合接口
IKeywordHybridSearchable<Product> collection = (IKeywordHybridSearchable<Product>)vectorStore.GetCollection<ulong, Product>("skproducts");

// 2. 配置混合搜索独有的 Options
var hybridSearchOptions = new HybridSearchOptions<Product>
{
    Top = 3,
    Skip = 0,
    IncludeVectors = false,

    // 过滤条件:前置过滤掉不符合条件的记录(作用于 IsIndexed = true 的字段)
    Filter = r => r.Category == "External Definitions" && r.Tags.Contains("memory"), 

    // ⚠️【多字段避坑核心】
    // 如果模型中包含多个向量字段或多个全文检索文本字段,必须显式用表达式指定目标,否则框架会抛出异常:
    VectorProperty = r => r.DescriptionEmbedding, // 显式指定要比对哪个向量
    AdditionalProperty = r => r.Description       // 显式指定要在哪个文本字段中检索关键字
};

// 3. 执行混合搜索
// 参数 1:searchVector - 用于语义搜索的向量值
// 参数 2:["happiness", "hotel", "customer"] - 用于倒排索引的全文检索关键词列表
var searchResult = collection.HybridSearchAsync(
    searchVector, 
    ["happiness", "hotel", "customer"], 
    hybridSearchOptions
);

// 4. 读取融合后的检索结果
await foreach (var result in searchResult)
{
    Console.WriteLine($"产品名称: {result.Record.Name}");
    Console.WriteLine($"混合搜索融合得分: {result.Score}");
}

4. 计算向量维度

直接查询向量值,然后查看长度。或是返回模型配置中声明的 Dimensions 参数值进行校验。

    var result = await embeddingClient.GenerateEmbeddingAsync("dimensions_test");

    var vector = result.Value.ToFloats();

    this.LogInfo($"{vector.Length}");

使用例子

对红楼梦进行向量化搜索。
针对《红楼梦》全书体量过大、难以直接交给 AI 提炼的问题,本流程采用「分而治之」的三阶段 AI 流水线,将长文本逐步拆小为可检索的结构化知识库。

章节解析:按「第 x 回」正则切分全书为章节,逐段标注 0-based 行号(本地无 AI)。
场景切分:AI 仅返回「行号区间 + 场景名」的 JSON,不回正文,token 消耗极小,可处理长回目。
元数据提炼:将小场景再交 AI 提取人物、地点、事件、诗词、关键词、情感基调等结构化字段,结果缓存为 RedMansionChunk 的 JSON 文件。
向量入库:用 BAAI/bge-m3(1024 维)生成嵌入,写入 Redis(可切 SQLite),支持语义检索与元数据过滤。
各阶段通过 cache/ 下的 JSON 衔接,已实现断点续跑;跨回目 chunk 衔接等边界逻辑仍有待完善。

创建向量存储器

    await using var serviceProvider = this.CreateServiceProviderCore(it =>
        {
            // 这里使用 redis 存储
            var connectionMultiplexer = ConnectionMultiplexer.Connect("192.168.0.70:6379,syncTimeout=30000");
            var database = connectionMultiplexer.GetDatabase();
            var vectorStore = new RedisVectorStore(database, new RedisVectorStoreOptions { EmbeddingGenerator = this.GetEmbeddingGenerator(), });

            // 这里创建向量存储器,并指定了向量计算接口
            it.AddTransient<RedisVectorStore>(_ =>
                new RedisVectorStore(database, new RedisVectorStoreOptions { EmbeddingGenerator = this.GetEmbeddingGenerator(), }));
        });

保存数据


        # 返回向量存储器
        var vectorStore = serviceProvider.GetRequiredService<RedisVectorStore>();

        // 获取集合,为了测试先删后建,保证数据干净
        using var collection = vectorStore.GetCollection<string, RedMansionInfo>("red_mansion_info");

        await collection.EnsureCollectionDeletedAsync();
        await collection.EnsureCollectionExistsAsync();

        // 载入数据并保存    
        await foreach (var chunk in LoadRedMansionChunksAsync())
        {
            await collection.UpsertAsync(chunk.ToVectorStoreInfo());
        }

搜索数据


        # 返回向量存储器
        var vectorStore = serviceProvider.GetRequiredService<RedisVectorStore>();

        // 获取与写入时同名的集合
        using var collection = vectorStore.GetCollection<string, RedMansionInfo>("red_mansion_info");

        var query = "林黛玉葬花";

        var options = new VectorSearchOptions<RedMansionInfo>
        {
            #  Characters 是场景人物数据,这里使用 Contains 进行过滤 
                Filter = it => it.Characters.Contains("林黛玉") && it.Characters.Contains("贾宝玉"),
                IncludeVectors = false, // 返回结果时,不返回向量值, 以减少占用空间
        };

        // 语义向量检索,取前 10 条
            var results = await collection.SearchAsync(query, 10, options).ToListAsync();

混合搜索数据

测试使用 redis 数据库不支持混合搜索,这里仅写出调用代码


       # 返回向量存储器
        var vectorStore = serviceProvider.GetRequiredService<RedisVectorStore>();

       if(collection is IKeywordHybridSearchable<RedMansionInfo> hybridSearchable )
            {
                # 这里使用全文索引搜索 ["林黛玉", "贾宝玉"]
                var results = hybridSearchable.HybridSearchAsync(query, ["林黛玉", "贾宝玉"], 5);   

                await foreach (var result in results.OrderByDescending(it => it.Score))
                {
                    logger.LogInformation(
                        "相似度: {Score:F4} | {Id} {Title} | 摘要: {Summary} | 事件: {EventTags} | 人物: {Characters} | 地点: {Location}",
                        result.Score,
                        result.Record.Id,
                        result.Record.ChapterTitle,
                        result.Record.Summary,
                        result.Record.EventTags,
                        result.Record.Characters,
                        result.Record.Location);
                }
            }

数据结构

public class RedMansionInfo
{
    // === 身份与定位 ===

    [VectorStoreKey]
    [JsonPropertyName("id")]
    public string Id { get; set; } = "";

    // 回目编号
    [VectorStoreData]
    [JsonPropertyName("chapter_id")]
    public int ChapterId { get; set; }

    // 场景序号
    [VectorStoreData]
    [JsonPropertyName("chunk_index")]
    public int ChunkIndex { get; set; }

    // 回目标题
    [VectorStoreData(IsIndexed = true)]
    [JsonPropertyName("chapter_title")]
    public string ChapterTitle { get; set; } = "";

    // === 元数据文本字段(原 xxxText 属性) ===

    // 人物文本(逗号分隔)
    [VectorStoreData(IsIndexed = true)]
    [JsonPropertyName("characters_text")]
    public string[] Characters { get; set; } = Array.Empty<string>();

    // 场景地点
    [VectorStoreData(IsIndexed = true)]
    [JsonPropertyName("location")]
    public string Location { get; set; } = "";

    // 事件标签文本(逗号分隔)
    [VectorStoreData(IsIndexed = true)]
    [JsonPropertyName("event_tags_text")]
    public string[] EventTags { get; set; } = Array.Empty<string>();

    // 是否含诗词
    // [VectorStoreData(IsIndexed = true)]
    [JsonPropertyName("has_poetry")]
    public bool HasPoetry { get; set; }

    // 诗词内容文本(空格分隔)
    [VectorStoreData(IsIndexed = true)]
    [JsonPropertyName("poetry_content_text")]
    public string[] PoetryContent { get; set; } = Array.Empty<string>();

    // 场景摘要
    [VectorStoreData(/*IsIndexed = true, */IsFullTextIndexed = true)]
    [JsonPropertyName("summary")]
    public string Summary { get; set; } = "";

    // 关键词文本(逗号分隔)
    [VectorStoreData(IsIndexed = true)]
    [JsonPropertyName("keywords_text")]
    public string[] Keywords { get; set; } = Array.Empty<string>();

    // 情感基调
    [VectorStoreData(IsIndexed = true)]
    [JsonPropertyName("tone")]
    public string Tone { get; set; } = "";

    // === 原文件内容 ===

    // 原文内容
    [VectorStoreData(IsFullTextIndexed = true)]
    [JsonPropertyName("content")]
    public string Content { get; set; } = "";

    // 原文的向量值
    [VectorStoreVector(1024)] public string SearchableTextEmbedding => this.SearchableText ?? string.Empty;

    // === 前后链接 ===

    [VectorStoreData]
    [JsonPropertyName("prev_id")]
    public string PrevId { get; set; } = "";

    [VectorStoreData]
    [JsonPropertyName("next_id")]
    public string NextId { get; set; } = "";

    // === 组合可搜索文本(FullText索引) ===

    [VectorStoreData(IsFullTextIndexed = true)]
    [JsonPropertyName("searchable_text")]
    public string SearchableText { get; set; } = "";
}
上一篇