Skip to content

03. 多租户、过滤与版本治理

版本:v1.1

最后更新:2026-07-09

适用对象:正在做企业知识库、内部检索平台、租户隔离型 RAG、权限检索或知识生命周期治理的团队

向量数据库一旦进入企业场景,最先把系统拉回现实的通常不是“检索算法”,而是:

  • 这个租户到底能看到什么
  • 过期文档为什么还在被召回
  • 删除为什么删不干净
  • 版本切换后为什么新旧知识混在一起
  • 个人权限为什么把 filter 复杂度拖爆

这篇专题要解决的就是这类问题。

1. 多租户问题为什么不能后补

很多 Demo 初期会默认:

  • 一套索引
  • 所有数据先混着放
  • 查询时再补过滤

这在企业里经常会变成长期债务,因为它会同时影响:

  • 权限安全
  • 检索性能
  • 生命周期治理
  • 成本边界
  • 事故排查

Pinecone 当前官方文档明确把 multitenancy 单独展开,并建议在强租户隔离场景优先考虑 namespace。这说明租户边界不是展示层问题,而是索引建模问题。

2. 先分清四种隔离层

一个更清楚的做法是把隔离层拆开看:

2.1 物理或资源级隔离

  • 独立集群
  • 独立项目
  • 独立索引

适合:

  • 合规要求高
  • 超大客户
  • 资源模型差异极大

2.2 逻辑空间隔离

  • namespace
  • collection
  • tenant partition

这是多数多租户系统最常用的一层。

2.3 metadata 过滤隔离

  • tenant_id
  • business_unit
  • acl_group
  • status

它灵活,但如果承担了过多职责,复杂度会持续上升。

2.4 应用后置精筛

当权限模型极细时,常会在检索后做二次筛选。

但要注意:

  • 后置精筛不能替代检索层主隔离

否则容易出现:

  • 候选池里先混进不该看的内容
  • 排序和缓存阶段已经被污染

3. namespace、collection、独立索引各自适合什么场景

3.1 namespace

Pinecone 当前文档明确把 namespace 作为多租户隔离的高效路径。

适合:

  • 每次查询大多只查单租户
  • 租户之间边界明确
  • 需要更稳定的成本边界

3.2 collection

更适合:

  • 按业务域或数据类型分层
  • 不同数据集生命周期不同
  • 需要更清晰的数据治理

3.3 独立索引

适合:

  • 大客户独占
  • 合规与性能要求很高
  • 数据规模差异巨大

代价是:

  • 运维更复杂
  • 配置更多
  • 回滚和迁移成本更高

4. metadata 过滤应该承担哪些职责

metadata 不是“备注信息”,而是治理层的一部分。

建议至少让它承担下面这些职责:

4.1 查询范围过滤

  • tenant_id
  • region
  • business_line
  • document_type

4.2 权限过滤

  • acl_group
  • visibility_scope
  • department_id

4.3 生命周期过滤

  • status
  • effective_at
  • expires_at
  • is_deleted

4.4 追溯过滤

  • source_system
  • document_id
  • version_id
  • parent_doc_id

如果 metadata 只能回答“这条是谁家的”,但回答不了“它现在是不是生效版本”,那系统迟早会出现旧知识污染。

4.5 过滤不是越晚越灵活,很多时候越早越稳

Azure AI Search 当前官方资料已经把 vector filter mode 明确拆成:

  • preFilter
  • postFilter
  • strictPostFilter

而且当前文档明确说明 preFilter 是默认模式。

这给工程上的启发很直接:

  • 如果租户、权限、生效状态这些边界在查询前就已知,越早过滤通常越稳
  • 如果先放进 ANN 候选池,再事后裁掉,容易出现 top-k 不够、候选池被污染、相关结果被错误挤掉
  • 越权边界、合规边界和生命周期边界,原则上不该只寄托在最后一步后处理

更实用的做法通常是:

  1. 检索前先确定硬边界。
  2. 召回阶段只在合法范围内找候选。
  3. 重排和答案组装阶段再做更细语义判断。

4.6 filter 字段最好分成“边界字段”和“运营字段”

不是所有 metadata 字段都应该承担同样职责。

