news 2026/6/10 8:07:12

别再只用向量数据库了:ES + Milvus + MinIO 三剑合璧的文档检索实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
别再只用向量数据库了:ES + Milvus + MinIO 三剑合璧的文档检索实战

一个被问了无数遍的问题:全文检索、语义搜索、文件存储,能不能各司其职,又能无缝协作?


引子:一个 ISO 9001 文档引发的思考

前几天我把一份 ISO 9001:2015 质量管理体系的 PDF 扔进了本地搭建的知识库系统里。文档是英文的,12 页,讲的是七大质量管理原则。

然后我用中文搜了一句:「质量管理七大原则是什么?」

结果很尴尬——向量相似度只有 0.06。不是召回的问题,是 Embedding 模型根本就不认识中英文之间的语义映射。

这让我重新审视了一个老问题:单一的检索引擎到底能不能扛住真实场景下的复杂查询?

答案是不能。而且解决之道,不在于选一个更强的模型,而在于——架构层面的分工协作


三种检索,三种基因

在聊架构之前,先搞清楚每个组件的底层基因。

Elasticsearch:倒排索引的暴力美学

ES 的本质是一个加强版的 Ctrl+F。它把文档拆成词条(Term),建一个巨大的倒排表:

"quality" → [doc1:pos3, doc2:pos15, doc3:pos7] "management" → [doc1:pos4, doc3:pos12] "principles" → [doc1:pos5]

查询时直接用 BM25 算法算相关性分数,整个过程不涉及任何 AI 模型。它的优势极其鲜明:

  • 精确匹配:搜 "quality management",一定命中包含这两个词的文档

  • 毫秒级响应:倒排索引查起来几乎没有延迟

  • 高亮支持:命中位置一目了然

但它的天花板也很明显——只认字面,不懂语义。你搜「如何降低内部成本」,虽然文档里写着 "bringing internal costs down",ES 是无法把这两者关联起来的。

Milvus:向量空间的语义牢笼

Milvus 走的是另一条路。入库时,文本被 Embedding 模型压成一个高维向量:

"Customer focus is the primary focus of quality management" │ ▼ all-MiniLM-L6-v2 │ [0.023, -0.147, 0.891, ..., 0.034] ← 384 维向量

查询时,用户的查询也被同一个模型编码成向量,然后在向量空间里找距离最近的邻居。这就是语义搜索——不看字面,看意思。

问题在于:向量搜索对精确关键词不敏感。搜 "ISO 9001 certification" 和搜 "certification ISO 9001",在语义空间里几乎一模一样,但它没法告诉你「这两个词在文档里到底出现了几次、离得有多远」。

MinIO:被低估的静默守护者

MinIO 的角色最简单却也最容易被忽略——它就是存文件的。不建索引,不算向量,不做搜索。

但它的价值在于:只有它手里握着原始文件。ES 和 Milvus 存的都是派生数据——索引和向量。用户最终要下载的,永远是 MinIO 里的那份源文件。


三合一:让三个引擎各自做最擅长的事

理解了各自的基因之后,协作方案就呼之欲出了:

上传一条文档 │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ MinIO ES Milvus 存 .md 源文件 全文索引 384维向量 file_id: abc file_id: abc file_id: abc │ │ │ └───────────────┼───────────────┘ │ 同一个 file_id 串联

MinIO 是仓库,ES 是目录,Milvus 是智能检索。三者互不依赖,只靠 file_id 关联。

查询时的调度逻辑:

查询类型ESMilvusMinIO
"quality management" 精确搜主力不参与补链接
"如何降低内部成本" 语义搜补元数据主力补链接
"certification 相关风险评估" 混合搜关键词过滤语义排序补链接

代码实战:从零搭建入库管线

核心逻辑很简单——一条文档进来,分三路写入:

