在现代信息检索中,传统的关键词搜索已经无法满足复杂语义查询的需求。通过 Semantic Kernel,我们可以将文本数据转化为向量(Embedding),并结合向量数据库实现高效的语义搜索。本文将详细讲解如何使用 C# 构建一个 PDF 向量搜索系统,实现从 PDF 文本提取、向量化存储,到语义搜索的完整流程。
技术栈与依赖
本文示例使用以下 NuGet 包:
| 库 | 功能 |
|---|---|
DocumentFormat.OpenXml |
Office 文档操作 |
Microsoft.Data.Sqlite |
SQLite 数据库操作 |
Microsoft.Extensions.AI |
AI SDK 接口,支持嵌入生成 |
Microsoft.Extensions.AI.Ollama |
Ollama 模型嵌入生成 |
Microsoft.Extensions.VectorData.Abstractions |
向量存储抽象接口 |
Microsoft.SemanticKernel.Connectors.Sqlite |
Semantic Kernel 与 SQLite 的向量存储连接 |
PdfPig |
PDF 文本抽取 |
这些依赖允许我们完成从 PDF 文档读取、文本切块、生成向量、存储和检索的完整流程。
数据模型设计
首先,我们定义 PdfVector 类来描述向量数据库中的记录:
public class PdfVector
{
[VectorStoreRecordKey]
public ulong Key { get; set; }
[VectorStoreRecordData]
public string FileName { get; set; }
[VectorStoreRecordData]
public string Text { get; set; }
[VectorStoreRecordVector(384, DistanceFunction.EuclideanDistance)]
public ReadOnlyMemory<float> Vector { get; set; }
}
-
Key:唯一 ID。 -
FileName:PDF 文件名。 -
Text:文本块。 -
Vector:文本向量表示,维度 384,使用欧氏距离计算相似度。
这些属性通过 Semantic Kernel 的特性标记,自动映射到向量存储。
初始化 SQLite 向量存储
首先创建 SQLite 数据库,并加载向量扩展 vec0.dll:
const string databasePath = "pdf_vectors.db";
if (!File.Exists(databasePath))
{
File.Create(databasePath).Dispose();
}
using var connection = new SqliteConnection($"Data Source={databasePath};");
await connection.OpenAsync();
connection.EnableExtensions(true);
connection.LoadExtension("./extensions/vec0.dll"); // 确保 vec0.dll 在项目目录
创建向量集合:
var vectorStore = new SqliteVectorStore(connection);
var pdfsCollection = vectorStore.GetCollection<ulong, PdfVector>("pdfs");
await pdfsCollection.CreateCollectionIfNotExistsAsync();
-
SqliteVectorStore用于管理向量数据。 -
pdfs集合用于存储 PDF 文本向量。
PDF 文本抽取与切块
为了向量化,我们需要先从 PDF 中提取文本,并将其切成适合的块:
static List<string> ExtractPdfChunks(string filePath, int chunkSize = 500)
{
var textBuilder = new StringBuilder();
using var pdf = UglyToad.PdfPig.PdfDocument.Open(filePath);
foreach (var page in pdf.GetPages())
textBuilder.AppendLine(page.Text);
string fullText = textBuilder.ToString();
var chunks = new List<string>();
for (int i = 0; i < fullText.Length; i += chunkSize)
{
int length = Math.Min(chunkSize, fullText.Length - i);
chunks.Add(fullText.Substring(i, length));
}
return chunks;
}
-
使用 PdfPig 打开 PDF。
-
将每页文本拼接成完整文本。
-
按固定长度(默认 500 字符)切分成块,便于向量化。
使用 Ollama 生成文本向量
我们使用 OllamaEmbeddingGenerator 将文本块转成向量:
IEmbeddingGenerator<string, Embedding<float>> generator =
new OllamaEmbeddingGenerator(new Uri("http://localhost:11434/"), "all-minilm:latest");
-
连接到本地 Ollama 服务。
-
"all-minilm:latest"是嵌入模型。 -
GenerateEmbeddingVectorAsync将文本块生成浮点向量。
存储向量到数据库
遍历 PDF 文件并存储向量:
ulong keyCounter = 0;
foreach (var file in pdfFiles)
{
var chunks = ExtractPdfChunks(file);
foreach (var chunk in chunks)
{
var vector = await generator.GenerateEmbeddingVectorAsync(chunk);
var record = new PdfVector
{
Key = keyCounter++,
FileName = Path.GetFileName(file),
Text = chunk,
Vector = vector
};
await pdfsCollection.UpsertAsync(record);
Console.WriteLine($"Upserted chunk from {record.FileName}");
}
}
-
每个文本块生成向量。
-
创建
PdfVector对象并插入/更新数据库。
向量搜索示例
向量搜索可以直接返回语义相关的 PDF 文本块:
Console.WriteLine("Enter your question:");
var query = Console.ReadLine();
var queryEmbedding = await generator.GenerateEmbeddingVectorAsync(query);
var searchOptions = new VectorSearchOptions
{
Top = 3,
VectorPropertyName = "Vector"
};
var results = await pdfsCollection.VectorizedSearchAsync(queryEmbedding, searchOptions);
await foreach (var result in results.Results)
{
Console.WriteLine($"File: {result.Record.FileName}");
Console.WriteLine($"Text: {result.Record.Text}");
Console.WriteLine($"Score: {result.Score}");
Console.WriteLine(new string('-', 50));
}
-
将用户输入的查询文本转为向量。
-
使用
VectorizedSearchAsync查询最相似的文本块。 -
输出文件名、文本和相似度评分。
总结
通过这篇文章,你学会了如何使用 C# 和 Semantic Kernel:
-
从 PDF 提取文本。
-
对文本进行切块。
-
使用 Ollama 模型生成文本向量。
-
使用 SQLite 向量存储管理向量数据。
-
基于向量实现语义搜索。
这个系统可扩展性强,例如:
-
支持 DOCX、TXT 等多种文件。
-
可以将向量存储迁移到 Postgres、FAISS 或 Milvus。
-
可结合大语言模型回答问题,实现 PDF 问答机器人。