跳到正文

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 才有,之前的版本没有。所以选型时先看部署形态,再看规模和成本。

把一门技术讲透到既懂也会用