pgvector 向量表与 DuckDB 临时数据源.md 25 KB

pgvector 向量表与 DuckDB 临时数据源

本文引用的文件

  • knowledge_base.sql
  • chat_attachment.sql
  • ai_clean.sql
  • application.yaml
  • KnowledgeVectorMapper.java
  • AttachmentVectorMapper.java
  • RagVectorMapper.java
  • AiCleanTemplateVectorIndex.java
  • RagSchemaService.java
  • OnnxEmbeddingEngine.java
  • AiDuckDb.java
  • DuckdbSnapshot.java
  • CaseDataSourceRegistry.java
  • CaseDataSourceReaper.java

目录

  1. 引言
  2. 项目结构中的数据库边界
  3. 核心数据源与使用场景
  4. pgvector 向量表设计
  5. DuckDB 临时数据源设计
  6. 两种数据源对比
  7. 向量检索参数与索引策略
  8. 生命周期、清理与备份恢复
  9. 性能调优建议
  10. 常见问题排查
  11. 结论

引言

本项目存在两类职责完全不同的持久化层:

  • PostgreSQL + pgvector:用于存储需要长期保留的向量嵌入,包括 Agent RAG、知识库相似检索和意图识别相关的模板语义索引。
  • DuckDB 单文件数据库:作为每个案件的本地临时数据源,承载清洗中间表、分析结果、非表格文件的 AI 识别档案等短期或过程数据。

文档目标不是把所有业务表都写出来,而是聚焦“哪些数据进入 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

图表来源

  • knowledge_base.sql:1-70
  • chat_attachment.sql:1-39
  • ai_clean.sql:1-214
  • ApplicationConfig: application.yaml

章节来源

  • knowledge_base.sql:1-70
  • chat_attachment.sql:1-39
  • ai_clean.sql:1-214

核心数据源与使用场景

PostgreSQL + pgvector

PG 在本项目中承担三类任务:

  1. 知识库与附件的业务元数据:记录文档归属、路径、字符数、块数、状态、下载日志等。
  2. 模板元数据:既有 table_info / table_field 和 AI 侧 ai_clean_template / ai_clean_field。
  3. 向量嵌入:通过 pgvector 的 vector(n) 列存储文本、模板、表结构的嵌入向量,并执行余弦相似度检索。

向量写入由 Java Mapper 直接拼接动态表名,并通过 ::vector 类型转换把 Java 端生成的字面量字符串传给 PostgreSQL。

章节来源

  • KnowledgeVectorMapper.java:1-64
  • AttachmentVectorMapper.java:1-60
  • RagVectorMapper.java:1-113

DuckDB 临时数据源

DuckDB 在本项目中是每案一库的嵌入式分析引擎,而不是共享事务型数据库。它的主要用途包括:

  • 按 AI 清洗模板动态建表并批量写入清洗结果。
  • 流式读取 Excel、CSV 等大文件。
  • 保存非表格文件的 AI 识别档案 file_ai_profile。
  • 为 Python 子进程提供只读快照副本,避免多进程争用同一 DuckDB 文件锁。

DuckDB 连接通过 org.duckdb.DuckDBDriver 以 JDBC 方式打开,并由 CaseDataSourceRegistry 按案件注册到动态数据源路由中。

章节来源

  • AiDuckDb.java:1-200
  • CaseDataSourceRegistry.java:1-200
  • DuckdbSnapshot.java:1-144

pgvector 向量表设计

向量表命名约定

代码中存在三组不同的向量表命名模式:

向量用途 物理表名模式 隔离维度 说明
知识库文档 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。

章节来源

  • knowledge_base.sql:14-20
  • KnowledgeVectorMapper.java:15-21
  • AttachmentVectorMapper.java:13-19
  • AiCleanTemplateVectorIndex.java:98-110
  • application.yaml:66-90

向量表字段设计

三种 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,因为模板可能增量更新。

章节来源

  • KnowledgeVectorMapper.java:22-64
  • AttachmentVectorMapper.java:20-60
  • RagVectorMapper.java:20-113

向量来源与维度约束

向量来源于两个通道:

  1. 本地 ONNX 模型:默认使用 bge-m3,输出 1024 维向量,CLS 池化后做 L2 归一化。
  2. 在线 embedding 模型:当本地开关关闭时,回退到系统中配置的 type='embedding' 活跃模型。

配置中声明的 dimensions 用于建表和状态展示;运行时还会校验实际输出维度是否与声明一致,避免错维向量写入 pgvector。

章节来源

  • application.yaml:66-90
  • OnnxEmbeddingEngine.java:1-200

DuckDB 临时数据源设计

每案一库的数据源注册

CaseDataSourceRegistry 把每个案件映射到一个物理 key,形如 case{caseId},由动态数据源路由在当前案件上下文下返回对应的 DuckDB 实例。开案时它会:

  • 检查是否超过最大同时打开案件数。
  • 校验 DuckDB 文件存在且大小大于零。
  • 根据预算配置计算内存上限和线程数。
  • 创建 DuckdbUnpooledDataSource。
  • 幂等补建 file_ai_profile 表。
  • 将数据源注册到路由表。

章节来源

  • CaseDataSourceRegistry.java:1-200

清洗数据写入 DuckDB

AiDuckDb 是 AI 清洗链路中面向案件 DuckDB 的读写通道。它与通用加载器共用同一个动态数据源路由,但不会继承基类,因为 AI 侧表名在运行期才确定。

关键行为包括:

  • 顺序执行建表 SQL,一条语句一个 Statement,便于定位 CTAS 失败。
  • 按 AI 模板字段动态生成 CREATE TABLE IF NOT EXISTS。
  • 使用批大小为 2000 的流式写入器,避免一次性 collect 导致堆溢出。
  • 写入失败时中断并抛出异常,让上层任务标记失败并可按 job 清理半写行。

章节来源

  • AiDuckDb.java:1-200

DuckDB 快照与多进程隔离

DuckDB 单文件库在同一时刻只允许一个进程以写方式打开;即使另一个进程以只读方式打开,也可能因文件锁失败。为此项目提供了 DuckdbSnapshot:

  • 使用 ATTACH + COPY FROM DATABASE 导出完整库副本。
  • 先写 .tmp-* 临时文件,成功后原子替换目标文件。
  • 导出别名每次随机,避免并发 ATTACH 冲突。
  • 最终删除临时文件。

这使 Python 子进程可以连独立副本,而不与 Java 主进程持有的写锁冲突。

章节来源

  • DuckdbSnapshot.java:1-144

两种数据源对比

维度 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 案件库

章节来源

  • knowledge_base.sql:1-70
  • chat_attachment.sql:1-39
  • ai_clean.sql:1-214
  • RagSchemaService.java:1-181
  • CaseDataSourceRegistry.java:1-200

向量检索参数与索引策略

相似度计算

所有向量检索都使用 pgvector 的 <=> 操作符计算余弦距离,并把分数表示为 1 - distance。因此:

  • 分数越高越相似。
  • 阈值过滤发生在 SQL 层:只有 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"]
    

图表来源

  • KnowledgeVectorMapper.java:44-56
  • AttachmentVectorMapper.java:38-54
  • RagVectorMapper.java:42-52

关键配置项

配置项 默认值 含义
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 检索条数

章节来源

  • application.yaml:66-160

索引重建注意事项

项目没有看到显式的 HNSW 或 IVFFLAT 索引管理脚本,向量检索主要依赖 <=> 全表扫描加 LIMIT。因此重建索引时需要特别注意:

  1. 维度必须一致:pgvector 的 vector(n) 会拒绝错误维度的向量。如果嵌入模型输出维度变化,必须重建对应向量表。
  2. 模型标签变化也要重建:表名包含 {tag},本地模型和在线模型、bge-m3 与其他模型应隔离在不同表中。
  3. 知识库和附件向量表不支持覆盖更新:它们使用 ON CONFLICT ... DO NOTHING,文档内容变更后应先删除旧 doc_id 再重新插入。
  4. 统一模板索引支持 upsert:rag_tpl{tag}_d{dims} 使用 ON CONFLICT ... DO UPDATE,模板字段更新后可增量覆盖。
  5. 重建可能很慢:全量重建有 max-documents-per-rebuild 限制,防止模板库异常膨胀时打爆内存或嵌入额度。

章节来源

  • AiCleanTemplateVectorIndex.java:98-110
  • RagVectorMapper.java:70-113
  • KnowledgeVectorMapper.java:31-64
  • application.yaml:116-145

生命周期、清理与备份恢复

持久化业务数据(PostgreSQL)

持久化数据包括知识库元数据、附件元数据、模板元数据和向量嵌入。这类数据不是按“临时会话”自动清理的,应该由以下策略维护:

  • 知识库:删除知识库时调用 deleteByKb 清理其全部向量行;删除文档时调用 deleteByDoc。
  • 附件向量:孤儿附件清理时按 attachmentId 删除向量行。
  • 模板向量:模板下线或删除时按 doc_id 精确删除。
  • 业务表:知识库、附件、模板等业务表的生命周期由各自业务逻辑管理。

章节来源

  • KnowledgeVectorMapper.java:58-64
  • AttachmentVectorMapper.java:56-60
  • RagVectorMapper.java:90-113

临时数据(DuckDB)

DuckDB 数据的生命周期围绕“案件”展开:

事件 行为
用户打开案件 校验文件存在且非空,分配内存和线程预算,注册数据源
清洗写入失败 已 flush 的行保留,但带 ai_job_id,可用撤销接口清理
用户主动退出案件 释放数据源
用户登出或被顶下线 释放数据源
会话自然过期 兜底回收器定期扫描并释放
导出快照 使用 COPY FROM DATABASE 生成独立副本供其他进程读取

章节来源

  • CaseDataSourceRegistry.java:1-200
  • CaseDataSourceReaper.java:1-125
  • AiDuckDb.java:1-200
  • DuckdbSnapshot.java:1-144

备份与恢复

  • PostgreSQL:应由系统级备份方案处理,包括业务表和向量表。向量表与普通表一样受 PG 备份机制保护。
  • DuckDB:可以直接复制 .ddb 文件,但要注意单文件写锁;更安全的方式是使用 DuckdbSnapshot.export 导出副本后再备份。
  • 快照副作用:导出期间会创建临时文件,成功替换后删除;失败时会尝试清理临时文件。

章节来源

  • DuckdbSnapshot.java:60-144

性能调优建议

PostgreSQL + pgvector

  • 合理设置阈值:score-threshold 过低会引入无关候选,过高可能导致召回为空。模板召回默认 0.30,Agent 选表也使用相近阈值。
  • 控制 top-k:模板召回默认 12,Agent 选表默认 5,附件 RAG 默认 8。top-k 越大,提示词越长,成本越高。
  • 避免跨模型混用:向量表名包含模型标签和维度,切换本地/在线模型时应重建对应表。
  • 监控向量表大小:知识库和附件向量表按业务实体增长,需要关注单表大小和检索延迟。
  • 注意 pgvector 全表扫描:仓库中没有显式 HNSW/IVFFLAT 索引管理,大规模向量表应考虑后续扩展 pgvector 索引策略。

章节来源

  • application.yaml:116-160
  • KnowledgeVectorMapper.java:44-56
  • AttachmentVectorMapper.java:38-54
  • RagVectorMapper.java:42-52

DuckDB 临时数据源

  • 限制并发案件数:默认最多同时打开 8 个案件,避免 N 倍叠加内存和线程。
  • 分摊内存预算:每个案件实例的 SET GLOBAL memory_limit 和 threads 由 DuckdbBudgetProperties 分摊,不要无界放大。
  • 使用流式写入:AI 清洗使用批大小 2000 的流式写入器,避免一次性收集大量行。
  • 谨慎开启 Python 子进程:子进程不能直接连主库,必须用 DuckdbSnapshot 导出副本。
  • 关注回收器:兜底回收器按 token 有效性判断,保守跳过失败查询,防止误关正在使用的案件。

章节来源

  • application.yaml:31-58
  • DuckdbUnpooledDataSource.java:1-200
  • AiDuckDb.java:1-200
  • CaseDataSourceReaper.java:1-125

常见问题排查

向量维度不一致

现象:写入向量时报错,或 pgvector 拒绝写入。
原因:嵌入模型输出维度与 vector(n) 列声明维度不一致。
处理:确认 zsjz.embedding.dimensions 与实际模型输出一致;本地模型由 OnnxEmbeddingEngine 首次推理探测维度,在线模型也有类似校验。

章节来源

  • OnnxEmbeddingEngine.java:140-200
  • application.yaml:66-90

向量检索结果为空

现象:Agent 选表或知识库检索返回空列表。
可能原因:

  • 向量索引尚未重建。
  • 向量索引开关关闭。
  • 嵌入模型不可用。
  • 相似度阈值过高。
  • 知识库/附件 payload 过滤条件不匹配。

处理:先检查索引状态接口,必要时调用重建接口;调整 score-threshold;确认 payload 中 kbId 或 sessionId 与检索参数一致。

章节来源

  • RagSchemaService.java:80-181
  • KnowledgeVectorMapper.java:44-56
  • AttachmentVectorMapper.java:38-54

多进程连接 DuckDB 失败

现象:Python 子进程无法打开案件 DuckDB 文件。
原因:Java 主进程持有写锁,DuckDB 单文件不允许其他进程写,甚至某些情况下只读也失败。
处理:使用 DuckdbSnapshot.export 导出副本后,让子进程连接副本。

章节来源

  • DuckdbSnapshot.java:1-144

案件数据源无法释放

现象:多人开案后达到 max-open-cases 上限。
可能原因:用户关闭浏览器后会话过期但没有触发监听器,导致数据源残留。
处理:兜底回收器会定期扫描;检查 reaper-initial-delay-ms 和 reaper-interval-ms;确认归属 token 是否失效、是否有在途任务探针阻止回收。

章节来源

  • CaseDataSourceReaper.java:1-125
  • application.yaml:31-58

结论

本项目的数据分层比较清晰:

  • PostgreSQL + pgvector 负责长期业务数据和向量嵌入,支撑 Agent RAG、知识库相似检索和模板语义识别。
  • DuckDB 负责每案临时分析和清洗,强调本地计算、流式写入和进程内生命周期管理。
  • 两者不是替代关系,而是互补关系:模板元数据进 pgvector 向量索引,但不扫描 DuckDB 案件库;清洗结果留在 DuckDB,不做向量检索。
  • 运维重点在于:向量维度与模型标签一致性、阈值与 top-k 调参、pgvector 表生命周期管理、DuckDB 并发与内存预算控制、以及案件数据源的回收与快照导出。