02-文档入库与 metadata:知识进入系统之前先有身份
RAG 的很多问题,表面发生在检索阶段,根子常常在入库阶段。
02-文档入库和 metadata:知识进入系统之前先有身份
RAG 的很多问题,表面发生在检索阶段,根子常常在入库阶段。
文档进入知识库时,需要留下来源、标题、解析状态、产品、版本、权限、内容 hash 和稳定 chunk_id。后面的检索、引用、删除、回滚都依赖这些信息。系统能回答,也要解释答案来自哪里;能召回文本,也要判断是否当前版本;能删除文档,也要让旧 chunk 退出检索结果。
所以 NexaRAG 在文档入库这一环节要补的核心能力,是让知识进入系统之前先获得身份。
这篇的核心判断是:
文档入库是 RAG 证据链的起点。 证据能否被检索、过滤、引用、评测和更新,取决于它入库时留下了多少可靠信息。
API 层只负责接住上传请求
当前知识库上传入口在 api/knowledge_router/upload.py。这个文件把 chunking 和向量库写入交给后面的入库链路,自己专注做 API 层该做的事:读取上传内容,收集表单里的 metadata,把内容交给知识库服务。
核心流程很薄:
content = (await file.read()).decode("utf-8", errors="replace")
metadata = upload_metadata(...)
result = knowledge.add_document(content, file.filename, **metadata)
if should_sync(result):
knowledge.sync_index()
return build_upload_response(result, file.filename)
这里的 doc_type、product、version、access_scope、mime_type 可以从上传表单传进来。用户未填写时,后面的 metadata 构建逻辑还会尝试根据 source 和 title 推断。
这个边界很关键。API 层只需要把“用户上传了一份知识”这件事转交给入库链路,DocumentRegistry 怎么存、chunk 怎么切,都由下游模块负责。
入库服务负责组织依赖
入库主入口在 rag/_ingestion/service.py。KnowledgeIngestion 负责组织几个关键依赖:
EmbeddingServiceDocumentLoaderRegistryDocumentRegistry- Chroma store
- 文本 splitter
- md5 去重文件
真正的入库业务流程交给 rag/_ingestion/pipeline.py 的 add_parsed_document()。
可以把当前入库链路压成这样:
上传文件或文本
-> DocumentLoaderRegistry 解析
-> ParsedDocument
-> build_source_document()
-> duplicate check
-> build_structured_chunks()
-> build_versioned_metadatas()
-> Chroma 写入
-> local fallback 写入
-> DocumentRegistry 注册 active version
-> IngestionResult
-> API response
这条链路里,每一步都在补证据身份。
Loader 先给解析结果定状态
最小 RAG demo 常见写法是直接 open().read(),然后切块写库。NexaRAG 选择在 rag/_loaders/registry.py 里先经过 DocumentLoaderRegistry。
当前支持的主要是纯文本和 Markdown:
PlainTextLoaderMarkdownLoaderUnsupportedLoader
解析结果统一返回 ParsedDocument。这个对象除了 text,还包含:
sourcetitlefile_formatmime_typeparser_nameparser_versionparse_statusparse_errorsource_hashnormalized_hash
这让“解析失败”和“不支持格式”变成了显式状态,避免错误内容被静默写入。
在 rag/_ingestion/results.py 里,入库结果也有明确分类:unsupported、failed、skipped、success。这对知识库管理很有用。暂时只支持 TXT/Markdown 可以接受,关键是系统要把支持范围说清楚,避免把乱码或空内容写进知识库。
SourceDocument 解决文档级身份
解析完成以后,rag/_metadata/builder.py 的 build_source_document() 会把 ParsedDocument 转成 SourceDocument。
SourceDocument 是文档级身份。它回答的是:这篇文档是谁,从哪里来,属于哪个产品和版本,能被谁看到,内容 hash 是什么。
rag/_schemas/models.py 里对应字段很直观:
doc_id
source
title
text
file_format
mime_type
parser_name
parser_version
parse_status
doc_type
product
version
access_scope
content_hash
source_hash
normalized_hash
created_at
updated_at
这些字段会直接影响后续链路。
doc_id 让文档可以被管理。
source 和 title 让引用能回到来源。
doc_type 和 product 支持检索过滤。
version 和 access_scope 支持生产级知识库治理。
content_hash 和 normalized_hash 支持去重和版本判断。
parse_status 和 parser_name 让解析过程可追踪。
这些信息在入库时建立好,后面再做检索、引用和治理就会轻很多。
KnowledgeChunk 解决证据级身份
文档级身份之后,还要建立证据级身份。RAG 真正进入检索和回答时,使用的是 chunk,所以每个 chunk 也必须有自己的身份。
KnowledgeChunk 的字段比普通 chunk 丰富很多。除了 text,它还包含:
chunk_id
doc_id
source
chunk_index
title
title_path
doc_type
product
version
access_scope
content_hash
normalized_hash
chunk_hash
chunk_strategy_version
block_type
block_types
heading_level
section_index
block_start_index
block_end_index
chunk_char_count
split_reason
这让 chunk 从“一段被切开的文字”升级成一个可检索、可过滤、可引用、可评测、可更新的证据单元。
举个例子,用户问“小米 15 Pro 电池容量是多少”,最终答案引用了 [E1]。当 [E1] 带着 doc_id、chunk_id、source、title_path、product、version 和 document_version_id,系统就能判断它是否来自正确文档、正确产品、当前版本,以及是否应该对当前用户可见。
metadata 的价值,就是在这里变得具体。
metadata_builder 把 chunk 变成可写入索引的证据
rag/_ingestion/metadata_builder.py 做的是一个很关键的转换:把 KnowledgeChunk 上的信息整理成向量库和本地 fallback 都能保存的 metadata。
其中 build_versioned_metadatas() 会先调用 build_chunk_metadata(chunk),再补上版本和索引相关字段:
{
"document_version_id": document_version_id,
"is_active": True,
"is_deleted": False,
"chunker_version": chunker_version,
"embedding_version": embedding_version,
}
这些字段为后面的动态知识库更新打基础。
is_active 和 is_deleted 会影响检索过滤。
document_version_id 让 chunk 能回到某个文档版本。
chunker_version 说明这批 chunk 是用哪套切分策略生成的。
embedding_version 说明向量来自哪个 embedding 模型或配置。
也就是说,metadata 既是展示字段,也是后续检索、回滚、重建索引和评测的控制面。
最终存储的 metadata 在哪里
到这里很容易混在一起,所以单独把最终存储说清楚。NexaRAG 入库后,持久化数据主要分成三类:chunk metadata、文档注册表、去重 hash。
第一类是每个 chunk 最终写入索引的 metadata。它由 rag/_metadata/builder.py 的 build_chunk_metadata() 生成,再由 rag/_ingestion/metadata_builder.py 的 build_versioned_metadatas() 补上版本和索引状态。最终字段包括:
doc_id
chunk_id
chunk_index
document_version_id
is_active
is_deleted
source
title
title_path
file_format
mime_type
parser_name
parser_version
parse_status
doc_type
product
version
access_scope
content_hash
source_hash
normalized_hash
chunk_hash
chunk_strategy_version
chunker_version
embedding_version
block_type
block_types
heading_level
section_index
block_start_index
block_end_index
chunk_char_count
split_reason
indexed_at
page_number # 有页码时才写入
sheet_name # 有表格 sheet 时才写入
table_index # 有表格编号时才写入
这里有几个细节。title_path 最终会存成 " > " 连接的字符串,block_types 会存成逗号连接的字符串;chunk.metadata 里额外带来的字段也会经过 _scalarize_metadata() 转成 Chroma 能接受的标量后合并进去。也就是说,最终写入索引的 metadata 尽量都是 string、number、boolean 这类可存储字段。
这些 chunk metadata 会存到两个地方:
Chroma 向量库
写入入口:KnowledgeIngestion._add_texts()
调用方式:self.store.add_texts(texts=chunks, metadatas=metadatas)
collection:config.collection_name,当前默认是 "rag"
persist_directory:config.persist_directory,当前默认是 ".chroma_db"
写入条件:CHROMA_WRITE_ENABLED=true,或测试/运行时显式传入 store
local fallback
写入入口:append_knowledge_chunks()
默认路径:data/knowledge_chunks.jsonl
单行结构:{"text": chunk.text, "metadata": metadata}
写入条件:始终写入,用于 BM25、本地调试、reindex 和动态知识库治理
第二类是文档级和版本级注册信息。它独立于每个 chunk 的 metadata,由 DocumentRegistry 单独保存成 JSON 文件。默认路径来自 rag/_registry/paths.py:
优先使用 settings.knowledge_registry_path
否则使用 settings.local_knowledge_chunks_path 同目录下的 document_registry.json
再否则使用 data/knowledge/document_registry.json
DocumentRegistry 里有三组数据:
documents
doc_id
title
source
doc_type
product
access_scope
created_at
updated_at
active_version_id
status
versions
version_id
doc_id
source
file_format
content_hash
normalized_text_hash
parser_name
parser_version
chunker_version
embedding_version
chunk_count
is_active
is_deleted
created_at
error
events
event_type
doc_id
version_id
created_at
detail
这三组 registry 数据负责回答“当前文档是谁、哪个版本 active、是否 deleted、发生过哪些版本事件”。它和 chunk metadata 是配套关系:chunk metadata 负责检索时过滤,registry 负责管理时看文档和版本状态。
第三类是去重 hash。source_doc.content_hash 会追加写入 config.md5_path,当前默认是 data/md5.text。这个文件只服务重复上传判断,不承载完整 metadata。
所以可以把最终存储关系压成一句话:
chunk text + chunk metadata -> Chroma / data/knowledge_chunks.jsonl
document/version lifecycle -> data/knowledge/document_registry.json
content_hash duplicate mark -> data/md5.text
去重和版本从入库开始
在 rag/_ingestion/pipeline.py 里,写入 chunk 前会先做重复判断:
find_duplicate_active(doc_id, content_hash, normalized_hash)
check_md5(content_hash)
has_active_duplicate_hash(...)
如果当前 active version 已经有相同内容,就返回 skipped。如果是新内容,则通过 document_registry.next_version_id() 生成新的版本 ID,切 chunk、写索引、注册 active version。
版本注册在 rag/_ingestion/versioning.py 和 rag/_registry/store.py 里完成。DocumentRegistry 会维护两类记录:
DocumentRecordDocumentVersionRecord
DocumentRecord 表示文档本身,比如 doc_id、title、source、doc_type、product、access_scope、active_version_id、status。
DocumentVersionRecord 表示某个版本,比如 version_id、content_hash、normalized_text_hash、parser_name、chunker_version、embedding_version、chunk_count、is_active、is_deleted。
这说明 NexaRAG 从入库阶段就把知识库视为长期变化的资源。文档会更新,会删除,会回滚,会重建索引。动态知识库能力从 metadata 和 registry 这里就已经埋下了接口。
这一步解决的内容和下一步
入库和 metadata 这一步解决的是证据身份问题。
它让文档有 doc_id 和版本,让 chunk 有稳定 chunk_id 和结构化 metadata,让解析状态可见,让重复上传可跳过,让 unsupported 有明确返回,让写库失败有 fallback。
下一步继续处理 chunk 质量本身。文档有身份,说明它进入系统时可追踪;至于切出来的 chunk 是否保留标题、段落、步骤和 QA 边界,还要看下一步的结构化 chunking。
这也是入库之后继续讲 chunk 的原因。证据先要有身份,接着才要有合适的粒度。
小结
文档入库是 RAG 证据链的起点。
NexaRAG 当前把上传入口放在 api/knowledge_router/upload.py,把入库组织放在 rag/_ingestion/service.py,把核心流程放在 rag/_ingestion/pipeline.py,把 metadata 构造放在 rag/_metadata/builder.py 和 rag/_ingestion/metadata_builder.py,把文档和版本状态交给 rag/_registry/*。这套拆分让“知识进入系统”从写入向量库扩展为建立文档身份、chunk 身份、版本身份和检索控制信息。
下一篇继续往下看:有了文档和 chunk 的身份以后,怎样把文本切成既适合召回、又保留语义边界的结构化 chunk。
参考材料:
- NexaRAG
api/knowledge_router/upload.py - NexaRAG
rag/_ingestion/service.py - NexaRAG
rag/_ingestion/pipeline.py - NexaRAG
rag/_ingestion/metadata_builder.py - NexaRAG
rag/_ingestion/versioning.py - NexaRAG
rag/_loaders/registry.py - NexaRAG
rag/_schemas/models.py - NexaRAG
rag/_metadata/builder.py - NexaRAG
rag/_registry/store.py - NexaRAG
rag/_registry/records.py - 原 RAG 系列
02-文档进入知识库之前-解析清洗结构化和metadata.md