☰
基于MongoDB与mongot实现RAG知识库存储与混合检索实战
2026/9/28 23:32:22 网站建设 项目流程

最近半年我一直在做 AI 知识库相关的项目,最头疼的一件事就是:文档原文、向量索引和全文检索到底应该放在哪里。身边不少团队的习惯做法是 MySQL 存业务数据、Elasticsearch 做全文检索、向量数据库管 embedding,三个系统串起来,看着挺专业,维护起来真想摔键盘。后来我把整套检索链路迁到了 MongoDB 上,底层正是 MongoDB 那个名为 mongot 的搜索引擎。这段时间踩了不少坑,也顺着 mongot 开源源码把它的内部机制梳理了一遍,今天把思路和实战过程完整写出来,希望能给正在选型 RAG 存储方案的人一点参考。

这套方案解决的核心问题很直接:让同一个数据库同时承担文档存储、结构化过滤、全文检索和向量召回,而 mongot 就是那个把搜索能力嵌进 MongoDB 体验里的关键进程。如果你正在搭 RAG 知识库,或者被“三件套”架构的数据同步问题折磨过,这篇文章应该能帮你少走很多弯路。

1. RAG 项目里最容易被低估的一环是检索

1.1 为什么传统“三件套”在 AI 场景里特别别扭

RAG,检索增强生成,说白了就是把外部知识塞给大模型,让它回答私域问题时少一点幻觉。完整流程不复杂:文档切分、向量化、入库、检索召回、拼 Prompt、LLM 生成。听着简单,可真落地的时候,你会发现最大的瓶颈不是大模型部分,而是“怎么把内容存下来、怎么把内容找回来”。

最传统的路线是 MySQL + Elasticsearch + 向量数据库三个系统各干各的。这个架构的第一个问题就是数据同步。同一份文档,业务字段要写进 MySQL,正文要同步到 ES,向量要推给向量库。任何一边写入失败,数据就永久不一致。我在项目里遇到过最崩溃的场景:用户修改了一条工单状态,我同时要去 MySQL 改字段、去 ES 更新文档、去向量库重跑 embedding,三套 SDK 来回调,写到最后自己都怕改错地方。

第二个问题是查询逻辑破碎。RAG 场景下的用户问题往往不是单纯的关键词匹配,而是像“找最近三个月售后部门提交的跟退款相关工单”这种结构化过滤加全文加语义的组合条件。这种查询拆分到三个系统里,你要写三套查询语句,再在代码层做结果融合,逻辑复杂不说,性能还很难优化。

第三个问题是运维负担。一个知识库没多大业务量,先搭三个中间件,对中小团队来说纯属过度设计。我当时就在想,有没有可能用一个系统把所有这些事情扛下来。

1.2 MongoDB + mongot 提供了另一种思路

MongoDB 在数据库领域不算新面孔,但很多人不清楚它的搜索层。MongoDB Atlas Search,以及企业版内置的搜索能力,底层是由一个独立进程叫 mongot 承载的。mongot 内嵌了 Lucene 相关的全文检索能力,也支持向量索引和 KNN 搜索。它不是简单包装,而是跟 MongoDB 的查询引擎深度集成:客户端的聚合查询里可以直接写 $search 和 $vectorSearch 阶段,由 mongod 转发给 mongot 去执行。

这意味着什么呢?一份知识库文档,它的原文、结构化元数据、向量表示,可以全部存在同一个集合里,然后通过同一条聚合管道,同时完成布尔过滤、关键词检索和向量召回。业务数据查询和搜索查询在用户视角里融为一体。我在项目里最直观的体会就是,删掉了两套同步代码,原来那种“改一条数据要小心翼翼怕漏同步”的状态彻底消失了。

这个方案适合谁?如果你正在给 RAG 应用做存储选型,或者你已经用了 MongoDB 但不知道它还能做搜索,又或者你想理解大规模检索系统的设计思路,这篇文章值得往下看。

2. 从源码视角看 mongot 引擎的核心设计

2.1 一条查询从 mongod 到 mongot 的完整旅程

mongot 开源以后,我就是冲着一个问题去看代码的:一条搜索查询到底是怎么被 MongoDB 处理的。我觉得与其逐行读完全部源码,不如沿着三条主线去梳理,这样效率高得多。

