增量投影怎样让 Vector、Keyword 和 Graph 独立演进

分层 Hash、Delta Planner、Consumer Cursor 和 Shadow Projection 共同解决更新、失败恢复与回滚。

本页目录 · 10 节

企业 Corpus 进入几十万 Chunk 后,「每天全量切块、Embedding、重建 Vector 和 Graph」不再只是慢,它还扩大失败半径:任一阶段失败都会让所有存储一起等待,删除和重试也很难说明做到哪里。

mt_rag 当前的 V3 Fast Lane 采用 Event-driven Incremental Projection:Source Change 先进入 Durable Journal;Vector 与 Graph 产生独立 Delta;Qdrant、pgvector 和 Neo4j 各自维护 Consumer State。Milvus 目前只生成 Dry-run Report,不执行真实写入,也不推进 Cursor;zvec 仍属于另一条 Build/Comparison 路径。失败只阻止对应 Consumer 前进,不让另一条存储链路替它宣称完成。

本篇知识地图

flowchart LR
  S["Source Pull"] --> J["Source Event Journal"]
  J --> C["Code Projection / Source Projector"]
  C --> V["Vector Delta Journal"]
  C --> G["Graph Delta Journal"]
  V --> E["Embedding Readiness"]
  E --> Q["Qdrant Cursor"]
  E --> P["pgvector Cursor"]
  E --> M["Milvus dry-run<br/>no real cursor advance"]
  G --> N["Neo4j Cursor"]
  Q --> X["Delta Validation"]
  P --> X
  M --> X
  N --> X

核心知识点是 Durable Journal、At-least-once Consumer、Stable Event Identity、Tombstone、Independent Cursor、Projection Validation 和 Rollback Boundary。

Event 是变化事实,不是一次脚本日志

当前有三类 Event:

Stream Operation 关键身份
Source upsert / delete Source Type、Entity Type/ID、Revision、Content/Payload Hash
Vector upsert_vector / upsert_payload / delete_point Stable Point ID、Embedding Key、Payload Hash、Source Event ID
Graph Node/Edge Upsert/Delete Stable Item/Edge ID、Props Hash、Source Event ID

Vector Change 会进一步分类:Text Hash 或 Embedding Key 变化才 upsert_vector;只有 Metadata 变化时使用 upsert_payload;完全相同则不产生事件。这样页面更新时间或 Repo Metadata 变化不一定强制重算 Embedding。

Derived Event ID 绑定 Durable source_event_id,而不是临时 Projection Run ID。同一个 Pending Source Event 因崩溃在新 Run 中重放时,Vector/Graph Event 能去重;后续真正的新 Revision 仍然是不同事件,即使内容恰好回到旧值。

Code Projection 为什么先固化再提升 Canonical State

代码增量不能直接读取变化中的 Working Tree。当前流程根据 Durable Previous Revision 与 Current Commit 计算 Changed Files,并从记录的 Commit 使用 git show <revision>:<path> 读取内容。

build-code-projection 先写一个 Content-addressed Immutable Artifact,包含 Changed-file Symbol、Contract、Vector 和 Graph 所需行;校验完整后才提升 Canonical Symbol State,并写 Applied Marker。Neo4j 应用且 Graph Lag 为零后,再为这一份 Projection 写独立 graph-validated.json

sequenceDiagram
  participant J as Source Journal
  participant P as Immutable Code Projection
  participant C as Canonical SQLite
  participant N as Neo4j
  J->>P: project recorded revision
  P->>P: checksum + manifest
  P->>C: transactional promote
  C-->>P: canonical-applied marker
  P->>N: graph events
  N-->>P: graph-validated marker when lag=0

进程在任意位置崩溃,下一次都重放同一 Artifact,不重新解释可变 Working Tree。旧 Commit 已被 Git 回收时,流程才回退到完整 Current Tree,并把不再存在的 Canonical Row 发成 Tombstone。

Fast Lane 与 Rich Lane 解决的是不同新鲜度

Daily Fast Lane 只做 Source Event、Code Projection、Source Vector Delta 和 Source Graph Delta。它为 Jira、Confluence、Attachment、Repository、DDB 生成受限 Parent/Child Vector,并为 Changed Symbol 生成 Vector 与 Call/GraphQL/REST Graph Edge。

Rich Lane 才运行 Catalog、Semantic Enrichment、Hierarchical Chunking、Domain/Impact/Interface Pack 和 Full Vector Export。当前 Rich Parent/Child Export 为十 GB 级;已有本地运行中 Hierarchical Chunking 和 Full Vector Export 都进入分钟级,后者还曾在数分钟后被操作系统终止。因此 Rich Lane 是维护任务,不是每日 Freshness Gate。

