写入、更新、删除与版本

本节目标

你将设计一条可重放、可对账的数据生命周期:稳定 ID 的幂等 upsert,来源/权限/向量一致更新,部分失败恢复,删除 tombstone 和物理清理,以及模型/索引双版本发布。

写入来自 canonical revision

一个 point 不应由“某次脚本临时算出的向量”定义,而应绑定:

tenant + point_id
source_id + source_revision
chunk_id/span + actual input hash
embedding space + revision
payload/ACL revision
ingestion job + attempt

上游 [[Knowledge/AI Agent Engineer/docs-CN/知识库构建/00-目录|知识库构建]]可用 canonical revision + outbox 产生事件;embedding worker 只处理明确 revision,向量库写入后再回报状态。这样可以从事实源重建,不依赖一次不可重放调用。

稳定 ID 与幂等 upsert

相同逻辑 point 的重试应使用相同 ID。若 source_revision 是上游生成的 opaque identity,向量库既不知道它的业务先后,也不得按 r10 > r9 之类的字符串关系猜顺序。安全的单 point 状态机是:

  • ID 不存在且调用方断言当前不存在:创建;
  • 完全相同的 point 重试:no-op,即使第一次成功响应丢失也不重复增长版本;
  • ID 存在且内容不同:调用方必须提交表示 expected-current 的 expected_source_revision,只有它与当前值完全相等才更新;
  • 同一 source_revision 却出现不同 vector、payload 或 content hash:冲突,不得当作更新;
  • 更新成功后,仍携带旧 expected-current 的并发/迟到写:CAS 失败,不能覆盖当前值。

因此“新 revision”指上游工作流在成功 CAS 后发布的新身份,不是向量库通过 revision 字符串大小判断出来的结论。Opaque 也不等于可复用:canonical source 必须保证同一 point 跨完整生命周期不重复使用旧 source_revision,否则会出现 ABA,例如 r1 → delete → r2 → delete → r1 后,旧 delete(r1, d1) 可能再次匹配。更强系统应把不可复用 generation、单调 row version 或永久历史 fence 纳入 CAS。

请求可携带:

  • event/job ID;
  • expected source revision;
  • content/input hash;
  • expected store/row version;
  • embedding contract signature。

产品是否提供 compare-and-set、事务或写 ordering 不同。若没有,应用层仍要保存版本并在写前/写后对账,不能把“upsert”误解为自动解决乱序并发。截至 2026-07-22,Qdrant Points 文档给出 payload version 条件更新与 insert_only / update_only 等 update mode;这些是产品级并发原语,仍不会自动理解业务 source_revision 的先后或删除政策。

部分成功

批量写入可能:

  • 全部成功;
  • 部分 point 成功;
  • 客户端超时但服务端已完成;
  • 某 shard/replica 成功、另一些待恢复;
  • 合法记录与坏维度/坏 payload 混在一批。

可靠流程:

  1. 每项有稳定 ID 与 input hash;
  2. 保存 provider/database request ID;
  3. 解析逐项或批次结果;
  4. 对成功项做 read-back/状态确认(按风险);
  5. 只重试未知/临时失败项;
  6. 永久坏数据隔离;
  7. 用 expected IDs - published IDs 闭合差集;
  8. 记录重试总预算。

不能因一次 HTTP 200 宣称全批完整,也不能因一次 timeout 全量生成新 ID 重写。

更新是多字段一致性问题

正文或 chunk 变化时,至少需要同步:

  • vector;
  • source revision;
  • content/input hash;
  • document/chunk mapping;
  • ACL/status;
  • embedding revision;
  • indexed/published revision。

最危险状态是“新 vector + 旧 ACL”或“旧 vector + 新正文”。解决方式取决于产品:

单数据库事务

pgvector 在 PostgreSQL 表中时,可利用数据库 transaction 把 vector、关系字段和业务状态一起提交。仍要处理 embedding API 在事务外完成的问题,通常先生成派生结果,再用短事务写入。

Staging + publish

专用向量库或跨系统流水线可写 staging space/version,完成数量、hash、ACL、quality 对账后原子切 published alias/pointer。

Versioned records

并行保留 point_id + source_revision,查询只读 published revision;切换后清理旧版。增加存储,但回滚清楚。

不要让应用实例各自切换配置,使用统一读指针。

