使用说明
依赖包引入
// 核心抽象包(定义数据模型、特性、接口等)
// 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; } = "";
}