C# 向量数据库实战:从零构建语义搜索系统

1次阅读
没有评论

共计 2459 个字符,预计需要花费 7 分钟才能阅读完成。

image.webp

传统搜索的瓶颈与语义搜索的价值

当我们使用 WHERE content LIKE '% 关键词 %' 进行搜索时,会遇到三个致命问题:

C# 向量数据库实战:从零构建语义搜索系统

  • 无法理解同义词(搜索 ” 汽车 ” 找不到 ” 轿车 ”)
  • 受限于措辞变化(” 如何更换轮胎 ” ≠ “ 轮胎更换教程 ”)
  • 缺乏语义关联(” 宠物医院 ” 和 ” 动物诊所 ” 明明相关却无法匹配)

语义搜索通过将文本转换为高维向量(比如 1536 维的 OpenAI embeddings),在向量空间中进行相似度计算。这种方案在电商搜索、知识库问答等场景能提升 30% 以上的准确率。

.NET 生态下的向量数据库选型

1. Milvus(推荐用于自建集群)

  • 优势:开源、支持分布式部署
  • 集成成本:需通过 gRPC 调用(官方提供.NET SDK)
  • 适合场景:千万级数据量,需要高吞吐

2. Pinecone(推荐用于 Serverless)

  • 优势:全托管服务,自动扩缩容
  • 集成成本:HTTP API 直接调用(NuGet 包Pinecone.Net)
  • 适合场景:快速验证原型,避免运维负担

3. Qdrant(平衡之选)

  • 优势:RESTful API 设计简单
  • 集成成本:需自行处理 JSON 序列化
  • 适合场景:中小规模生产环境

核心实现步骤

步骤 1:文本向量化处理

安装必要的 NuGet 包:

dotnet add package Microsoft.ML.OnnxRuntime

加载预训练模型(示例使用 MiniLM-L6-v2):

// 模型下载:https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2
var session = new InferenceSession("all-MiniLM-L6-v2.onnx");

float[] GetEmbedding(string text) {var inputTensor = new DenseTensor<string>(new[] {text}, new[] { 1});
    var inputs = new List<NamedOnnxValue> {NamedOnnxValue.CreateFromTensor<string>(InputNames.First(), inputTensor)
    };

    // 时间复杂度:O(n) 其中 n 为文本长度
    using var results = session.Run(inputs);
    return results.First().AsTensor<float>().ToArray();}

步骤 2:EF Core 与向量数据库集成

以 Qdrant 为例的存储模型定义:

public class DocumentVector {public Guid Id { get; set;}
    [Column(TypeName = "jsonb")] // PostgreSQL 的 JSONB 类型
    public float[] Vector { get; set;}
    public string OriginalText {get; set;}
}

// 分页查询优化(避免内存溢出)public async Task<List<DocumentVector>> SearchSimilar(float[] queryVector, int topK=10) {
    return await dbContext.DocumentVectors
        .OrderBy(d => EF.Functions.L2Distance(d.Vector, queryVector)) // 欧式距离
        .Take(topK)
        .AsNoTracking()
        .ToListAsync();}

步骤 3:相似度算法选择

  • 余弦相似度:适合文本匹配(忽略向量长度)

    // 计算两个向量的余弦相似度
    float CosineSimilarity(float[] a, float[] b) {
        float dot = 0, magA = 0, magB = 0;
        for (int i = 0; i < a.Length; i++) {dot += a[i] * b[i];
            magA += a[i] * a[i];
            magB += b[i] * b[i];
        }
        return dot / (MathF.Sqrt(magA) * MathF.Sqrt(magB));
    }

  • 欧式距离:适合推荐系统(考虑绝对距离)

    // 计算欧式距离(L2 范数)float EuclideanDistance(float[] a, float[] b) {
        float sum = 0;
        for (int i = 0; i < a.Length; i++) {sum += MathF.Pow(a[i] - b[i], 2);
        }
        return MathF.Sqrt(sum);
    }

生产环境注意事项

向量维度陷阱

  • 384 维 vs 1536 维:维度越高精度越好,但查询延迟线性增长
  • 建议:通过 BenchmarkDotNet 测试不同维度下的 QPS

批量插入优化

// 错误做法:单条插入
foreach(var text in texts) {dbContext.Add(new DocumentVector { ...});
    await dbContext.SaveChangesAsync(); // 每次都是网络往返!}

// 正确做法:批量提交
var batch = new List<DocumentVector>();
for (int i = 0; i < texts.Length; i++) {batch.Add(new DocumentVector { ...});
    if (batch.Count % 1000 == 0) {await dbContext.BulkInsertAsync(batch); // 使用 EFCore.BulkExtensions
        batch.Clear();}
}

索引维护策略

  • 重建时机:当新增数据超过原有数据量的 20%
  • 最佳实践:每月凌晨低峰期执行VACUUM FULL(PostgreSQL)

延伸思考:多语言混合搜索

当用户同时搜索中文 ” 苹果 ” 和英文 ”apple” 时:
1. 应该使用多语言模型(如 paraphrase-multilingual-MiniLM-L12-v2)
2. 还是为每种语言维护单独的向量空间?
3. 如何统一处理 CJK(中日韩)语言的 tokenization 问题?

欢迎在评论区分享你的解决方案!

正文完
 0
评论(没有评论)