为什么 Vector、Keyword 和 Neo4j 要使用不同存储

语义相似、精确标识符和关系遍历是三种查询问题,强行放入一个数据库会牺牲正确性与可解释性。

本页目录 · 10 节

「配置标签对应哪段代码」需要语义召回,「RebuildProjectionJob 在哪里定义」需要精确 Identifier,「它最终调用哪个 Provider」需要有方向、有类型、有 Evidence 的路径遍历。这三种问题的数据结构和评分方式不同,强行交给一套「万能数据库」,最终会在至少一类问题上失真。

mt_rag 使用 Polyglot Persistence:Qdrant 处理 Dense Vector,SQLite FTS/Exact Alias 处理 Lexical Identity,Neo4j 处理 Canonical Relation;CAS 与 Manifest 仍然是它们背后的事实和版本底座。

本篇知识地图

flowchart TB
  C["CAS + Manifest<br/>replayable evidence"]
  C --> V["Qdrant<br/>semantic similarity"]
  C --> K["SQLite FTS + Exact Alias<br/>identifier / lexical"]
  C --> G["Neo4j<br/>verified relation / path"]
  V --> A["Evidence Bundle"]
  K --> A
  G --> A
  A --> API["POST /v1/answer"]

这里的知识点是 Dense Index、Lexical Index、Exact Identity、Graph Traversal、Parent/Child Context、Projection Consistency 和 External API Boundary。

先分清当前四条运行平面

当前项目同时保留四类用途,不能写成一条已经完全切换的生产链:

  1. data/01-rawdata/07-manifest 的 Rich Batch,用于维护性全量/局部重建;
  2. V3 Event Fast Lane,用 Journal 和 Cursor 把日常变化投影到 Vector/Graph;
  3. 默认外部 /v1/answer,通过 answer-qdrant-local 使用 Qdrant,也可对比 zvec/pgvector,并可选 Neo4j;
  4. 当前 System RAG Shadow Query,使用独立 Manifest、Qdrant Collection、Keyword DB 和 Canonical System Graph,尚未 Cutover。

本章讲的是共同的存储职责,并在涉及严格 70/30、Canonical Graph 或 Generation Gate 时特指 System RAG Shadow。Readiness 不能自动把 Shadow 变成外部 API 的 Production Store。

Qdrant:负责相似,不负责关系证明

当前 System RAG Embedding 使用本地 Qwen/Qwen3-Embedding-0.6B,1024 维,Cosine Distance。Active System Runtime 的 max_seq_length 配置为 2048;配置中另有模型能力 Profile 记录 8192,这两者不能混写,运行时应以实际 Embedding 配置和 Index Manifest 为准。

写入 Payload 不只包含 Text,还保留:

source_id / source_type / chunk_id / parent_id
title / summary / locator / graph_ids
original_content_hash / original_cas_uri
repository / commit_sha / file_path / symbol
freshness or currentness fields

查询前会验证 Query Embedder 的 Model、Dimension 和 Runtime Profile 与 Index Manifest 完全一致;Source Manifest Generation 或 Vector Consumer Cursor 落后也会直接报 Stale。相同的 1024 维 Embedding Cache 还能供 zvec、pgvector 对比,从而把「模型效果」与「Store 实现」分开测量。

Vector Payload 中的 graph_ids 只是跳转锚点,不是关系本身。把 calls=[...] 塞进 Payload 可以展示一层邻居,却无法高效回答方向、Relation Type、路径长度和每条 Edge Evidence。

SQLite FTS 与 Exact Alias:守住代码身份

System Keyword DB 有三组关键结构:

  • chunks:保存 Child/Parent 文本、Source Identity 与 Payload;
  • FTS5 chunks_fts:只索引 Child 的 Title、Summary 和 Text,使用 Trigram Tokenizer;
  • exact_aliases:为 Issue Key、File Path、Symbol、Graph ID、Source ID 和标题建立 Exact Alias。

FTS 解决自然语言与词法召回;Exact Alias 解决 ISSUE-1234、GraphQL Field、CamelCase Method 这类不能只靠语义近似的问题。Exact Candidate 如果没进入 Dense Top-K,System Shadow 会按 Point ID 向 Qdrant取得真实 Cosine Score,再把它加入 Dense Rank;不会伪造 Rank 来让它满足 70/30。

Keyword DB 还保存 Parent Row。Child 命中后通过 parent_id Hydrate Parent Text,兼顾精确定位与完整阅读。Parent Context 不因此成为一条新的独立高分 Evidence。