from minio import Minio from elasticsearch import Elasticsearch from pymilvus import Collection from sentence_transformers import SentenceTransformer import hashlib from pathlib import Path ​ # 初始化三个客户端 minio_client = Minio("localhost:9000", access_key="...", secret_key="...", secure=False) es_client = Elasticsearch("http://localhost:9200") milvus_col = Collection("docs") embedder = SentenceTransformer("all-MiniLM-L6-v2") ​ def ingest_document(file_path: str): """一条文档,三路写入""" content = Path(file_path).read_text(encoding="utf-8") doc_id = hashlib.md5(content.encode()).hexdigest()[:16] obj_name = f"{Path(file_path).stem}_{doc_id}.md" ​ # ① MinIO — 存源文件(不加工,原样扔进去) minio_client.fput_object("documents", obj_name, file_path) ​ # ② Elasticsearch — 存全文索引(标题 + 正文 + 元数据) es_client.index(index="docs", body={ "doc_id": doc_id, "title": extract_title(content), "content": content, "minio_path": f"documents/{obj_name}", "created_at": "2026-06-09T12:00:00" }) ​ # ③ Milvus — 分块 → Embedding → 写入向量 chunks = split_into_chunks(content, chunk_size=500) vectors = embedder.encode(chunks, normalize_embeddings=True) milvus_col.insert([ [doc_id] * len(chunks), # file_id list(range(len(chunks))), # chunk_id chunks, # chunk_text vectors.tolist() # embedding ])

注意:以上是简化示例,实际工程中需要处理分块策略、重试机制、事务保证等。核心思路不变:三条写入路径共享同一个doc_id,后续检索靠这个 ID 关联

几个关键设计决策

  1. ID 由内容哈希生成,而非文件名——同一份内容无论改什么名,ID 不变,天然去重

  2. 分块粒度 300~500 字,太短语义不完整,太长向量噪音大

  3. 向量归一化:内积(IP)= 余弦相似度,检索时更快

  4. MinIO 不做任何加工:它就是存文件的,连分块都不参与


进阶:当 ES + Milvus 还不够好

上面这套方案跑起来没问题,但有一个硬伤:两路分数不可比

ES 的 BM25 分数可以是 0 到无穷大,Milvus 的向量相似度是 0 到 1。把这两路结果粗暴合并,就像把马拉松成绩和跳远成绩加在一起排名——没有意义。

解法一:Milvus 原生 Hybrid Search

Milvus 2.4 开始支持在一个 Collection 里同时做稠密向量(语义)和稀疏向量(关键词)搜索:

from pymilvus import AnnSearchRequest, RRFRanker ​ # 稠密检索 dense_req = AnnSearchRequest(dense_vec, "embedding", param={"metric_type": "IP"}, limit=20) ​ # 稀疏检索(BM25 向量) sparse_req = AnnSearchRequest(sparse_vec, "sparse_vec", param={"metric_type": "IP"}, limit=20) ​ # 一次调用,内部 RRF 融合 results = collection.hybrid_search( [dense_req, sparse_req], rerank=RRFRanker(k=60), limit=5 )

这样 ES 就不需要参与检索链路了,退化为元数据存储 + Kibana 可视化。

解法二:RRF 融合排序

RRF(Reciprocal Rank Fusion)不关心原始分数,只关心排名:

RRF(chunk, k=60) = 1/(60 + rank_dense) + 1/(60 + rank_sparse)

举例:

ChunkDense 排名Sparse 排名RRF 得分最终
"Process approach..."321/63 + 1/62 =0.0320🥇
"Customer focus..."151/61 + 1/65 = 0.0318🥈
"Engagement..."281/62 + 1/68 = 0.0308🥉

解法三:Cross-Encoder 精排

Bi-Encoder(入库用的那种)把 query 和文档分别编码,速度快但交互不充分。Cross-Encoder 把(query, document)拼在一起编码,精度高得多:

from sentence_transformers import CrossEncoder reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2") ​ # 候选池 15 条 → 逐对打分 → 取 Top 5 pairs = [(query, chunk) for chunk in candidates] scores = reranker.predict(pairs) final = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)[:5]

三阶段流水线:粗筛(Milvus Hybrid 2000万个→20条)→ 融合(RRF 统一排名)→ 精排(Cross-Encoder 20条→5条)。这是目前工业界验证最充分的检索增强(RAG)架构。


为什么不是 All-in-One?

你可能想问:让 Milvus 既存关键词索引又存向量,甚至附带存文件,不是一个更简单的方案吗?

技术上可行,但违背了一个核心原则:存储与计算分离,检索与索引解耦