第一条主线是查询流转路径。当客户端向 mongod 发起一个带 $search 或 $vectorSearch 阶段的聚合查询时,mongod 并没有自己去扫数据,而是先解析出搜索意图,把查询请求转换成内部协议,转发给同机部署的 mongot 进程。mongot 在它管理的 Lucene 索引里执行检索,返回一批文档的 _id 和相关性分数;mongod 拿到这些 id 之后,再从自己的存储引擎里回表取完整文档,继续执行后续的聚合阶段。

这个设计耐人寻味,它把搜索索引和源数据在物理上放在同一个集群,逻辑上又做了清晰分工。索引只负责快速筛选出候选集,真正的数据读取还是 MongoDB 存储引擎的强项。对应用层来说,mongot 完全透明,你永远写 MongoDB 聚合管道,不需要学 Lucene QueryParser 或者 Elasticsearch DSL,这是 mongot 最核心的产品价值。

2.2 全文索引和向量索引为什么能在一个引擎里共存

第二条主线是索引管理。传统全文索引基于倒排表,通过 BM25 这类公式来衡量关键词和文档的相关性;向量索引则更像高维空间里的最近邻搜索问题,常见实现是 HNSW 图。mongot 内部把这两类能力统一到 Lucene 生态里,对外则暴露成不同的索引类型。

对 RAG 应用来说,这带来的直接好处是混合检索变得非常自然。你可以给同一个集合建一个全文索引和一个向量索引,然后在一次聚合管道里先跑 $search 再跑 $vectorSearch,把两边的结果按权重合并。以前在 ES 和向量库之间手动查询再合并的时代,这个操作要写不少胶水代码。现在 mongot 帮你把两边能力收到一起,我实际测试下来,混合召回对回答质量的提升非常明显。纯向量检索容易把语义相近但完全无关的内容捞回来,加入全文检索后,实体名、型号、编号这类精确信息就稳了。

第三条主线更关键,就是相关性分数的处理。mongot 会把搜索内部的评分通过 $meta 语法暴露给聚合管道,这意味着你可以在 MongoDB 查询层直接读取每条命中结果的分数,再做二次归一化、排序或者过滤。这个能力听着简单,却是生产环境必需的,没有它,混合检索的权重调优根本无从谈起。

2.3 从源码设计里读出的三个经验

读这套引擎代码,我最大的感受是它的设计思路非常务实。第一,索引与存储分离、但体验统一,搜索系统的复杂细节被封装在独立进程里,用户无感知地获得了搜索能力。第二,评分在管道里可以流动,让上层应用对检索结果有极强的控制力。第三,所有控制延迟的手段都落在候选集和限制条件上,全文搜索和向量搜索都不是全量扫描,通过控制候选集大小和返回条数来调节资源消耗,这是所有成熟搜索引擎的通用思路。

3. 实战:在 MongoDB 上搭一个可用的 RAG 知识库

3.1 环境准备与文档切分

理论讲再多,不如直接跑起来。我用 MongoDB Atlas 的免费层 M0 集群,不需要花钱就能跑通整条链路。如果你想在企业内部环境试,用 MongoDB Enterprise 部署并开启搜索能力,逻辑也是一样的。

依赖库准备起来不复杂,核心就这几个:pymongo 做数据库操作,sentence-transformers 做向量化,langchain 做流程编排,再配一个大模型接口。向量模型我推荐 BAAI/bge-m3,中文效果好,还支持本地部署,不用把文本送到外部 API,数据安全上更安心。

切分是 RAG 里特别容易被忽视、却对召回质量影响最大的环节。切得太碎,上下文信息不完整;切得太粗,无关内容混进来影响精度。我最终用了 chunk_size=500、chunk_overlap=50 的参数,并把章节标题存成单独的字段。这样一个切块既保留语境,也方便回答时溯源到具体章节。

3.2 写入向量并建立搜索索引

把文档处理成块之后,写入过程很直接,先连接 MongoDB,然后把标题、正文、元数据和 embedding 一起插入集合。

from pymongo import MongoClient from sentence_transformers import SentenceTransformer uri = "你的MongoDB连接串" client = MongoClient(uri) db = client["rag_demo"] col = db["docs"] model = SentenceTransformer("BAAI/bge-m3") emb = model.encode("这里是文档正文片段").astype("float32").tolist() col.insert_one({ "title": "某某手册章节", "content": "这里是文档正文片段", "category": "售后", "embedding": emb, })

