Home Projects Blog Resume Contact 中文
Back to list
2026年7月5日 1,962 words 5 min read

02-文档入库与 metadata:知识进入系统之前先有身份

RAG 的很多问题,表面发生在检索阶段,根子常常在入库阶段。

#NexaRAG#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_typeproductversionaccess_scopemime_type 可以从上传表单传进来。用户未填写时,后面的 metadata 构建逻辑还会尝试根据 source 和 title 推断。

这个边界很关键。API 层只需要把“用户上传了一份知识”这件事转交给入库链路,DocumentRegistry 怎么存、chunk 怎么切,都由下游模块负责。

入库服务负责组织依赖

入库主入口在 rag/_ingestion/service.pyKnowledgeIngestion 负责组织几个关键依赖:

  • EmbeddingService
  • DocumentLoaderRegistry
  • DocumentRegistry
  • Chroma store
  • 文本 splitter
  • md5 去重文件

真正的入库业务流程交给 rag/_ingestion/pipeline.pyadd_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:

  • PlainTextLoader
  • MarkdownLoader
  • UnsupportedLoader

解析结果统一返回 ParsedDocument。这个对象除了 text,还包含:

  • source
  • title
  • file_format
  • mime_type
  • parser_name
  • parser_version
  • parse_status
  • parse_error
  • source_hash
  • normalized_hash

这让“解析失败”和“不支持格式”变成了显式状态,避免错误内容被静默写入。

rag/_ingestion/results.py 里,入库结果也有明确分类:unsupportedfailedskippedsuccess。这对知识库管理很有用。暂时只支持 TXT/Markdown 可以接受,关键是系统要把支持范围说清楚,避免把乱码或空内容写进知识库。

SourceDocument 解决文档级身份

解析完成以后,rag/_metadata/builder.pybuild_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 让文档可以被管理。
sourcetitle 让引用能回到来源。
doc_typeproduct 支持检索过滤。
versionaccess_scope 支持生产级知识库治理。
content_hashnormalized_hash 支持去重和版本判断。
parse_statusparser_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_idchunk_idsourcetitle_pathproductversiondocument_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_activeis_deleted 会影响检索过滤。
document_version_id 让 chunk 能回到某个文档版本。
chunker_version 说明这批 chunk 是用哪套切分策略生成的。
embedding_version 说明向量来自哪个 embedding 模型或配置。

也就是说,metadata 既是展示字段,也是后续检索、回滚、重建索引和评测的控制面。

最终存储的 metadata 在哪里

到这里很容易混在一起,所以单独把最终存储说清楚。NexaRAG 入库后,持久化数据主要分成三类:chunk metadata文档注册表去重 hash

第一类是每个 chunk 最终写入索引的 metadata。它由 rag/_metadata/builder.pybuild_chunk_metadata() 生成,再由 rag/_ingestion/metadata_builder.pybuild_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.pyrag/_registry/store.py 里完成。DocumentRegistry 会维护两类记录:

  • DocumentRecord
  • DocumentVersionRecord

DocumentRecord 表示文档本身,比如 doc_idtitlesourcedoc_typeproductaccess_scopeactive_version_idstatus

DocumentVersionRecord 表示某个版本,比如 version_idcontent_hashnormalized_text_hashparser_namechunker_versionembedding_versionchunk_countis_activeis_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.pyrag/_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