方案优点代价
All-in-One部署简单换引擎=迁移全部数据;一个挂了全挂
三件套分立各自独立扩展;换模型只重建 Milvus多一个 file_id 关联逻辑

真实场景中,MinIO 可能要扩到 TB 级,ES 的 Kibana 要给业务团队做看板,Milvus 的模型可能要 3 个月迭代一次——把它们绑在一起,是给自己埋雷。


总结

组件核心能力技术原理适合场景
MinIO存储源文件S3 兼容对象存储存一切原始文件
Elasticsearch关键词检索 + 元数据倒排索引 + BM25精确匹配、全文浏览、Kibana 可视化
Milvus语义检索 + 混合检索向量相似度 + RRF 融合模糊查询、跨语言、语义理解

一条黄金法则:让 MinIO 管存储,ES 管关键词和元数据,Milvus 管语义。三者之间只靠一个 file_id 沟通,不传数据,不分职责。

这套架构不仅适用于文档检索,把它换成图片(MinIO 存原图 + ES 存标签 + Milvus 存 CLIP 向量)、视频(MinIO 存视频 + ES 存字幕 + Milvus 存帧向量)、甚至代码仓库,逻辑完全一致。


本文给出的代码均为独立可运行的简化示例,完整项目(入库管线 + 搜索 API + Docker Compose 部署)可参考各组件官方文档组合实现。

撰文时使用的技术栈:Elasticsearch 8.x + Milvus 2.4 + MinIO + FastAPI + sentence-transformers

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 8:07:01

3个关键步骤解密跨平台数据访问难题:Dislocker实战指南

3个关键步骤解密跨平台数据访问难题:Dislocker实战指南 【免费下载链接】dislocker FUSE driver to read/write Windows BitLocker-ed volumes under Linux / Mac OSX 项目地址: https://gitcode.com/gh_mirrors/di/dislocker 你是否遇到过这样的困境&#x…

作者头像 李华
网站建设 2026/6/10 8:05:52

GTU内部组件的功能分配

1200kVA/600kV GTU将多个功能组件集成于一个充SF6的罐体内,各组件的性能和配合决定了整台设备的试验能力。一、高压变压器组件高压变压器为单铁芯柱结构,铁芯采用冷轧取向硅钢片,叠片系数不低于0.96。低压绕组分为主励磁绕组和补偿绕组。主励…

作者头像 李华
网站建设 2026/6/10 8:05:00

公共Tracker智能配置策略:优化BT下载网络连接的完整方案

公共Tracker智能配置策略:优化BT下载网络连接的完整方案 【免费下载链接】trackerslist Updated list of public BitTorrent trackers 项目地址: https://gitcode.com/GitHub_Trending/tr/trackerslist 在当今数字内容分发时代,BitTorrent下载速度…

作者头像 李华
网站建设 2026/6/10 8:04:57

2026年整理的市场上口碑过硬的GEO生产厂家盘点及挑选攻略

2026年,AI搜索已经渗透到用户消费决策的全链路,据公开数据显示,已有超60%的用户习惯通过豆包、Deepseek、文心一言等AI工具查找产品、服务相关信息,对应的GEO(AI搜索优化)赛道也成为企业获客的新风口。但市…

作者头像 李华
网站建设 2026/6/10 8:02:28

Q02HPLC串口升级为以太网桥接器后实现与WinCC及触摸屏三端零延迟通讯

一、项目背景在食品加工行业,某企业拥有一条自动化水平较高的月饼生产线,该生产线以三菱 Q02H PLC为控制核心,可实现原料输送、和面搅拌、压延成型、烘烤等月饼全流程自动化生产作业。随着企业对生产精细化管理与远程监控要求的持续提升&…

作者头像 李华
网站建设 2026/6/10 7:59:19

模拟:将规则翻译为代码

介绍 “模拟”(Simulation) 并不是一种像“动态规划”或“二分查找”那样有着严格数学公式或固定套路的算法,而是一种编程思想 简单来说,模拟就是:题目怎么说,你就怎么写代码。讲解一下面试中高频出现的lru算法和lfu算法 LRU算法 lru(Least recently used,最近最少…

作者头像 李华