这里有个新手特别容易忽略的地方:MongoDB 不会自动“理解”哪个字段是向量,你必须显式建立索引。全文索引和向量索引的映射定义不一样。全文索引只映射需要的字段,我建议关闭动态映射,这样既能控制索引体积,也能避免意外字段被加进来。

{ "mappings": { "dynamic": false, "fields": { "title": { "type": "string" }, "content": { "type": "string", "analyzer": "lucene.standard" }, "category": { "type": "string" } } } }

向量索引的定义更关键,路径、维度、相似度算法一个都不能错。

{ "mappings": { "dynamic": false, "fields": { "embedding": { "type": "vector", "path": "embedding", "numDimensions": 1024, "similarity": "cosine" } } } }

bge-m3 输出 1024 维,配置维度必须跟模型一致,否则索引建起来了,查询结果却永远是空的。这个坑我后面还会细说。

3.3 用 $search 和 $vectorSearch 做混合召回

查询阶段是整个链路的核心。假设用户问“售后服务对退款有什么政策”,第一步要用同一个嵌入模型把这个问题向量化,然后用聚合管道查向量索引。

query_embedding = model.encode(user_question).astype("float32").tolist() pipeline = [ { "$vectorSearch": { "index": "vector_index", "path": "embedding", "queryVector": query_embedding, "numCandidates": 100, "limit": 5 } }, { "$project": { "title": 1, "content": 1, "category": 1, "score": { "$meta": "vectorSearchScore" } } } ] docs = list(col.aggregate(pipeline))

全文检索对应另一种语法,走的也是同一个聚合管道接口:

pipeline = [ { "$search": { "index": "fulltext_index", "text": { "query": user_question, "path": ["title", "content"] } } }, { "$limit": 5 }, { "$project": { "title": 1, "content": 1, "score": { "$meta": "searchScore" } } } ] docs = list(col.aggregate(pipeline))

如果你的需求比较轻,两种检索方式二选一就能跑起来。但生产环境我更推荐混合检索,把向量结果和全文结果分别取回,然后对分数做归一化后再合并。全文分数和向量分数量纲不同,直接相加没有意义。我习惯先各自做 min-max 归一化,然后按权重合并,全文给 0.3,向量给 0.7。这个比例不是拍脑袋定的,是用一批人工标注的相似问题反复验证才调出来的。

max_v = max([d["score"] for d in vec_hits]) or 1 max_t = max([d["score"] for d in text_hits]) or 1 for d in vec_hits: d["final_score"] = 0.7 * d["score"] / max_v for d in text_hits: d["final_score"] = 0.3 * d["score"] / max_t merged = sorted(vec_hits + text_hits, key=lambda x: x["final_score"], reverse=True)[:5]

这段代码看起来普通,但里面的权重调优才是 RAG 质量的关键。不同业务的数据分布差别很大,没有一个通用的黄金比例,必须用实际查询日志和人工标注去校准。

3.4 上下文组装与大模型生成

召回做完后,把命中的片段拼接成上下文,交给大模型生成回答。最终代码大概长这样:

from langchain_openai import ChatOpenAI context = "\n\n---\n\n".join( f"来源:{d['title']}\n{d['content']}" for d in top_docs ) prompt = f"""你是一名售后服务助手。请严格依据以下资料回答用户问题。 如果资料中没有答案,请直接说明不知道,不要编造。 资料: {context} 用户问题:{user_question} """ llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2) answer = llm.invoke(prompt).content print(answer)

这一步看着平平无奇,但 RAG 的最终答案质量,绝大部分取决于这一步的输入,而上下文拼得好不好,完全取决于上一步的检索质量。

4. 实战中踩过的坑与排查方法

4.1 索引已经建好但搜索不到数据

这是重复率最高的坑。索引状态明明显示 Active,一跑查询,结果集却是空的。第一排查思路就是看索引路径和文档字段名是否完全一致。我见过索引里写了 embeddings,文档字段叫 embedding 的,也见过向量存成 numpy 数组没转成 list 的,这类问题建索引时不会报错,查询时颗粒无收。

另一个高频起因是换了嵌入模型。模型一变,向量维度大概率跟着变,旧向量索引不会自动重建,必须删掉重跑一遍向量化、再重新建索引。向量索引本质上是图结构,维度一变整个图就失效了,别想着原地更新,最务实的办法就是全量重建。

4.2 numCandidates 设置不当导致召回率偏低