更稳的做法通常会把它们至少拆成两组:

  • 边界字段:tenant、namespace、acl_group、status、effective_at、expires_at
  • 运营字段:source_system、owner、doc_type、product_line、language、tag

前者决定“能不能进候选池”,后者决定“怎么缩小范围和怎么排查”。

如果把这两类字段混着用,常见后果是:

  • 权限条件和运营筛选条件写成一团
  • 后面没人说得清哪些字段改了会影响越权风险
  • filter 表达式越来越长,但系统仍然不稳

5. 为什么不要把个人级权限直接塞成超长过滤条件

Pinecone 当前文档明确提醒要避免用高基数 ID 列表做大范围过滤。

这在工程上的翻译很直接:

  • 不要把几千个 allowed_user_ids 直接塞进一次检索条件里,当作默认权限方案

更稳的办法通常是:

  1. 先做租户级或业务域级硬隔离。
  2. 再做 group / role 级过滤。
  3. 实在需要个人级控制时,再做后置精筛或预聚合授权。

因为个人级权限一旦全压到检索层,会同时带来:

  • filter 复杂度飙升
  • 查询延迟波动
  • 配置难维护
  • 越权排查变难

5.1 更像生产系统的权限做法,通常会把“人”折叠成组

很多团队一开始就把用户 ID 直接塞进 metadata,是因为业务权限模型还没有被整理成:

  • role
  • group
  • dataset grant
  • exception list

但对检索系统来说,更稳定的路径通常是:

  • 检索层只认 tenant / group / dataset scope
  • 个人级差异放到授权映射层维护
  • 极少数例外再做后置精筛或显式 deny / allow override

这样做的价值是:

  • filter 表达式更短
  • 缓存键更稳定
  • 权限变更更容易审计
  • 大规模用户迁移不会直接拖垮检索层

6. 版本治理为什么是向量检索的一等问题

很多团队以为版本治理是文档平台的事。

但对检索系统来说,如果没有版本治理,会直接出现:

  • 新旧制度同时被召回
  • 删除了源文档,旧 chunk 仍可命中
  • 灰度发布时用户拿到混合版本
  • 回答里出现新旧引用冲突

所以一个更完整的版本体系通常至少要包含:

  • content_version
  • chunk_version
  • embedding_version
  • index_version
  • serving_version

这部分可以和 AI知识版本化专题 一起看。

7. 父文档、chunk、版本三层关系怎么建

建议至少能稳定表达下面这条谱系:

text
tenant_id
 -> document_id
   -> content_version
     -> chunk_id
       -> embedding_version
         -> index_version

这样做的价值是:

  • 能按父文档整批删除
  • 能快速回滚到旧版本
  • 能解释“当前答案引用的是哪个版本的哪个 chunk”

8. 更新策略要先定什么

8.1 整文重建还是增量重建

如果文档结构变动大,整文重建通常更稳。

如果只是局部变化、切片边界稳定,增量重建更省。

8.2 旧版本保留多久

要先回答:

  • 为了回溯是否保留历史版本
  • 保留多久
  • 是否在检索层默认隐藏

8.3 新鲜度怎么验

Pinecone 当前文档把 data freshness 单独讲,就是在提醒你:

  • 向量库不是“写进去就结束”,还要验证什么时候才算真的可检索

8.4 serving_version 最好和写入版本分开

很多系统的问题不是“新索引没写进去”,而是“新索引已经写了,但 serving 还没切过去”。

更稳的做法通常会把:

  • ingest_version
  • index_version
  • serving_version

分开记录。

这样你才能真正支持:

  • shadow index
  • 灰度切流
  • 蓝绿回滚
  • 回答回放时还原当时线上实际使用的版本

否则系统很容易陷入一种危险状态:

  • 数据层以为已经更新
  • 检索层还在读旧索引
  • 应用层又以为自己正在用新知识

8.5 版本切换最好带“退出条件”,而不是只带“上线动作”

真正稳的版本治理,不只是定义怎么切新版本,还要定义:

  • 什么信号出现时停止放量
  • 什么条件下回滚到上一个 serving_version
  • 哪些 query bucket 要单独观察
  • 哪些租户或数据集允许先灰度

这也是为什么版本治理和评测、回放、告警通常要绑在一起看,而不是只看 upsert 成功率

