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
267 lines
11 KiB
Markdown
267 lines
11 KiB
Markdown
# **知识库引擎数据模型设计审查报告**
|
||
|
||
文档状态: ✅ 已确认 (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"
|
||
|
||
**潜在风险(需要补充):**
|
||
|
||
1. **缺失 `EkbKnowledgeBase` 表**:目前的 Schema 中 `EkbDocument` 有 `kbId` 字段,但没有定义 `EkbKnowledgeBase` 表。
|
||
* *风险*:你之前提到“用户有 3 个库,系统有业务库”。如果没有这个表,你无法管理“谁拥有这个库”、“这是系统库还是用户库”、“这个库的 RAG 策略是什么”。
|
||
2. **`Chunk` 还可以更灵活**:目前的 `EkbChunk` 只有 `sectionType`。
|
||
* *建议*:建议给 Chunk 也加上 `metadata` (JSONB)。比如做职业考试题时,你可能希望直接在 Chunk 层面标记 `{ "isAnswer": true }`,这样检索时可以加权。
|
||
|
||
场景模拟
|
||
|
||
场景 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。
|
||
* **数据流**:
|
||
1. 创建 EkbDocument (kbId=用户的个人库ID)。
|
||
2. status \= PROCESSING。
|
||
3. 后台解析 \-\> 填充 extractedText。
|
||
4. 后台切片 \-\> 插入 EkbChunk。
|
||
5. status \= COMPLETED。
|
||
|
||
### **场景 B:ASL 智能文献筛选**
|
||
|
||
* **系统操作**:批量导入 1000 篇文献,并进行 PICO 提取。
|
||
* **数据流**:
|
||
1. 创建 EkbDocument (kbId=ASL临时库ID, contentType='literature')。
|
||
2. 调用 LLM 提取 PICO。
|
||
3. 更新 structuredData \= { "pico": { "P": "...", "I": "..." } }。
|
||
4. **查询时**:SELECT \* FROM EkbDocument WHERE structuredData-\>'pico'-\>\>'I' LIKE '%Aspirin%' (利用 GIN 索引加速)。
|
||
|
||
## **6\. 下一步行动 (Action Plan)**
|
||
|
||
1. **数据库迁移**:
|
||
* 更新 prisma/schema.prisma。
|
||
* 运行 npx prisma migrate dev \--name init\_ekb\_v2。
|
||
2. **索引创建 (SQL)**:
|
||
* 手动执行 HNSW 索引创建语句(Prisma 暂不支持自动生成 vector 索引)。
|
||
* 手动执行 pg\_bigm 索引创建语句。
|
||
3. **Service 层更新**:
|
||
* 更新 KnowledgeBaseEngine,在入库时必须要求传入 kbId。
|
||
* 在检索时,增加对 structuredData 的 JSON 过滤支持。 |