know-engine 的文档管理系统实现了一套完整的多版本管理方案,核心目标是:在不停机的前提下,支持知识文档的版本迭代、历史追溯与任意版本切换,同时保证 RAG 检索的向量数据与文档内容始终保持版本一致性。 PS:本章功能演示用到的测试数据: https://nfturbo-file.oss-cn-hangzhou.aliyuncs.com/llm/%E6%B5%8B%E8%AF%95%E7%94%A8%E6%95%B0%E6%8D%AE.zip
多版本支持的数据模型设计
设计思路:逻辑文档与物理版本分离
之前我们设计的只有knowledge_document的单表方案在面对版本管理时会遇到诸多问题:每次更新都会覆盖原始数据,无法回溯;切换版本需要删除再重建,存在服务中断窗口。 于是我们改成采用 「逻辑文档 + 物理版本快照」 分离模型,将文档的"身份信息"与"内容版本"拆分存储:
knowledge_document(逻辑文档)
│ doc_id 文档唯一身份
│ doc_title 标题(跨版本不变)
│ knowledge_base_type 知识库类型(跨版本不变)
│ accessible_by 权限(跨版本不变)
└─ current_version_id ──────────────────────┐
▼
knowledge_document_version(版本快照) 当前激活版本
│ version_id 版本唯一ID
│ doc_id 关联逻辑文档
│ version 语义化版本号(1.0.0)
│ doc_url MinIO 原始文件地址
│ converted_doc_url 转换后文件地址
│ content_hash SHA-256 内容哈希
└─ status 版本处理状态
knowledge_segment(知识分段)
│ document_id 关联逻辑文档 ID
└─ document_version 关联版本 ID(version_id)
核心表结构
knowledge_document(逻辑文档表)该表只存储文档的不变属性,新增 current_version_id 作为指向当前激活版本的"激活指针"。通过更换这个指针即可完成版本切换,代价极低且原子。
create
)
knowledge_document_version(版本快照表)每行存储一个版本的完整快照,通过 UNIQUE KEY uk_doc_version(doc_id, version) 约束保证同一文档的版本号不重复。
create
)
knowledge_segment(知识分段表)分段通过 document_version 字段绑定到具体版本 ID,使得不同版本的分段数据可以并存于同一张表中,互不干扰。
CREATE
)
上传新版本
同一份文档,如果有新的版本,可以通过上传新版本的方式更新文档内容。流程如下:
上传后数据变化如下:1.0.0 -> 2.0.0
| 数据表 | 数据变化 |
| --- | --- |
| knowledge_documen | current_version_id 更新为新版本的 version_id(2.0.0); |
| knowledge_document_version | 新增 1 条新版本记录(2.0.0);status 从 UPLOADED 演进至 VECTOR_STORED; |
| knowledge_segment | 新增新版本(2.0.0)对应的 N 条片段;新版本片段 status 从 STORED → VECTOR_STORED 并回填 embedding_id; |
| Elasticsearch 向量库 | 写入新版本向量;(2.0.0) |
上传后重新选择新的分段方式后,就可以把新文档的内容存储到向量数据库中。这个过程为了不影响用户体验,实现了一个不停机更新:
✅文档如何实现不停机更新
RAG系统中,文档的更新也是个比较常见的操作。我们也支持文档的更新,支持以下这三种操作: 1、文档的基本信息更新,如名称、描述等。 2、指定分段的内容更新。 3、整个文档的更新 文档基本信息更新 更新范围限制 文档基本信息更新(PUT /a LLMentor
版本切换
版本切换(switchVersion)的语义是:将指定的历史版本重新激活为当前版本,使其分段重新进入向量库,接管 RAG 检索流量。
主要流程如下:
版本切换后数据状态如下:2.0.0 -> 1.0.0
| 数据表 | 数据变化 |
| --- | --- |
| knowledge_documen | current_version_id 更新为目标版本的 version_id(1.0.0); |
| knowledge_document_version | 目标版本(1.0.0) status 由 CHUNKED 升为 VECTOR_STORED; |
| knowledge_segment | 目标版本(1.0.0)片段 status 从 STORED → VECTOR_STORED 并回填 embedding_id; |
| Elasticsearch 向量库 | 写入目标版本(1.0.0)向量;删除原当前版本(2.0.0)的向量 |
版本启停能力(deactivate / activate)
版本切换的底层由两个对称操作支撑: 版本失效(deactivateVersion) 将一个 VECTOR_STORED 版本下线:
① 校验版本状态为 VECTOR_STORED
② 调用 cleanupOldVersionData:按 docId + versionId 清理旧版本的 ES 向量
③ 将该版本所有分段状态 VECTOR_STORED → STORED,清空 embeddingId
④ 版本状态 VECTOR_STORED → CHUNKED
关键代码(KnowledgeDocumentServiceImpl.deactivateVersion):
// 清理 ES 向量
documentCleanupService
// 分段状态降级,清空 embeddingId
LambdaUpdateWrapper
knowledgeSegmentMapper
// 版本状态降级
version
knowledgeDocumentVersionService
版本生效(activateVersion) 将一个已切片但未向量化的版本(CHUNKED)重新向量化上线:
① 校验版本状态为 CHUNKED
② 分页扫描该版本下所有 status=STORED 且 skip_embedding=0 的分段
③ 批量 embed 写入 ES → 记录 embeddingId
④ 分段状态 STORED → VECTOR_STORED
⑤ 版本状态 CHUNKED → VECTOR_STORED
关键代码(KnowledgeDocumentServiceImpl.activateVersion):
// 分页向量化,避免单批数据量过大
Page
while
seg
seg
knowledgeSegmentService
page
}
version
knowledgeDocumentVersionService
switchVersion 完整流程
switchVersion 将上述两个操作串联,实现版本的原子切换:
// DocumentProcessServiceImpl.switchVersion
// ① 旧版本所有分段状态降为 STORED(退出向量检索)
LambdaUpdateWrapper
knowledgeSegmentMapper
// ② 目标版本重新向量化(activateVersion 内部已处理完整激活流程)
boolean
// ③ 更新激活指针
document
knowledgeDocumentService
整个切换的过程中,向量数据的处理我们是通过embedAndStore方法实现的,这里面其实