9. 删除为什么经常是最难的一步

真实系统里最让人头疼的删除,不是“删一条记录”,而是:

  • 按父文档删全量 chunk
  • 按版本删旧索引
  • 按租户下线删整批数据
  • 按状态撤回不再生效的知识

如果 ID 和 metadata 设计没提前考虑,删除阶段通常会暴露问题:

  • 找不全
  • 删不干净
  • 不知道哪些还残留

9.1 删除验收不要只看“接口返回成功”

生产里更像样的删除验收,通常至少要补三层确认:

  1. record / chunk 级别确认目标记录真的被移除或失效。
  2. query 级别确认典型查询已不再召回旧内容。
  3. 引用 / 答案 级别确认旧版本不会继续出现在最终响应里。

否则很容易出现:

  • delete 成功了,但旧缓存还在
  • 主索引删了,但 rerank 数据源没删
  • 检索层删了,但答案拼装仍在读旧引用

9.2 “逻辑删除”与“物理删除”最好分阶段处理

很多企业环境里,最稳的删除路径并不是一步物理清空,而是:

  1. 先把 status=is_deletedeffective=false 写入边界字段,先退出召回。
  2. 再在后台做批量物理删除、快照保留、索引清理和引用回收。

这样做的好处是:

  • 风险动作可以先止血
  • 法务 / 审计留痕更清楚
  • 即使后台删除稍慢,也不会继续被线上召回

10. Weaviate 的多租户启发

Weaviate 当前官方文档不仅支持多租户,还把 tenant states 单独拿出来管理。

这很有启发,因为企业环境里的租户并不是永远处于同一种状态:

  • 活跃
  • 冷租户
  • 停用
  • 下线

这意味着更成熟的治理,不应该只想“怎么查”,还要想:

  • 哪些租户该保热
  • 哪些租户可冷却
  • 哪些租户应停止写入或逐步清理

10.1 tenant state 真正解决的是“冷热租户成本”

Weaviate 当前 tenant states 文档把状态拆得已经很具体:

  • ACTIVE
  • INACTIVE
  • OFFLOADED

这背后的价值不是多几个枚举值,而是把租户生命周期真正纳入资源模型:

  • 活跃租户保热,保障读写
  • 不活跃租户可保留本地但不对外服务
  • 长尾租户可转冷存储,等需要时再恢复

这对企业知识平台特别重要,因为真实租户分布常常不是均匀的:

  • 少数大租户很热
  • 大量尾部租户极少访问
  • 某些租户会阶段性冻结或下线

如果没有租户状态治理,最后经常会变成:

  • 明明几乎没人访问的数据,仍长期占着热资源
  • 真正高频租户却要和冷租户抢同一层容量

11. Qdrant / payload 过滤给我们的启发

Qdrant 当前官方资料把过滤和 payload 单独组织,这提醒我们:

  • payload / metadata 不是附属品,而是检索规划的一部分

工程上最值得记住的是:

  • 如果你的系统强依赖过滤、权限、状态和生命周期,那么选型时要把 payload / metadata 表达能力当成硬指标

11.1 Qdrant 的一个现实提醒:不一定需要每个租户一个 collection

Qdrant 当前 multitenancy 文档明确把一个常见建议说得很直接:

  • 多数情况下,按 embedding model 使用单 collection,再配合 payload-based partitioning 做租户隔离

这件事很值得记住,因为很多团队一碰到多租户就会冲向:

  • 每个租户一个 collection
  • 每个租户一份配置
  • 每个租户单独备份和迁移

但如果租户很多、其中大量还是长尾小租户,这样做很容易把治理面炸开。

更稳的做法通常是先问:

  • 你的租户是大客户少量重隔离,还是海量长尾轻隔离
  • 查询基本只在单租户内,还是经常跨租户做分析
  • 哪些租户真的值得独立资源边界,哪些更适合 payload 分区

11.2 snapshot / restore 不是运维附录,而是版本治理工具

Qdrant 当前官方教程把 snapshot 的创建、下载和恢复讲得很完整,这说明:

  • 快照不仅是备份
  • 它也是迁移、回滚、恢复和变更验证的一部分