这个取舍让「今天改了一个 Symbol」不必重建整个企业语料,但也承认一项限制:部分 Aggregate Transform 还不是 Entity-level Incremental,Rich Projection 的全局视图可能晚于 Fast Lane。

Cursor 是每个 Destination 自己的完成证明

每个 Consumer 按自己的 Cursor 读取连续 Event Batch:

  1. 批次整体成功,且不是 Dry-run,才推进到 through_sequence
  2. Exception、Partial Apply、Dry-run、Missing Embedding 都不推进;
  3. 失败记录 Event Sequence、Event ID、Run ID 和 Error;
  4. 下一次从同一个 Cursor 重试;
  5. Store 使用 Stable ID 和幂等 Upsert/Delete,允许重放已写成功但 Cursor 尚未提交的操作。

Vector Stream 还有顺序阻断:pending_embeddingmissing_vector 会阻止后续 Event 越过它返回给同一 Consumer,避免 Cursor 跳过一条未准备好的向量。Tombstone 不需要 Embedding,可以直接进入 Ready Stream。

删除比新增更危险

增量系统最危险的 Bug 不是漏加一个 Point,而是把一次 Source 不完整误判成大规模删除。当前 Delete Safety Policy 默认阻止:

  • 删除数量超过 1000;或
  • Previous Scope 至少 20 条且删除比例超过 25%。

Missing Vector Export Input 阻止 Tombstone 计算;Empty Graph Input 阻止 Graph Delete;只有显式 Recovery/Large-delete Override 才能绕过,而且必须先人工检查 Source Completeness。

File Add/Modify/Rename/Delete 分别生成明确 Symbol 与 Edge 动作。只有 Authoritative Repository Delete Event 才能删除仓库拥有的全部 Row;Partial Discovery 中没出现某 Repo 绝不是删除证据。

验收状态不能只看 Exit Code

Delta Validation 区分:

  • blocked:必要 Artifact 缺失或 Embedding 阻塞;
  • partial:Adapter Report 缺失、Apply Mode 混合或仍有 Lag;
  • delta_valid:Delta 合法,但未请求 Adapter;
  • dry_run_valid:所有请求 Adapter Dry-run 有效;
  • applied_valid:所有请求 Adapter 真正应用成功且 Cursor Lag 为零。

只有 applied_valid 才设置 applied=true。进程没有报错、Source 拉取完成、Qdrant 已更新,都不能替 Neo4j Cursor 证明完成。

失败模式与取舍

失败点 Cursor/重放行为 Trade-off
Store 写成功,Cursor 提交前崩溃 重放 Stable ID Upsert/Delete 依赖 Destination 操作幂等
Missing Embedding 阻断 Vector Stream,不越过 Sequence 后续 Ready Event 也会等待,保住顺序
Neo4j 失败 Graph Cursor 不动,Vector Consumer 可继续 Store 之间短暂版本不齐,查询 Gate 必须可见
Source Run 无变化 不产生 Derived Work,但旧 Backlog 仍可被消费 Journal 成为 Retry Queue,不能只看 Latest Run 文件
大规模 Tombstone 默认拒绝并要求显式 Override 真正大删除需要维护窗口
Rich Projection 太重 Daily 只走 Fast Lane Aggregate/Domain View 收敛较慢

可复核的项目证据

  • src/mt_rag/refresh/contracts.py:Source/Vector/Graph Event 与 Stable Identity;
  • src/mt_rag/refresh/journal.py:SQLite Journal、Readiness、Checkpoint、Failure 和 Lag;
  • src/mt_rag/refresh/consumer.py:Success-only Cursor Advance;
  • src/mt_rag/refresh/delta.py:Vector Change Classification 与 Delete Safety;
  • src/mt_rag/refresh/code_projection.py:Content-addressed Projection、Applied/Graph Validated Marker;
  • src/mt_rag/refresh/source_vectors.pysource_graphs.py:Fast Lane Projection;
  • docs/03-reference/incremental-refresh-v3-event-design.md:完整 Transaction、Consumer 和 Staged Enablement Contract。

参考资料

增量投影真正的完成条件不是「本轮 Pipeline 跑完」,而是每个启用 Consumer 都能从同一变化事实独立重放、验证,并明确说明自己还差多少 Lag。

相关文章

在做类似的事情?

很乐意就分布式系统、交付流程和应用 AI 交换意见。

[email protected]