增量投影怎样让 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:
- 批次整体成功,且不是 Dry-run,才推进到
through_sequence; - Exception、Partial Apply、Dry-run、Missing Embedding 都不推进;
- 失败记录 Event Sequence、Event ID、Run ID 和 Error;
- 下一次从同一个 Cursor 重试;
- Store 使用 Stable ID 和幂等 Upsert/Delete,允许重放已写成功但 Cursor 尚未提交的操作。
Vector Stream 还有顺序阻断:pending_embedding 或 missing_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.py与source_graphs.py:Fast Lane Projection;docs/03-reference/incremental-refresh-v3-event-design.md:完整 Transaction、Consumer 和 Staged Enablement Contract。
参考资料
- Debezium Architecture 官方文档;
- Debezium Outbox Event Router;
- RFC 9110:Idempotent Methods;
- SQLite Transactions 官方文档;
- SQLite Write-Ahead Logging;
- Qdrant Points:幂等写入;
- Qdrant Collection Aliases。
增量投影真正的完成条件不是「本轮 Pipeline 跑完」,而是每个启用 Consumer 都能从同一变化事实独立重放、验证,并明确说明自己还差多少 Lag。
相关文章
为什么用本地不可变 CAS 保存证据底座
向量库和图数据库都是可重建投影;CAS 保存 Raw、IR 与 CPG,Manifest Generation 负责引用这些不可变对象。
RAG 不是一条必经流水线,而是一种按需取证的能力
先理解目标、先看当前源码,只有跨来源信息真的能改善判断时,才调用 Local RAG 去找证据候选。