对检索系统来说,snapshot 真正能解决的是:

  • 某次批量导入错误后怎么快速回退
  • 某个 collection 迁移后怎么验内容一致
  • 某租户或某版本出现事故时怎么恢复到前一状态

12. 一个更稳的企业权限检索分层

更推荐的分层通常是:

  1. tenant:namespace 或强逻辑分区
  2. domain / dataset:collection 或 metadata
  3. role / group:metadata filter
  4. individual exceptions:后置精筛或专门授权表

好处是:

  • 检索层不会背过多细粒度复杂度
  • 生命周期治理更清楚
  • 越权排查路径更短

12.1 更像生产系统的过滤链,通常至少有四层

很多团队把“过滤”只理解成一条向量库查询表达式,但真实系统里更常见的是四层链路:

  1. request scope:请求入口先确定 tenant、user、dataset、time window。
  2. retrieval filter:检索层只在合法范围内召回候选。
  3. rerank / assembly filter:重排和引用组装继续剔除不合规候选。
  4. response guard:最终答案前再检查输出中是否混入不该暴露的引用。

这四层不是重复劳动,而是分别解决:

  • 不让脏候选进池
  • 不让错误候选升序
  • 不让脏引用进答案
  • 不让最终响应越权出站

13. 哪些日志字段最值得先保留

如果你后面要排查串数、旧知识、误召回,至少要保留:

  • request_id
  • tenant_id / namespace
  • filter 表达式
  • parent_doc_id
  • chunk_id
  • version_id
  • source_system
  • status / effective window
  • top-k 候选池

没有这套字段,很多事故最后只能靠猜。

13.1 事件日志也要覆盖“租户状态变化”和“版本切换”

除了请求级日志,还建议显式记录这些治理事件:

  • tenant state change
  • namespace / collection migration
  • index rebuild start / finish
  • serving_version switch
  • bulk delete job start / finish
  • rollback trigger

因为很多事故并不是单次查询本身出错,而是:

  • 某次状态切换之后整批查询开始异常
  • 某次版本切换之后旧知识重新冒出来
  • 某次批量删除作业没有跑完,但没人注意到

14. 典型故障到根因映射

14.1 出现跨租户内容

优先查:

  • namespace 是否正确
  • 缓存是否带租户键
  • filter 是否在每条链路都生效

14.2 老制度压过新制度

优先查:

  • version 字段是否参与过滤或排序
  • 旧版本是否真的删除
  • 生效窗口字段是否有效

14.3 已下线文档仍可检索

优先查:

  • 删除是否只删源文档,没删 chunk
  • 索引更新是否存在延迟
  • serving 层是否还指向旧版本

14.4 只有某些用户查不到该有的内容

优先查:

  • acl_group 是否遗漏
  • group 映射是否更新
  • 过滤表达式是否过严

15. 上线前最该做的检查

  1. 是否已经明确租户隔离层在 namespace、collection、metadata 还是独立索引。
  2. 是否已经避免把超大个人 ID 列表直接放进检索过滤。
  3. 是否可以按父文档、版本、租户做批量删除。
  4. 是否能追溯一次回答引用的是哪个版本的哪个 chunk。
  5. 是否能验证下线、回滚、灰度时不会混入旧版本。
  6. 是否对缓存、日志、回放链路都加上租户和版本键。
  7. 是否已经明确哪些过滤必须前置,哪些可以后置。
  8. 是否能把 tenant state、snapshot、rollback 纳入运行手册。

16. 常见反模式

  • 所有租户混在一个大索引,后面再想补权限。
  • 只做 tenant_id 过滤,不做版本和状态过滤。
  • 只有 document_id,没有 chunk_id 和 version_id。
  • 删除流程没有验收,默认认为“调用 delete 就成功了”。
  • 回答里只有引用文本,没有来源版本和父文档身份。
  • 过滤逻辑散落在检索、重排、缓存和答案层,但没有统一边界定义。
  • 没有 tenant state / 冷热分层,所有租户永久占用同一层热资源。
  • serving_version 和 index_version 混成一个值,导致回滚时根本不知道线上读的是谁。

17. 推荐搭配阅读

18. 重点官方资料

以下资源已按 2026-07-09 复核可访问: