应先清洗分块再向量化:pdf用pdfpig提取文本,recursivecharactertextsplitter切块(512/64),每块单独embed;选qdrant作向量库,建集时设size=1536、distance.cosine,插入带payload,查询时searchrequest需显式启用withpayload=true、scorethreshold=0.3。

用 OpenAIEmbedding 生成文件内容向量,别直接喂原始文本
文件语义搜索的核心不是“搜关键词”,而是把文件内容转成能比较的向量。C# 里最直接的路是用 OpenAIEmbedding(比如 text-embedding-3-small),但很多人一上来就拿整篇 PDF 或 Word 的原始字符串丢进去——结果要么超长报错 400 Bad Request: invalid_request_error,要么向量质量差、检索不准。
真正该做的,是先做内容清洗和分块:
-
PDF文件用PdfPig或IronPdf提取纯文本,跳过页眉页脚、表格结构、页码 - 按语义切分:用
RecursiveCharacterTextSplitter(来自LangChain.Chains)按句号/换行切,chunkSize=512,chunkOverlap=64是较稳的起点 - 每块文本送进
CreateEmbeddingAsync,别拼成大段再 embed —— OpenAI 对单次输入长度有限制,且语义粒度会糊掉
选 Qdrant 而不是 Elasticsearch 做向量库,除非你已有 ES 且启用了 knn
C# 生态里对接向量数据库,Qdrant 是目前最省心的选择。它原生支持点积/余弦距离、filter + search 混合查询、HTTP/gRPC 双协议,C# 客户端 Qdrant.Client 包封装干净。而 Elasticsearch 虽然熟悉,但它的 knn 搜索在 8.x 后才稳定,且需要单独开 vector 字段、设 index_options,稍不注意就查不到结果。
关键配置差异:
- 建集合时,
Qdrant必须指定VectorParams的Size(比如 1536)和Distance(推荐Distance.Cosine) - 插入数据前,确保每条记录带
payload:至少含source_file_path、chunk_index、text_preview,否则搜到结果根本不知道来自哪份文件哪一段 - 查询时用
SearchRequest,别用Scroll或Query——后者不走向量索引
SearchAsync 返回结果没排序?检查 WithPayload 和 WithVector 是否漏设
调 QdrantClient.SearchAsync 后发现结果 Score 全是 0,或者顺序乱、不按相似度降序排——大概率是忘了在请求里显式启用 payload 和 score 解析。
Qdrant.Client 默认不返回 payload,也不保证 Result 列表按 score 排。必须写全:
new SearchRequest
{
Vector = embedding,
Limit = 5,
WithPayload = true, // 不加这句,payload 是 null
WithVector = false, // 一般不用返回向量本身,关掉省带宽
ScoreThreshold = 0.3f // 过滤掉明显不相关的(cosine 距离下 0.7 以上才较可信)
}另外,Score 在 cosine 场景下是 [−1, 1] 区间,值越接近 1 越相似;如果用的是 Distance.Euclidean,那 score 是距离值,越小越好——别看名字想当然。
本地调试时别用 localhost:6333 直连 Qdrant,先确认 Docker 容器网络可通
Windows 上用 Docker Desktop 起 qdrant/qdrant,C# 程序跑在宿主机,连 http://localhost:6333 却超时或返回 Connection refused,不是代码问题,是网络没通。
常见断点位置:
- Docker Desktop 设置里关了
Use the WSL 2 based engine?开了反而容易 DNS 解析失败,建议关掉并重启 Docker - 容器启动命令漏了
-p 6333:6333,或者 Windows 防火墙拦了 6333 端口 - 用
curl http://localhost:6333/health能通,但 C# 报错?检查HttpClient是否设了Timeout太短(默认 100 秒够用),以及是否用了http://127.0.0.1:6333(某些 .NET 版本对 localhost 解析更稳)
向量搜索真正难的不是调通 API,而是让 chunk 有区分度、让 filter 条件能和向量 query 同时生效、还有 embedding 模型更新后旧向量不能混查——这些都得在第一次入库时就想好版本标记和元数据结构。










