为什么 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。
先分清当前四条运行平面
当前项目同时保留四类用途,不能写成一条已经完全切换的生产链:
data/01-raw到data/07-manifest的 Rich Batch,用于维护性全量/局部重建;- V3 Event Fast Lane,用 Journal 和 Cursor 把日常变化投影到 Vector/Graph;
- 默认外部
/v1/answer,通过answer-qdrant-local使用 Qdrant,也可对比 zvec/pgvector,并可选 Neo4j; - 当前 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,以及 CALLS、ROUTES_TO、USES_OPERATION、SELECTS_FIELD、RESOLVES_FIELD、READS、WRITES 等 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 可以选择 qdrant、zvec、pgvector,或 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。
参考资料
- Qdrant Collections 官方文档;
- Qdrant Points 官方文档;
- SQLite FTS5 官方文档;
- pgvector 官方项目;
- Neo4j Cypher Manual;
- Neo4j GraphRAG Retriever 官方文档;
- Qwen3 Embedding 技术报告。
Store 的职责越清楚,系统越容易解释一次答案:这部分由语义发现,这部分由精确标识符命中,这条关系由图和 Evidence 证明;没有哪一个数据库被赋予「真相」的特权。
相关文章
70/30 Hybrid Retrieval 和 Graph Anchor 怎样权衡
Vector、Keyword、Parent-Child Context 与 Verified Graph Path 分层组合,避免相似内容被误当成调用证明。
RAG 不是一条必经流水线,而是一种按需取证的能力
先理解目标、先看当前源码,只有跨来源信息真的能改善判断时,才调用 Local RAG 去找证据候选。