权限更新优先级

ACL 变更不一定需要重新生成 vector,但必须立即更新所有可读路径:

  • 活动 collection 的 payload;
  • old/shadow spaces;
  • sparse/inverted index;
  • query/result/parent cache;
  • replicated copies;
  • export/backup 恢复后的撤销日志。

如果向量与 payload 不能原子更新,安全基线是先将 status 设为不可检索,再更新 ACL/vector,验证后重新发布。不要在中间态保留过宽权限。

删除的三个层次

逻辑不可见

查询立即排除,常用 tombstone、status=deleted 或删除 point。记录 deletion event、tenant、source revision 和时间/序号。Delete 可能先于索引写入到达:即使当前向量库从未见过该 point,也应为目标 tenant/source/event 落 deletion intent fence;若把“未找到”直接当成功且不留状态,迟到 create 会把已删除内容重新写入。

索引/存储回收

ANN 图、segment、WAL、compaction 可能延后回收。删除 API 成功不等于磁盘空间立刻下降,也不等于旧 segment 不再存在于备份。

保留与合规

同步处理:

  • canonical source;
  • 所有 chunks/points/named vectors;
  • 所有 embedding spaces;
  • sparse/keyword index;
  • caches、exports、debug artifacts;
  • snapshots/backups 的保留与到期;
  • 审计证明。

是否能立刻从不可变备份删除取决于治理/法规和系统设计;需记录隔离、到期与恢复后重放删除的机制,不能虚假承诺“任何副本已物理擦除”。

Tombstone 的作用与风险

Tombstone 可阻止旧事件把已删除 point 意外复活,并帮助恢复/迁移后重放删除。它至少包含:

  • point/source ID;
  • tenant/security domain;
  • deleted_source_revision:删除实际绑定的当前 source 身份;
  • 当前删除意图唯一且可追踪的 delete_event_id / delete fence;
  • 删除时的 store/row revision 或时间;
  • retention/physical purge 状态。

风险:

  • tombstone 自身无限增长;
  • ID 被另一 tenant 重用;
  • 未认证调用方用 delete-before-create 恶意占用别人的 point ID;
  • 备份恢复缺 tombstone 导致复活;
  • 并发 upsert/delete 顺序不明确。
  • source identity 被跨生命周期复用,产生无法由当前 tombstone 识别的 ABA。

定义 resurrection policy:同一 tenant 是否允许、需要哪个 source revision/审批;跨 tenant ID 复用默认拒绝。当 tombstone 仍是当前状态时,delete 重试只有 tenant、deleted_source_revision、delete_event_id 三者全部相同才是幂等 no-op,任何部分不同都应进入冲突。Delete-before-create 会写入不存在 ID 的 tombstone,因此删除 API 必须先从可信身份与 canonical mapping 派生 tenant/source;否则攻击者可用它进行 ID squatting。

本库玩具项目的 schema v2 保留 point_id + tenant_id + deleted_source_revision + delete_event_id + deleted_at_store_revision。未知/未索引 point 的 delete 也写入相同 fence。Tombstone 默认拒绝 upsert;同 tenant 只有提交匹配旧 source/delete event 的 ResurrectionToken,并使用不同的新 source identity,才能显式 resurrect。该 token 只是并发与重放栅栏,不是授权凭据;调用前仍必须完成真实身份认证、授权和业务审批。跨 tenant 永远拒绝。

玩具库在 resurrection 成功后会移除当前 tombstone,因此 delete_event_id 只在“当前删除态”提供重放 fence,不是永久审计历史。生产系统应把 create/update/delete/resurrection 事件写入外部 append-only lifecycle/audit log,并用 generation/history ledger 防止 revision ABA;本例没有这两项能力,不能声称阻止所有历史旧事件。

Schema v1 tombstone 缺少 source/delete fence,脚本会明确拒绝自动迁移,要求从 canonical source 重建;凭空补一个 fence 会制造无法证明的历史状态。

Embedding/索引版本迁移

完整流程承接 [[Knowledge/AI Agent Engineer/docs-CN/Embedding/04-相似度索引与版本迁移|Embedding 空间迁移]]:

  1. 冻结 old space contract/alias;
  2. 创建 new collection/index;
  3. 从 canonical chunks 重算,不从旧向量猜;
  4. 对账 IDs、source/hash、ACL、tombstone、dimension/norm;
  5. old-query→old-index、new-query→new-index 独立评测;
  6. 影子/灰度;
  7. 原子切读;
  8. 停止旧写入;
  9. 保留回滚窗口;
  10. 按删除/保留政策回收旧空间。