Neo4j:只存可遍历语义图

Neo4j System Graph 保存 Canonical Entity、Typed Relation 和独立 Evidence:Repository、File、Class、Method、GraphQLOperation、Endpoint、Page、Issue 等 Node,以及 CALLSROUTES_TOUSES_OPERATIONSELECTS_FIELDRESOLVES_FIELDREADSWRITES 等 Relation。

它不保存原始 PDF、完整 HTML、Embedding 或 Full CPG Blob;这些回到 CAS。它也不是 Vector Store。Graph Query 的价值是:从已解析 Anchor 出发,按方向、Relation Allowlist、Hop Bound 和 Edge Evidence 找 Path。

System Shadow Query 先检查 Projection Marker、Source Manifest 与 Graph Cursor,再只返回 confidence=VERIFIED 且 Evidence 完整的路径。默认外部 API 的 Neo4j Expansion 是另一条运行路径,配置的最大 Graph Depth 为 3;Shadow search_verified_paths 当前调用上限是 4 Hop。两套数值都不是「图越深越好」的通用结论。

为什么不使用分布式事务

Qdrant、SQLite 和 Neo4j 无法共享一个本地 ACID Transaction。当前设计使用 Stable ID、Manifest、Independent Cursor 和 Idempotent Upsert/Delete 达成可检测的最终一致性:

flowchart LR
  M["Source Manifest G(current)"] --> V["Vector Cursor G(current)"]
  M --> G["Graph Cursor G(current)"]
  V --> Q{"query gate"}
  G --> Q
  Q -->|both current| A["serve verified path"]
  Q -->|any lag| S["stale / partial / insufficient"]

Vector 可以先更新,Graph 可以稍后重试,但查询不能把两套不同 Generation 的结果拼成「已验证调用链」。这种策略放弃瞬时全局事务,换取 Store Failure Isolation 和可重放。

外部调用方为什么只应该访问 Answer API

产品代码直接读取 Qdrant 或 Neo4j,会绕过 Source Type Policy、Manifest Gate、Rerank、Citation Guard 和 Fail-closed Output。当前支持的边界是 POST /v1/answer 或流式 /v1/answer/stream

API 可以选择 qdrantzvecpgvector,或 compare_stores 返回对比;Neo4j 通过 use_neo4j_graph 作为关系扩展,不出现在 vector_store 枚举里。Store Comparison 应使用同一 Embedding 和同一 Query Set,否则无法判断差异来自模型还是数据库。

失败模式与取舍

Store/边界 失败模式 当前策略
Qdrant Collection 与 Manifest/Embedder 不匹配 查询前校验 Generation、Hash、Model、Dimension、Profile
FTS Index 对应旧 Chunk File Metadata 保存 Input SHA-256,不一致则重建/拒用
Exact Alias 精确结果不在 Dense Top-K 请求真实 Dense Score 后再进入 RRF
Neo4j Graph Projection 或 Cursor 落后 不输出 Verified Path,明确 Stale
多 Store 一边成功、一边失败 独立 Cursor 重试;不伪装成全局完成
Direct Store Access 绕过 Guard 和 Citation 外部只暴露 Answer API
Polyglot Persistence 部署、监控与备份更复杂 每个 Store 可从 CAS/Manifest 重建,职责更清楚

可复核的项目证据

  • config/rag-runtime.json:Embedding、Collection、Vector Store、Neo4j 和 Shadow 配置;
  • src/mt_rag/retrieval/system_index.py:Qdrant Payload、Embedding Cache、SQLite FTS/Exact Alias 和 Manifest Gate;
  • src/mt_rag/retrieval/stores/adapters.py:Store Adapter Boundary;
  • src/mt_rag/system_graph/batch.py:Canonical Graph 到 Neo4j 的 Projection;
  • src/mt_rag/system_graph/query.py:Graph Projection/Cursor Gate 与 Verified Path;
  • src/mt_rag/service/server.py/v1/answer、Store Selection 和 Strict Request;
  • docs/02-runbooks/local-llm-service.md:当前 External Service、Ports、Profiles 和 API Contract。

参考资料

Store 的职责越清楚,系统越容易解释一次答案:这部分由语义发现,这部分由精确标识符命中,这条关系由图和 Evidence 证明;没有哪一个数据库被赋予「真相」的特权。

相关文章

在做类似的事情?

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

[email protected]