主题
MongoDB 与 AI
一句话:文档模型天然装得下「切片 + 元数据 + 向量」,
$vectorSearch让过滤和召回在一次聚合里完成;但它是 Atlas 能力,不是 Community 的默认能力。
适用版本:MongoDB Atlas(8.0+);自建版见下文「边界」。
为什么需要
RAG 的每条切片本质上是一个文档:正文、来源、租户、权限、时间、向量。用独立向量库时,这些字段要拆到两个系统,写入双写、删除双删,过滤条件还得先查 MongoDB 再回表。
MongoDB 的解法是把整条切片作为一个文档,向量只是其中一个字段。检索时用 $vectorSearch 做 ANN,同时在同一个 stage 里用 filter 做标量过滤——先过滤再召回,不是召回后再筛。
这解决了两类常见事故:
- 权限过滤失效:召回出用户无权访问的切片。
- 过滤后为空:topK 先被无关租户占满,筛完一条不剩。
核心机制
索引定义
Atlas Vector Search 的索引不是 db.collection.createIndex(),而是在 Atlas 侧单独建的搜索索引:
json
{
"mappings": {
"dynamic": true,
"fields": {
"embedding": {
"type": "knnVector",
"dimensions": 1536,
"similarity": "cosine"
},
"tenantId": { "type": "string" },
"docId": { "type": "objectId" }
}
}
}底层是 Lucene 的 HNSW 图。filter 里用到的字段要在 mappings 中声明,否则过滤只能退化成扫描,延迟会明显上升。
查询
js
db.chunks.aggregate([
{
$vectorSearch: {
index: "vector_index",
path: "embedding",
queryVector: queryVec,
numCandidates: 200, // 候选集,通常为 limit 的 10–20 倍
limit: 10,
filter: { tenantId: "t1", docId: { $in: allowedDocIds } }
}
},
{ $project: { content: 1, docId: 1, score: { $meta: "vectorSearchScore" } } }
])numCandidates 是召回率与延迟的旋钮:太小会漏,太大接近暴力扫描。先用 10–20 倍 limit,再用真实问题集测 recall。
混合检索
$search:BM25 全文召回,适合型号、错误码、专有名词。$vectorSearch:语义召回,适合同义表达。$rankFusion(8.1+,Atlas):用 RRF 把两路结果按排名融合,不需要归一化分数。$scoreFusion(8.2 预览 / 8.3 GA):按分数加权融合,适合你能量化两路置信度的场景。
RRF 的好处是免调权重,坏处是丢掉分数差异;分数融合更灵活,但权重需要评测集来定。
索引怎么保持同步
Atlas 通过 Change Streams 把写入同步到搜索索引,通常是秒级延迟。这意味着:
- 写入成功 ≠ 立刻能搜到。
- 事务提交与索引可见是两个时间点,不能拿「刚写入就能检索」做一致性假设。
最小上手
Atlas 集群上先建索引(UI 或 Admin API),再执行:
js
// 插入三条切片
db.chunks.insertMany([
{ tenantId: "t1", docId: ObjectId(), content: "退款政策", embedding: [0.1, 0.2, 0.3] },
{ tenantId: "t1", docId: ObjectId(), content: "物流时效", embedding: [0.9, 0.1, 0.1] },
{ tenantId: "t2", docId: ObjectId(), content: "发票开具", embedding: [0.2, 0.8, 0.2] }
])
// 向量 + 租户过滤
db.chunks.aggregate([
{ $vectorSearch: { index: "vector_index", path: "embedding",
queryVector: [0.1, 0.2, 0.3], numCandidates: 20, limit: 2,
filter: { tenantId: "t1" } } },
{ $project: { content: 1, score: { $meta: "vectorSearchScore" } } }
])预期只返回 t1 的两条,t2 的发票切片不会出现——这就是「过滤在召回内部」和「召回后再过滤」的区别。
生产动作
- 索引构建:数据量大时先建索引再导入通常更慢;按官方建议的批量导入流程做,并观察索引状态。
numCandidates:从10 × limit起步;召回不达标先加候选,再加limit。- 过滤字段:租户、权限、时间这些高频过滤字段必须进 mappings;否则每次查询都在为全库付费。
- 量化:Atlas 支持对向量做量化(binary/scalar)以降低内存,是否需要 rescoring 按官方文档配置。
- 监控:索引同步延迟、查询 P99、
numCandidates与实际扫描量;慢查询先用explain看是否走了 ANN。 - 成本:向量索引常驻内存,Atlas 的规格选择主要由「向量条数 × 维度」决定,不是文档条数。
边界
- Community/自建版长期没有
$vectorSearch。2025-09 起官方提供 8.2+ 的公开预览,需要额外部署mongot进程;8.2 之前只能$text+ 应用层算余弦。选型前先确认部署形态,这是最容易踩的坑。 - Atlas 绑定:索引、量化、融合算子都跟 Atlas 版本走,迁移成本高。
- 不是十亿级向量库:规模上限、维度上限、
numCandidates上限都以官方限制为准;超过千万级要高压缩比和高 QPS,用 Milvus。 - 搜索索引最终一致:不适合「写入后立即按向量校验」的强一致流程。
- 事务不覆盖搜索索引:多文档事务提交后,索引仍需时间追上。
讲清楚
MongoDB 做 AI 检索的立足点是文档模型。RAG 的一条切片本来就是一个文档,正文、来源、租户、权限、向量放在一起,过滤和召回就能在一次聚合里完成,不需要双写,也不会出现「先召回再过滤导致结果为空」。
能力核心是
$vectorSearch,底层是 Lucene 的 HNSW。真正影响效果的是两个参数:numCandidates决定召回率,filter字段有没有进索引决定延迟。混合检索用$rankFusion做 RRF,把 BM25 和向量两路结果按排名融合。但有个前提必须先说清楚:这是 Atlas 的能力。自建 Community 版要到 8.2+ 且额外跑 mongot 才有,之前的版本没有。所以选型时先看部署形态,再看规模和成本。