Index 参数变化但 embedding 不变也可用类似 blue/green;这样能区分索引构建失败与模型质量变化。

同步模式

Dual write

应用同时写 source DB 与 vector DB。简单但容易一边成功一边失败;需要 outbox/重试/对账。

Change Data Capture / Outbox

源事务提交业务状态与 outbox event,worker 异步更新向量库。延迟可见,但事件可重放、事实来源清楚。必须处理重复、乱序和 poison event。

Periodic reconciliation

定期比较 canonical ID/revision/hash 与向量库,发现漏项、陈旧、孤儿和 tombstone 不一致。它不是替代实时同步,而是最后完整性网。

可组合:outbox 实时 + nightly reconciliation。

发布对账

每次 batch/revision:

维度检查
Inventoryexpected/published/missing/extra IDs
Versionsource、embedding、schema、index build revision
Integritycontent/input/vector hashes、dimension、finite/norm
Securitytenant/ACL/status、越权测试
Lifecycledeletes/tombstones/purge queue
Qualitycanary/gold top-k 与关键子组
Operationserror/retry、searchable lag、index ready

Extra/orphan points 与 missing points 同样危险;不要只比总数,因为“一缺一多”总数仍相同。

常见错误与排查

  • 重试生成新 ID:改稳定 ID + event/input hash。
  • 旧事件覆盖新正文:比较 source revision/CAS。
  • 未找到 point 就丢弃 delete:仍落 deletion intent fence,阻止迟到 create。
  • 跨生命周期复用 source identity:使用不可复用 generation/row version 或永久 history ledger,避免 ABA。
  • 向量和 ACL 分两步且中间可搜:先撤下 status,或用事务/staging。
  • HTTP 200 当全批成功:逐项/差集对账。
  • 删除后又复活:恢复/乱序 upsert 没有 tombstone/version。
  • 只删当前 collection:旧空间、cache、sparse index 和备份遗漏。
  • 原地覆盖模型向量:新旧空间混合,改双索引。
  • 只比 point count:加 ID set、hash 与 ACL 对账。

练习

  1. 把 source revision 当作 opaque 且不可复用的 identity,为两个并发 writer 写出 expected-current CAS 的接受/拒绝规则,并解释 ABA 风险。
  2. 设计 100 项批写中 97 成功、2 临时失败、1 坏维度的恢复状态机。
  3. 画“ACL 收紧 + vector 更新不能原子”的安全发布顺序。
  4. 从 document 删除映射到 12 chunks、2 spaces、keyword index、2 caches 和 backup retention。
  5. 比较 dual write 与 outbox 的失败场景和对账方式。
  6. 设计 delete-before-create、同 tenant resurrection token、真实授权检查与跨 tenant ID reuse 的策略,解释 fence、授权和永久审计日志不能互相替代。

掌握检查

  • Point 写入绑定 canonical/source/input/embedding revision。
  • Upsert 对完全相同事件幂等;非同值更新显式匹配 expected-current,同 revision 异内容冲突。
  • 部分/未知成功逐项对账,不整批盲重试。
  • Vector、payload、ACL、status 在可见版本中一致。
  • 删除覆盖逻辑不可见、物理回收与保留治理。
  • Delete 即使先于 create 到达也落 current fence;未知 ID 删除、授权、resurrection 与永久审计历史边界清楚。
  • Canonical source 不复用同一 point 的 source identity;更强系统用 generation/history ledger 防 ABA。
  • 模型/索引迁移使用新空间、评测、原子切换和回滚。
  • 定期 reconciliation 检查 ID 集、hash、ACL 和 delete。

小结与下一步

单机生命周期正确后,分片、副本和并发会引入旧读、乱序与部分可用。下一步:[[Knowledge/AI Agent Engineer/docs-CN/向量数据库/05-一致性分片与副本|一致性、分片与副本]]。

参考资料

来源获取日期:2026-07-22。返回 [[Knowledge/AI Agent Engineer/docs-CN/向量数据库/00-目录|向量数据库目录]]。