$vectorSearch 里的 numCandidates 决定进入最终排名的候选数量,limit 才是最终返回条数。很多新手把两个参数设成一样,比如 limit 是 5,numCandidates 也是 5,这样向量索引实际只保留极少的邻居,准确率会差得离谱。我的建议是 numCandidates 至少是 limit 的 10 到 20 倍。想返回 5 条,候选设 100;想返回 10 条,候选设 150 到 200。候选集会带来更多计算,但不会增加最终返回条数,对召回率是实打实的帮助。RAG 场景里,先保证找得到,再谈排得好,所以如果响应时间还有余量,候选集宁可大一点。

4.3 相关性分数看着不对劲

向量搜索的相似度算法可以选 cosine、euclidean 或 dotProduct。我自己常用的是 cosine,尤其在 bge-m3 这类模型上,cosine 的区分度最直观。euclidean 在低维场景还行,在高维向量上距离值会被稀释,不太适合做 RAG 的召回排序。

还有一点,别迷信分数的绝对值。不同模型、不同索引类型产出的分数分布完全不同,最靠谱的做法是用一批已经标注过“是不是正确答案”的测试数据,去标定一个合理阈值,再决定是直接取 top-k 还是按分数过滤。检索不是配一次就完事,它需要跟着业务数据的变化持续调整。

4.4 常见错误速查表

| 现象 | 可能原因 | 处理方法 | | 索引 Active 但搜不到 | 索引路径与字段名不一致 | 核对 path,删除并重建索引 | | 向量查询结果为空 | 维度与模型输出不一致 | 统一维度,重建向量索引 | | 全文检索匹配不到中文词 | 没有配置合适 analyzer | 尝试 lucene.standard 或 smartcn | | 混合检索效果差 | 分数没有归一化就相加 | 先 min-max 归一化再加权 | | 查询延迟明显变高 | numCandidates 设置过大 | 在召回率和延迟之间折中 | | 文档更新后搜不到 | 索引刷新有延迟 | 等待刷新周期,或调整刷新配置 |

这些坑基本都出在配置和习惯问题上,没有一个是搜索引擎原理层面的高深内容。做一次记录,后续项目能省下大量定位问题的时间。

5. 从这套方案里得到的架构思考

5.1 数据存储收敛是一个明显的趋势

过去我们习惯把数据结构和非结构化分成两个世界,数据库管结构化,搜索引擎管全文。RAG 工作负载把这两个世界的边界打穿了:既要存原文,又要算向量,还要支持各种过滤查询。mongot 给了我们一个务实的方案,用主数据库来承载数据资产,把搜索能力当成可插拔的一部分,而不是引入另一套独立体系。

这个思路对选型很有启发。如果团队本身已经在用 MongoDB,我会建议先在现有数据库上把搜索能力用起来,小范围验证 RAG 效果,而不是急着部署一套独立的向量数据库。对绝大多数中小数据集,mongot 支撑的检索性能完全够用。等数据量真正到了千万级以上,再考虑引入专有搜索引擎也不迟。

5.2 对 agentic RAG 和 AI 工作负载的扩展价值

现在 RAG 已经不完全满足于一问一答了,更多场景在往 agentic RAG 演进,也就是让模型自主决定先查什么、再查什么、调用什么工具。这种模式下,检索接口的稳定性、过滤表达能力和元数据丰富度,比单轮问答重要得多。MongoDB 聚合管道天然支持把各种条件组合进一个查询里,mongot 的索引能力让这个组合查询既像数据库查询又像搜索引擎查询,而且可以放心放进循环反复执行。

我见过一个团队做了非常漂亮的例子:在一个聚合管道里组合了布尔筛选、全文检索、向量相似度、地域过滤四层条件,上层套了一个 ReAct Agent,让模型把复杂问题拆成多个检索子任务。整个过程没有引入新中间件,性能也稳定。以后做带自主规划能力的 AI 应用,这种强组合表达能力会越来越吃香。

6. 再聊聊开源源码这件事

虽然这个引擎已经公开,我还是想多说一句:这类代码开源真正的意义,不只是让你本地编译跑一次,而是给技术社区提供了一个理解现代搜索引擎架构的活教材。我建议后来者别急着逐行读,先按我前面说的三条主线,把查询流转、索引管理、分数处理捋明白,再回头看 Atlas Search 遇到的各种报错和调优建议,你会瞬间明白背后的原因。检索是 RAG 的命门,而 MongoDB 加 mongot 这套方案,是我目前见过把检索和业务数据库距离拉得最近的选择。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询