写入、更新、删除与版本
本节目标
你将设计一条可重放、可对账的数据生命周期:稳定 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 混在一批。
可靠流程:
- 每项有稳定 ID 与 input hash;
- 保存 provider/database request ID;
- 解析逐项或批次结果;
- 对成功项做 read-back/状态确认(按风险);
- 只重试未知/临时失败项;
- 永久坏数据隔离;
- 用
expected IDs - published IDs闭合差集; - 记录重试总预算。
不能因一次 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 空间迁移]]:
- 冻结 old space contract/alias;
- 创建 new collection/index;
- 从 canonical chunks 重算,不从旧向量猜;
- 对账 IDs、source/hash、ACL、tombstone、dimension/norm;
- old-query→old-index、new-query→new-index 独立评测;
- 影子/灰度;
- 原子切读;
- 停止旧写入;
- 保留回滚窗口;
- 按删除/保留政策回收旧空间。
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:
| 维度 | 检查 |
|---|---|
| Inventory | expected/published/missing/extra IDs |
| Version | source、embedding、schema、index build revision |
| Integrity | content/input/vector hashes、dimension、finite/norm |
| Security | tenant/ACL/status、越权测试 |
| Lifecycle | deletes/tombstones/purge queue |
| Quality | canary/gold top-k 与关键子组 |
| Operations | error/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 对账。
练习
- 把 source revision 当作 opaque 且不可复用的 identity,为两个并发 writer 写出 expected-current CAS 的接受/拒绝规则,并解释 ABA 风险。
- 设计 100 项批写中 97 成功、2 临时失败、1 坏维度的恢复状态机。
- 画“ACL 收紧 + vector 更新不能原子”的安全发布顺序。
- 从 document 删除映射到 12 chunks、2 spaces、keyword index、2 caches 和 backup retention。
- 比较 dual write 与 outbox 的失败场景和对账方式。
- 设计 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-一致性分片与副本|一致性、分片与副本]]。
参考资料
- Qdrant: Points
- Qdrant API: Upsert points
- pgvector official repository
- PostgreSQL: Transactions
[[Knowledge/AI Agent Engineer/docs-CN/知识库构建/00-目录|知识库构建]]
来源获取日期:2026-07-22。返回 [[Knowledge/AI Agent Engineer/docs-CN/向量数据库/00-目录|向量数据库目录]]。