主题
元数据、权限过滤与增量更新
本章要回答的问题:同一个知识库,不同人能看到的东西不一样,怎么保证不越权?文档改了、删了,索引怎么跟上?
前八节解决“能不能查到、答得对不对”。这一节解决两个长期运行才会暴露的问题:谁能看到什么,以及资料变了以后索引还是不是真的。
这两件事都不难在演示里糊弄过去,难在长期正确:越权一次就是事故,陈旧答案一次就会让人不再信任系统。
元数据不是可选项
一个块除了文本,至少要带:
| 字段 | 用途 |
|---|---|
doc_id | 出处、去重、增量更新的定位键 |
page / clause | 引用粒度(页码或条款号) |
section | 上下文补全(“这条属于第几章”) |
type | 正文 / 表格 / 图注,决定切分与展示方式 |
authority | 效力层级(法律 / 部门规章 / 内部制度) |
access | 可见范围(公开 / 内部 / 受限) |
authority 影响“哪条规则优先”,access 影响“谁能看”。两者不能混:一条内部制度可能对全员可见,一条公开法规也可能只对合规岗有意义。
本书的语料里,事故报告含个人信息、工艺参数卡属于受限资料,其余按内部处理;这是演示用的简化模型,不是真实 ACL。 真实系统还要处理用户组、继承、临时授权与审计日志。
先过滤还是后过滤
假设一次查询返回 5 条,其中 2 条是受限文档。两种做法:
- 后过滤(post-filter):先检索一个更宽的候选(本书取 top-20),再把受限的删掉、截到 5 条。结果可能不足 5 条,召回受损失;如果受限的占多数,可能一条不剩。
- 前过滤(pre-filter):只让可见的点参与检索。结果仍是 5 条,召回不受影响;代价是每次查询都要在子集上算,或者维护多套索引。向量库文档把这两种做法分开讲,是因为它们的召回行为不同(引用 [1])。
本书在 12 道查询上对比(6 道指向受限文档,6 道指向允许文档):
| 策略 | 每次查询的越权结果 | 允许文档 Recall@5 | 平均延迟 |
|---|---|---|---|
| 不过滤 | 1.17 条 | 0.667 | 0.07 ms |
| 后过滤 | 0 | 0.667 | 0.03 ms |
| 前过滤 | 0 | 0.667 | 0.16 ms |
(实测,见 benchmarks/rag/metadata.csv;794 块,暴力检索,12 道查询。亚毫秒级计时波动较大,这里只比量级。)
这组数字要这样读:
- 不过滤时,top-5 里平均有 1.17 条越权内容——这就是“忘了做权限”的真实后果;
- 后过滤把越权清零,允许文档召回仍是 0.667(6 道题里有 2 道本来就没进前 5,删受限结果救不回来);
- 前过滤的召回与后过滤相同,延迟约 5 倍(0.16 ms 对 0.03 ms),因为每道查询都要在子集上重算。
在 794 块上,0.16 ms 可以忽略;在百万级向量上,前过滤要么维护“按权限分区的索引”,要么接受每次查询的掩码开销。这是 5.5 说的“图索引不擅长过滤”的延伸。
增量更新:别每次重建
文档会改。全量重建的代价是线性的:794 块向量化 46.7 秒(实测);2 万块就是 20 分钟;百万块就是几小时。
增量更新的做法是:
- 用
doc_id+ 块序号定位受影响的块; - 只对这些块重新向量化;
- 索引里“标记旧的 + 插入新的”;
- 更新倒排/BM25 与元数据。
本书改一条保养计划(把 2000 h 改成 1000 h、NAS 9 级 改成 NAS 7 级):
| 操作 | 耗时 |
|---|---|
| 单块向量化 | 约 0.07 秒(5 次中位数 0.069,最大 0.073) |
| HNSW 删旧 + 插新 | 0.5 毫秒 |
| 全量重建(对照) | 46.7 秒 |
(实测,见 benchmarks/rag/metadata.csv)
改完后,查询“MP-003 的液压油更换标准”,原来排在前 5 的旧文本不再出现,新文本进入 top-5(实测:陈旧文本命中 0,旧文本在更新前命中 1)。 删除也一样:把该块从索引里去掉后,top-5 里不再出现它。
删除比更新更容易出错
更新至少有“新文本”可以验证;删除没有。常见的坑:
- 只删向量,不删原文:检索不到,但生成时如果按
doc_id回表,仍可能读到旧内容; - 只删原文,不删向量:向量还在,检索命中后回表为空,模型拿到空上下文;
- 软删除不清理:
mark_deleted只是标记,图仍占内存,删多了召回与延迟都会退化(引用 [2])。
正确顺序是:先让文档在所有检索路径上不可见(向量、BM25、元数据),再异步清理存储;清理完成前,任何回表都要带“已删除”校验。
版本与审计
长期运行的系统,索引一定要能回答三个问题:
- 这个块来自哪份文档的哪个版本?
- 它是哪天、被谁、用什么模型写进索引的?
- 用户看到的答案,当时用的是哪个版本?
最小做法:每个块带 doc_version 与 indexed_at;每次回答把命中的块 id 与版本写进日志。 没有这三样,出了越权或陈旧答案,无法回溯。
工程含义
权限必须在检索前生效。 后过滤能挡住内容,但挡不住“候选被浪费”和“侧信道”(返回条数变化泄露文档存在性)。
增量更新要能定位到块。 块 id 必须稳定,不能每次重切都变;文档版本与切分参数都要参与 id 的生成。
删除要走完整链路。 向量、倒排、原文、缓存四处都不可见,才算删掉。
把权限模型写进测试。 至少准备“受限用户查受限文档”“内部用户查受限文档”“删除后查询”三类用例,每次改索引都跑一遍。
动手实验
动手实验:用两套索引做前过滤
假设:为“内部/受限”各建一套 HNSW,前过滤的延迟能回到不过滤的水平,而召回不变。
做法:按 access 把块分成两个子集,各建一套索引;查询时按用户权限选索引;在 8 道查询上对比“单索引 + 掩码”与“双索引”的延迟、召回与内存占用。
记录:p50/p95、Recall@5、索引总内存、更新一块时两套索引的写放大。注意:索引数量会随权限组合爆炸,先算清楚你最多需要几套。
参考资料
- Qdrant. Filtering(向量检索中的 payload 过滤:前置与后置的差异). https://qdrant.tech/documentation/concepts/filtering/
- hnswlib 项目. README:
mark_deleted与更新语义(删除是标记,不立即收缩图). https://github.com/nmslib/hnswlib - NIST. Guide to Attribute Based Access Control (ABAC) Definition and Considerations(权限模型的分层与审计,NIST SP 800-162). https://doi.org/10.6028/NIST.SP.800-162