本文引用的文件
本项目存在两类职责完全不同的持久化层:
文档目标不是把所有业务表都写出来,而是聚焦“哪些数据进入 pgvector”“哪些数据留在 DuckDB”,以及两者在类型、索引、生命周期和运维上的差异。
从脚本和实现看,持久化数据主要落在 PostgreSQL;向量能力依赖 pgvector 扩展;案件级临时计算和数据清洗走 DuckDB JDBC。
graph TB
PG["PostgreSQL<br/>主库"] --> KB["知识库元数据<br/>knowledge_base / knowledge_doc"]
PG --> ATT_META["附件元数据<br/>chat_attachment"]
PG --> TPL_META["模板元数据<br/>ai_clean_template / ai_clean_field"]
PG --> PLAT["平台与治理表<br/>table_info / table_field 等"]
PG --> KB_VEC["知识库向量表<br/>kb_vec_{kbId}{tag}_d{dims}"]
PG --> ATT_VEC["附件向量表<br/>chat_att_vec_{caseId}_d{dims}"]
PG --> SCHEMA_VEC["统一表结构向量表<br/>rag_tpl{tag}_d{dims}"]
DUCK["DuckDB 单文件库<br/>每案一个 .ddb"] --> CASE_TABLES["案件动态表<br/>ai_clean_* 生成表"]
DUCK --> FILE_AI_PROFILE["非表格文件 AI 识别档案<br/>file_ai_profile"]
APP["AI Server 应用"] --> PG
APP --> DUCK
图表来源
章节来源
PG 在本项目中承担三类任务:
table_info / table_field 和 AI 侧 ai_clean_template / ai_clean_field。vector(n) 列存储文本、模板、表结构的嵌入向量,并执行余弦相似度检索。向量写入由 Java Mapper 直接拼接动态表名,并通过 ::vector 类型转换把 Java 端生成的字面量字符串传给 PostgreSQL。
章节来源
DuckDB 在本项目中是每案一库的嵌入式分析引擎,而不是共享事务型数据库。它的主要用途包括:
file_ai_profile。DuckDB 连接通过 org.duckdb.DuckDBDriver 以 JDBC 方式打开,并由 CaseDataSourceRegistry 按案件注册到动态数据源路由中。
章节来源
代码中存在三组不同的向量表命名模式:
| 向量用途 | 物理表名模式 | 隔离维度 | 说明 |
|---|---|---|---|
| 知识库文档 RAG | kb_vec_{kbId}{tag}_d{dims} |
知识库 ID + 模型标签 + 维度 | 每个知识库一张向量表,检索时再强制过滤 payload->>'kbId' |
| 附件大文档 RAG | chat_att_vec_{caseId}_d{dims} |
案件 ID + 维度 | 同案件不同会话之间靠 payload 中的 sessionId 隔离 |
| 统一表结构语义索引 | rag_tpl{tag}_d{dims} |
模型标签 + 维度 | 全局一张表,同时服务 Agent 选表和 AI 模板匹配召回 |
其中 {tag} 来自当前使用的嵌入模型标签(例如本地 bge-m3),{dims} 是配置声明的向量维度,默认值为 1024。
章节来源
三种 Mapper 中的向量表基本结构一致,都是“文档标识 + 原文片段 + 结构化负载 + 向量列”。
| 字段 | 类型 | 作用 |
|---|---|---|
doc_id |
VARCHAR(128) 主键 |
唯一标识一个向量片段或模板条目 |
content |
TEXT |
分块后的正文、模板摘要或表结构描述 |
payload |
JSONB |
存储业务隔离键,如 kbId、sessionId、attachmentId、docId、tableId 等 |
embedding |
vector(${dims}) |
pgvector 稠密向量列 |
知识库和附件向量表使用 ON CONFLICT (doc_id) DO NOTHING 去重;统一表结构索引则使用 ON CONFLICT (doc_id) DO UPDATE,因为模板可能增量更新。
章节来源
向量来源于两个通道:
type='embedding' 活跃模型。配置中声明的 dimensions 用于建表和状态展示;运行时还会校验实际输出维度是否与声明一致,避免错维向量写入 pgvector。
章节来源
CaseDataSourceRegistry 把每个案件映射到一个物理 key,形如 case{caseId},由动态数据源路由在当前案件上下文下返回对应的 DuckDB 实例。开案时它会:
DuckdbUnpooledDataSource。file_ai_profile 表。章节来源
AiDuckDb 是 AI 清洗链路中面向案件 DuckDB 的读写通道。它与通用加载器共用同一个动态数据源路由,但不会继承基类,因为 AI 侧表名在运行期才确定。
关键行为包括:
CREATE TABLE IF NOT EXISTS。章节来源
DuckDB 单文件库在同一时刻只允许一个进程以写方式打开;即使另一个进程以只读方式打开,也可能因文件锁失败。为此项目提供了 DuckdbSnapshot:
ATTACH + COPY FROM DATABASE 导出完整库副本。.tmp-* 临时文件,成功后原子替换目标文件。这使 Python 子进程可以连独立副本,而不与 Java 主进程持有的写锁冲突。
章节来源
| 维度 | PostgreSQL + pgvector | DuckDB 临时数据源 |
|---|---|---|
| 部署形态 | 外部关系数据库,通常常驻 | 进程内嵌入式数据库,每案一个单文件 |
| 主要用途 | 长期保留的业务元数据和向量嵌入 | 案件清洗中间结果、分析结果、临时快照 |
| 典型表 | knowledge_base、knowledge_doc、chat_attachment、ai_clean_template、向量表 |
动态 ai_clean_* 清洗表、file_ai_profile |
| 数据类型 | JSONB、TEXT、BIGINT、TIMESTAMP、vector(n) |
VARCHAR、INTEGER、BIGINT、TIMESTAMP、原生 DuckDB 类型 |
| 索引策略 | pgvector 向量列 + 业务二级索引;向量检索基于余弦距离 | 未看到显式 pgvector 或全文索引;适合列存扫描和聚合 |
| 隔离方式 | 业务字段过滤 + 向量表命名隔离 | 每案一个库文件 + 动态数据源路由 |
| 生命周期 | 随业务实体生命周期增长,需手动或定时清理 | 随案件打开而创建资源,退出/登出/过期回收 |
| 并发访问 | 客户端连接多个会话,PG 负责并发控制 | 单文件写锁限制;Python 子进程必须用快照 |
| 备份恢复 | 由 PG 备份工具处理 | 可复制 .ddb 文件或用 DuckdbSnapshot 导出 |
| 是否参与向量检索 | 是 | 否,向量索引不扫描 DuckDB 案件库 |
章节来源
所有向量检索都使用 pgvector 的 <=> 操作符计算余弦距离,并把分数表示为 1 - distance。因此:
score >= threshold 的结果才返回。返回条数由 LIMIT 控制。
flowchart TD
Start["输入查询向量"] --> Threshold["应用相似度阈值"]
Threshold --> ScoreCalc["计算 1 - 余弦距离"]
ScoreCalc --> Filter{"分数是否大于等于阈值"}
Filter -->|否| Drop["丢弃该候选"]
Filter -->|是| Rank["按分数降序排序"]
Rank --> Limit["取 top-k"]
Limit --> Return["返回 content、payload 和 score"]
图表来源
| 配置项 | 默认值 | 含义 |
|---|---|---|
zsjz.embedding.dimensions |
1024 | 向量维度,用于建表和状态展示 |
zsjz.embedding.enabled |
false | 本地 ONNX 模型开关;false 时回退在线 embedding 模型 |
zsjz.ai-clean.vector.top-k |
12 | AI 模板向量召回候选数 |
zsjz.ai-clean.vector.score-threshold |
0.30 | 模板向量召回相似度阈值 |
etl.rag.top-k |
5 | Agent 表结构语义检索默认返回条数 |
etl.rag.score-threshold |
0.3 | Agent 表结构检索相似度阈值 |
zsjz.chat-attachment.chunk-size |
1000 | 附件 RAG 分块大小 |
zsjz.chat-attachment.max-chunks |
500 | 单个附件最大分块数 |
zsjz.chat-attachment.top-k |
8 | 附件 RAG 检索条数 |
章节来源
项目没有看到显式的 HNSW 或 IVFFLAT 索引管理脚本,向量检索主要依赖 <=> 全表扫描加 LIMIT。因此重建索引时需要特别注意:
vector(n) 会拒绝错误维度的向量。如果嵌入模型输出维度变化,必须重建对应向量表。{tag},本地模型和在线模型、bge-m3 与其他模型应隔离在不同表中。ON CONFLICT ... DO NOTHING,文档内容变更后应先删除旧 doc_id 再重新插入。rag_tpl{tag}_d{dims} 使用 ON CONFLICT ... DO UPDATE,模板字段更新后可增量覆盖。max-documents-per-rebuild 限制,防止模板库异常膨胀时打爆内存或嵌入额度。章节来源
持久化数据包括知识库元数据、附件元数据、模板元数据和向量嵌入。这类数据不是按“临时会话”自动清理的,应该由以下策略维护:
deleteByKb 清理其全部向量行;删除文档时调用 deleteByDoc。attachmentId 删除向量行。doc_id 精确删除。章节来源
DuckDB 数据的生命周期围绕“案件”展开:
| 事件 | 行为 |
|---|---|
| 用户打开案件 | 校验文件存在且非空,分配内存和线程预算,注册数据源 |
| 清洗写入失败 | 已 flush 的行保留,但带 ai_job_id,可用撤销接口清理 |
| 用户主动退出案件 | 释放数据源 |
| 用户登出或被顶下线 | 释放数据源 |
| 会话自然过期 | 兜底回收器定期扫描并释放 |
| 导出快照 | 使用 COPY FROM DATABASE 生成独立副本供其他进程读取 |
章节来源
.ddb 文件,但要注意单文件写锁;更安全的方式是使用 DuckdbSnapshot.export 导出副本后再备份。章节来源
score-threshold 过低会引入无关候选,过高可能导致召回为空。模板召回默认 0.30,Agent 选表也使用相近阈值。章节来源
SET GLOBAL memory_limit 和 threads 由 DuckdbBudgetProperties 分摊,不要无界放大。DuckdbSnapshot 导出副本。章节来源
现象:写入向量时报错,或 pgvector 拒绝写入。
原因:嵌入模型输出维度与 vector(n) 列声明维度不一致。
处理:确认 zsjz.embedding.dimensions 与实际模型输出一致;本地模型由 OnnxEmbeddingEngine 首次推理探测维度,在线模型也有类似校验。
章节来源
现象:Agent 选表或知识库检索返回空列表。
可能原因:
处理:先检查索引状态接口,必要时调用重建接口;调整 score-threshold;确认 payload 中 kbId 或 sessionId 与检索参数一致。
章节来源
现象:Python 子进程无法打开案件 DuckDB 文件。
原因:Java 主进程持有写锁,DuckDB 单文件不允许其他进程写,甚至某些情况下只读也失败。
处理:使用 DuckdbSnapshot.export 导出副本后,让子进程连接副本。
章节来源
现象:多人开案后达到 max-open-cases 上限。
可能原因:用户关闭浏览器后会话过期但没有触发监听器,导致数据源残留。
处理:兜底回收器会定期扫描;检查 reaper-initial-delay-ms 和 reaper-interval-ms;确认归属 token 是否失效、是否有在途任务探针阻止回收。
章节来源
本项目的数据分层比较清晰: