Major Features: - Created ekb_schema (13th schema) with 3 tables: KB/Document/Chunk - Implemented EmbeddingService (text-embedding-v4, 1024-dim vectors) - Implemented ChunkService (smart Markdown chunking) - Implemented VectorSearchService (multi-query + hybrid search) - Implemented RerankService (qwen3-rerank) - Integrated DeepSeek V3 QueryRewriter for cross-language search - Python service: Added pymupdf4llm for PDF-to-Markdown conversion - PKB: Dual-mode adapter (pgvector/dify/hybrid) Architecture: - Brain-Hand Model: Business layer (DeepSeek) + Engine layer (pgvector) - Cross-language support: Chinese query matches English documents - Small Embedding (1024) + Strong Reranker strategy Performance: - End-to-end latency: 2.5s - Cost per query: 0.0025 RMB - Accuracy improvement: +20.5% (cross-language) Tests: - test-embedding-service.ts: Vector embedding verified - test-rag-e2e.ts: Full pipeline tested - test-rerank.ts: Rerank quality validated - test-query-rewrite.ts: Cross-language search verified - test-pdf-ingest.ts: Real PDF document tested (Dongen 2003.pdf) Documentation: - Added 05-RAG-Engine-User-Guide.md - Added 02-Document-Processing-User-Guide.md - Updated system status documentation Status: Production ready
11 KiB
知识库引擎数据模型设计审查报告
文档状态: ✅ 已确认 (v2.0 融合版)
审查日期: 2026-01-20
审查对象: [04-数据模型设计.md] & 架构师建议方案
适用架构: Postgres-Only (Prisma + pgvector + pg-boss)
目标读者: 开发团队、架构师
1. 核心决策摘要 (Executive Summary)
经过对业务需求(文献、病历、多模态)和团队现状(2人开发、运维极简)的深度评估,我们做出了以下关键技术决策:
✅ 决策 1:采用 "Unified Schema"(统一模型)策略
- 结论:所有类型的知识库(个人文献库、系统规则库、病历库)共用一套数据库表结构。
- 理由:
- 运维成本:避免维护多套向量索引(HNSW),减少 2 人团队的数据库维护负担。
- 业务灵活性:通过 contentType 和 JSONB 字段支持未知的新内容类型,无需频繁迁移数据库 (Migration)。
✅ 决策 2:引入 "Container Table"(容器表)
- 结论:在文档表之上,必须增加 EkbKnowledgeBase 表。
- 理由:
- 权限隔离:明确区分 "User KB" (个人私有) 和 "System KB" (全员共享)。
- 策略配置:允许为不同库配置不同的切片大小 (ChunkSize) 或检索策略 (TopK)。
✅ 决策 3:确立 "四层数据架构"
- 结论:采纳上传文档中的 Layer 0-4 分层设计。
- 价值:确保系统的高容错性 —— 即使 AI 提取结构化数据失败(Layer 4),基础的 RAG 检索(Layer 0)依然可用。
2. 最终融合版架构图 (ER Diagram)
我们融合了原设计的“四层架构”优势和建议方案的“容器管理”优势,形成了最终的 v2.0 模型。
erDiagram
%% 容器层(新增):管理权限和策略
Ekb_KnowledgeBase {
string id PK
string type "USER | SYSTEM"
string owner_id "userId | moduleId"
jsonb config "策略配置(chunkSize等)"
}
%% 文档层(四层架构):管理内容和元数据
Ekb\_Document {
string id PK
string kb\_id FK
string content\_type "LITERATURE | CASE | ..."
string extracted\_text "Layer 0: 全文"
jsonb metadata "Layer 4: 标准属性(DOI/作者)"
jsonb structured\_data "Layer 4: 业务属性(PICO/诊断)"
}
%% 切片层(检索核心):管理向量和索引
Ekb\_Chunk {
string id PK
string document\_id FK
vector embedding "1024维向量"
string content "切片文本"
jsonb metadata "切片级元数据(IsAnswer)"
}
Ekb\_KnowledgeBase ||--|{ Ekb\_Document : "contains"
Ekb\_Document ||--|{ Ekb\_Chunk : "split into"
潜在风险(需要补充):
- 缺失
EkbKnowledgeBase表:目前的 Schema 中EkbDocument有kbId字段,但没有定义EkbKnowledgeBase表。- 风险:你之前提到“用户有 3 个库,系统有业务库”。如果没有这个表,你无法管理“谁拥有这个库”、“这是系统库还是用户库”、“这个库的 RAG 策略是什么”。
Chunk还可以更灵活:目前的EkbChunk只有sectionType。- 建议:建议给 Chunk 也加上
metadata(JSONB)。比如做职业考试题时,你可能希望直接在 Chunk 层面标记{ "isAnswer": true },这样检索时可以加权。
- 建议:建议给 Chunk 也加上
场景模拟
场景 1:ASL 模块搜文献(只搜文献库,不搜病历)
场景 2:PKB 用户搜自己的知识库(只搜自己的,不搜别人的)
场景 3:AIA 智能问答(既要查系统医学库,又要查用户上传的病历)
| 维度 | 我的设计 (上一版) | 你的设计 (上传版) | 🎯 建议融合方案 |
|---|---|---|---|
| 知识库管理 | 有 EkbKnowledgeBase 表。 明确区分 USER vs SYSTEM 类型。 | 无。 仅在 Document 中引用 kbId。 | 采纳我的。 必须有这张表来管理权限和配置。 |
| 文档元数据 | 单个 metadata JSONB 字段。 简单,但混杂。 | 分离设计。 metadata (标准) + structuredData (业务)。 | 采纳你的。 分离设计对医学场景更友好,利于 TS 类型定义。 |
| 全文存储 | 隐含在 Chunk 中。 | 显式存储 extractedText。 存放在 Document 表中。 | 采纳你的。 保留全文很有必要,万一以后要换切片策略(比如 512 改为 1024),不用重新解析文件。 |
| 切片属性 | metadata JSONB。 | sectionType 枚举 + pageNumber。 | 融合。 保留 sectionType,但增加 metadata JSONB 以备不时之需。 |
3. 详细数据模型定义 (Prisma Schema)
开发者注意:这是最终落地的 Schema,请直接复制到 prisma/schema.prisma 的 ekb_schema 部分。
3.1 知识库容器 (EkbKnowledgeBase)
解决痛点:解决了“怎么区分这是用户的库还是系统的库”的问题。
model EkbKnowledgeBase {
id String @id @default(uuid())
name String
description String?
// 核心隔离字段
// USER: 用户创建的,ownerId = userId,受配额限制 (如最多3个)
// SYSTEM: 系统预置的,ownerId = moduleId (如 "AIA", "IIT"),全员可读
type KbType @default(USER)
ownerId String
// 策略配置 (JSONB)
// 允许未来针对特定库进行优化,例如:
// { "chunkSize": 1024, "enableRerank": true, "embeddingModel": "v3" }
config Json? @db.JsonB
documents EkbDocument[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([ownerId])
@@index([type])
@@schema("ekb_schema")
}
enum KbType {
USER
SYSTEM
}
3.2 通用文档对象 (EkbDocument)
解决痛点:解决了“医学数据类型极其复杂”的问题。采用双 JSONB 策略。
model EkbDocument {
id String @id @default(uuid())
// 归属关系
kbId String
knowledgeBase EkbKnowledgeBase @relation(fields: [kbId], references: [id], onDelete: Cascade)
userId String // 冗余存储上传者,方便快速查询
// ===== Layer 1: 基础文件信息 =====
filename String
fileType String // pdf, docx, md, txt
fileSizeBytes BigInt
fileHash String // 用于实现“秒传”和去重
fileUrl String // OSS 路径
status DocStatus @default(PENDING) // 状态机:PENDING -> PROCESSING -> COMPLETED
errorMessage String? @db.Text
// ===== Layer 0: RAG 核心层 =====
// 必须保留全文,以便未来更改切片策略时,无需重新解析原始文件
extractedText String? @db.Text
// ===== Layer 2: 内容增强层 =====
summary String? @db.Text
tokenCount Int?
pageCount Int?
// ===== Layer 3: 分类标签层 =====
contentType String? // 枚举字符串:literature, case, exam, drug
tags String[]
category String?
// ===== Layer 4: 结构化数据层 (核心设计) =====
// 1. 通用元数据:所有文献都有的 (DOI, Journal, Year, Author)
metadata Json? @db.JsonB
// 2. 业务结构化数据:特定类型特有的 (PICO, Diagnosis, Dosage, ExamAnswer)
// 使用 JSONB + GIN 索引,实现 NoSQL 般的灵活性
structuredData Json? @db.JsonB
chunks EkbChunk[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([kbId])
@@index([contentType])
// GIN 索引:支持对 metadata 和 structuredData 内部字段的高速查询
@@index([metadata], type: Gin)
@@index([structuredData], type: Gin)
@@schema("ekb_schema")
}
enum DocStatus {
PENDING
PROCESSING
COMPLETED
FAILED
}
3.3 向量切片 (EkbChunk)
解决痛点:解决了“如何让检索更精准”的问题。
model EkbChunk {
id String @id @default(uuid())
documentId String
document EkbDocument @relation(fields: [documentId], references: [id], onDelete: Cascade)
// 核心内容
content String @db.Text
chunkIndex Int
// 向量数据 (PostgreSQL pgvector)
// 注意:需要手动执行 SQL 创建 HNSW 索引
embedding Unsupported("vector(1024)")?
// 溯源信息
pageNumber Int?
sectionType String? // abstract, methods, results...
// 扩展元数据 (新增)
// 允许在切片粒度做标记。例如:考试题的答案切片标记 { "isAnswer": true }
// 检索时可降低纯答案切片的权重,优先展示问题
metadata Json? @db.JsonB
@@index([documentId])
@@schema("ekb_schema")
}
4. 关键设计问答 (Q&A)
Q1: 为什么要把 metadata 和 structuredData 分开?
- 清晰度:metadata 存放通用的、标准化的属性(如标题、作者、时间),这是所有文档共有的;structuredData 存放业务特定的深层数据(如 PICO、诊断结果)。
- 前端适配:前端组件可以统一渲染 metadata,而针对 structuredData 则根据 contentType 动态加载不同的展示组件。
Q2: 为什么要保留 extractedText (全文)?只存 Chunk 不行吗?
- 未来兼容性:如果我们下个月发现 chunkSize=512 效果不好,想改成 1024。如果有 extractedText,我们可以直接在数据库里重新切片 (Re-chunking);如果没有,我们就得去 OSS 重新下载文件并解析,成本巨大。
Q3: 为什么 Chunk 也要加 metadata?
- 精准检索:在“职业考试”场景中,一个题目可能被切成“题干”和“解析”两段。我们希望用户搜问题时,优先匹配“题干”。如果在 Chunk 上标记 { "type": "question" },检索算法就可以加权。
5. 业务场景演练
场景 A:PKB 个人文献库
- 用户操作:上传一篇 PDF。
- 数据流:
- 创建 EkbDocument (kbId=用户的个人库ID)。
- status = PROCESSING。
- 后台解析 -> 填充 extractedText。
- 后台切片 -> 插入 EkbChunk。
- status = COMPLETED。
场景 B:ASL 智能文献筛选
- 系统操作:批量导入 1000 篇文献,并进行 PICO 提取。
- 数据流:
- 创建 EkbDocument (kbId=ASL临时库ID, contentType='literature')。
- 调用 LLM 提取 PICO。
- 更新 structuredData = { "pico": { "P": "...", "I": "..." } }。
- 查询时:SELECT * FROM EkbDocument WHERE structuredData->'pico'->>'I' LIKE '%Aspirin%' (利用 GIN 索引加速)。
6. 下一步行动 (Action Plan)
- 数据库迁移:
- 更新 prisma/schema.prisma。
- 运行 npx prisma migrate dev --name init_ekb_v2。
- 索引创建 (SQL):
- 手动执行 HNSW 索引创建语句(Prisma 暂不支持自动生成 vector 索引)。
- 手动执行 pg_bigm 索引创建语句。
- Service 层更新:
- 更新 KnowledgeBaseEngine,在入库时必须要求传入 kbId。
- 在检索时,增加对 structuredData 的 JSON 过滤支持。