第 3 / 21 篇

第2章 为文档建立身份、版本与来源

知识库常从一个共享目录起步。文件名被当成文档身份,修改时间被当成版本,上传者填写一组权限。等到同名文件覆盖、同步乱序和部门调整同时出现,这些隐含规则就会互相冲突。索引之前,应把来源契约写清楚。

身份必须包含租户

本书用 (tenant, doc_id) 组成文档唯一键。数据集中,acme 和 beta 都有 travel,前者住宿上限五百元,后者九百元。两篇文档的编号相同不代表它们可以合并;同一个 alice 字符串出现在不同租户,也不代表拥有跨租户权限。

SQLite 的 docs 表对这两个字段建立联合唯一约束。查询时租户来自可信上下文,并进入每条权限筛选 SQL。这个约束要落实在存储与查询里,不能只靠拼接不同的文件路径。测试刻意保留同名文档,正是为了使遗漏租户条件的实现立即暴露。

文档使用递增正整数 revision。相同版本且规范化摘要相同,是可重复处理的同一事件;相同版本但摘要不同,是冲突;较小版本是过期事件。系统拒绝后两种情况,而不是猜测最后到达的消息一定最新。删除也占用一个新的内容版本,以便挡住延迟到达的旧内容。

作者构造数据也要有清单

lab/data/corpus.json 保存八篇文档,包含内容、格式、租户、版本和初始 ACL。queries.json 保存十三个查询及相关文档标签。manifest.json 保存两个文件的 SHA-256,加载器在读取时复核它们。这样,一份报告对应哪版输入就可以被检查。

这些指纹没有证明制度内容来自真实企业。文档和查询都由作者编写,只是可审查的教学样例。SECRET-ACME-BOB 与 SECRET-BETA 是用来观察权限边界的合成标记,不是实际机密。数据作者、标签作者与实现作者相同,也意味着这套评估没有独立盲测的性质。

真实接入时,应增加源系统地址、原文件指纹、连接器版本、采集时间与授权依据。不要把原始文件摘要和本书的投影摘要混淆:两个不同的 JSON 排版可能投影成同一正文,而一个原始文件经过不同解析器也可能产生不同投影。

内容事件和权限事件分开

第一次接入文档时,初始 ACL 是明确列表;空列表拒绝所有人,星号只表示该租户内的用户。后续内容更新保留已有 ACL,避免旧同步任务把已经撤销的用户重新加回去。权限更新走独立的 set_acl(),通过 expected_acl_revision 比较交换,拒绝过期策略写入。

这是一项有意的取舍:上传新正文时附带的 ACL 不会自动覆盖当前授权。如果源系统把正文和策略作为一个原子事件发送,连接器应建立更完整的事件协议,而不是依赖调用顺序碰运气。本书选择分离两个状态机,让撤权含义保持清晰。

接入下一批文档前,先回答身份是否稳定、版本能否比较、权限由谁授权、删除是否有墓碑。只有这些答案清楚,才值得讨论哪一种模型更能理解内容。