Asterism

数据加载:把异构来源变成可检索文档

内容摘要

数据加载是 RAG 管道的第一步,也是最容易被忽视的一步。本文介绍 Spring AI 的各类文档读取器(Text、PDF、HTML、Markdown、JSON、Tika)以及 Unstructured 文档处理库,涵盖元数据抽取、文档清洗与常见格式的处理方案。

一是提取文本,把 PDF、Word、Markdown 这些格式里的文字内容读出来,变成纯文本。

二是抽取元数据,文档来源、页码、作者、标题、创建时间——这些信息在后面溯源和过滤的时候非常有用。比如你不想让 AI 引用过期的制度文档,在检索层可以根据创建时间做过滤,但如果这一步元数据没抽到,后面想过滤都做不了。

三是整理成统一的数据结构,方便后面做切分、向量化和入库。

1.2 使用 SpringAI 文档读取

SpringAI 已经为我们提供了大量的读取器,拿来即用。这些读取器都来自 —— DocumentReader

在 Spring AI中,DocumentReader 是一个用于从各种格式的文档中提取文本内容并将其转换为 Document 对象的核心组件。这些 Document 对象随后可以被用于向量嵌入(embedding)、语义搜索、RAG(Retrieval-Augmented Generation)等 AI 应用场景,主要作用:

1.2.1 模块前置准备

添加依赖:

<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    <version>1.1.0.0</version>
</dependency>

配置文件:

spring:
  application:
    name: RAG
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY}
      embedding:
        options:
          model: text-embedding-v4
          dimensions: 768

1.2.2 文本读取器

读取器是 Spring AI 中最常用的一种读取器,用于从文本文件中读取内容。

public List<Document> read(File file) throws IOException {
    String fileName = file.getName().toLowerCase();
    Resource resource = new FileSystemResource(file);
    TextReader textReader = new TextReader(resource);
    return textReader.get();
}

执行结果:
rag-reader-text.pngrag-reader-text.png

由于初始文本格式错乱,导致读取结果中包含大量空行。PDF读取也会出现类似问题,后续统一文档清洗进行处理!

1.2.3 PDF 读取器

引入依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pdf-document-reader</artifactId>
    <version>1.1.0</version>
</dependency>

这个包里面提供了两个reader:ParagraphPdfDocumentReaderPagePdfDocumentReader

区别是,PagePdfDocumentReader 是”按页切分”,而 ParagraphPdfDocumentReader 是”按语义段落切分”。

在 rag 中,如果你需要实现 PDF 读取策略,通常建议:

建议优先考虑使用 ParagraphPdfDocumentReader,因为它能够更好地保留信息的完整性。段落通常是一个完整的意思表达,这对于 LLM 理解上下文非常有帮助。但是他非常依赖PDF 本身的质量。如果 PDF 是扫描件或者没有良好的内部结构标记,它的效果可能不理想甚至回退到按行读取。

代码执行

private List<Document> readPdf(Resource resource, File file) {
    // 创建 PDF 文档读取器配置,设置页面边距和每个 Document 包含的页数
    PdfDocumentReaderConfig config = PdfDocumentReaderConfig.builder()
            .withPageTopMargin(50)
            .withPageBottomMargin(50)
            .withPagesPerDocument(1)
            .withPageExtractedTextFormatter(
                    new ExtractedTextFormatter.Builder()
                            .withNumberOfTopTextLinesToDelete(0)
                            .build())
            .build();

    ParagraphPdfDocumentReader pdfReader =
            new ParagraphPdfDocumentReader(resource, config);
    return pdfReader.read();
}

报错:
rag-reader-pdf-error.pngrag-reader-pdf-error.png
这个错误是因为 ParagraphPdfDocumentReader 要求 PDF 文件必须有目录(TOC),但当前处理的 PDF 文件没有。根据错误提示,应该使用 PagePdfDocumentReader 或 TikaDocumentReader 来代替。

调整代码后,重新读取:

/**
 * 读取 PDF 文件。
 *
 * @param resource PDF 文件资源
 * @param file 本地 PDF 文件
 * @return PDF 中读取到的文档列表
 */
private List<Document> readPdf(Resource resource, File file) {
    PdfDocumentReaderConfig config = PdfDocumentReaderConfig.builder()
            .withPageTopMargin(50)
            .withPageBottomMargin(50)
            .withPagesPerDocument(1)
            .withPageExtractedTextFormatter(
                    new ExtractedTextFormatter.Builder()
                            .withNumberOfTopTextLinesToDelete(0)
                            .build())
            .build();

    try {
        // 优先使用段落读取器,它要求 PDF 具备目录结构
        ParagraphPdfDocumentReader pdfReader =
                new ParagraphPdfDocumentReader(resource, config);
        return pdfReader.read();
    } catch (IllegalArgumentException error) {
        // 没有目录结构时,降级为按页读取
        if (error.getMessage() != null
                && error.getMessage().contains("Document outline")) {
            log.warn(
                    "PDF 文件 [{}] 没有目录结构,使用 PagePdfDocumentReader 读取",
                    file.getName());
            PagePdfDocumentReader pagePdfReader =
                    new PagePdfDocumentReader(resource, config);
            return pagePdfReader.read();
        }
        throw error;
    }
}

rag-reader-pdf.pngrag-reader-pdf.png

1.2.4 HTML读取器

基于Jsoup HTML解析器,可以使用selector选择器指定提取网页标签内容。
引入依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-jsoup-document-reader</artifactId>
    <version>1.1.0</version>
</dependency>

代码逻辑:

/**
 * 根据网站地址读取 HTML 内容。
 */
public List<Document> readHtmlFromUrl(String url) throws IOException {
    log.info("开始从 URL 读取 HTML 内容: {}", url);
    Resource resource = new UrlResource(new URL(url));

    JsoupDocumentReaderConfig config = JsoupDocumentReaderConfig.builder()
            .selector("p")
            .charset("UTF-8")
            .includeLinkUrls(true)
            .metadataTags(List.of("author", "date", "title", "description"))
            .additionalMetadata("source_url", url)
            .build();

    List<Document> documents = new JsoupDocumentReader(resource, config).get();
    log.info("从 URL 读取完成,共获取 {} 个文档", documents.size());
    return documents;
}

执行结果:
rag-reader-html.pngrag-reader-html.png

1.2.5 Markdown读取器

引入依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-markdown-document-reader</artifactId>
    <version>1.1.0</version>
</dependency>

代码逻辑:

private List<Document> readMarkdown(Resource resource, File file) {
    MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
            .withHorizontalRuleCreateDocument(true)
            .withIncludeCodeBlock(false)
            .withIncludeBlockquote(false)
            .withAdditionalMetadata("filename", file.getName())
            .build();

    return new MarkdownDocumentReader(resource, config).get();
}

执行结果:
rag-reader-markdown.pngrag-reader-markdown.png

1.2.6 JSON读取器

引入依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-json-document-reader</artifactId>
    <version>1.1.0</version>
</dependency>

代码逻辑:

private List<Document> readJson(Resource resource, File file) {
    return new JsonDocumentReader(resource).get();
}

执行结果:
rag-reader-json.pngrag-reader-json.png

1.2.7 通用Tika读取器

一种通用文件处理器,可以处理大部分常见文档格式,如word、pdf、ppt等等,可以自动识别文档类型并提取文本和元数据。
引入依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-tika-document-reader</artifactId>
    <version>1.1.0</version>
</dependency>

代码逻辑:

private List<Document> readTika(Resource resource, File file) {
    return new TikaDocumentReader(resource).get();
}

执行结果:
rag-reader-tika-doc.pngrag-reader-tika-doc.png
rag-reader-tika-ppt.pngrag-reader-tika-ppt.png

1.3 文档清洗

在前面的步骤中,我们已成功实现文档读取功能。然而,读取结果往往包含大量无效干扰字符,因此需要进行数据清洗。数据清洗是对文本内容进行整理和优化的过程,包括去除多余空格、换行符、无意义的特殊符号和重复内容等。通过清洗,后续的文档分片和向量化操作可以在干净、一致的数据上进行,从源头提升知识库的质量与检索效果。为后续的文档分片、向量化以及检索生成等环节提供了更高质量的基础支撑。实际项目中可以根据自己的文档数据、业务需求,定制化的开发清洗策略。

2、Unstructured文档处理库

Unstructured 是一个专业的文档处理库,专门设计用于RAG和AI微调场景的非结构化数据预处理。提供了统一的接口来处理多种文档格式,是目前应用较广泛的文档加载解决方案之一。Unstructured 在格式支持和内容解析方面具有明显优势,它一方面支持 PDF、Word、Excel、HTML、Markdown 等多种文档格式,并通过统一的 API 接口避免为不同格式分别编写代码,另一方面可以自动识别标题、段落、表格、列表等文档结构,同时保留相应的元数据信息。

2.1 支持的文档元素类型

Unstructured 能够识别和分类以下文档元素 :

元素类型描述
Title文档标题
NarrativeText由多个完整句子组成的正文文本,不包括标题、页眉、页脚和说明文字
ListItem列表项,属于列表的正文文本元素
Table表格
Image图像元数据
Formula公式
Address物理地址
EmailAddress邮箱地址
FigureCaption图片标题/说明文字
Header文档页眉
Footer文档页脚
CodeSnippet代码片段
PageBreak页面分隔符
PageNumber页码
UncategorizedText未分类的自由文本
CompositeElement分块处理时产生的复合元素

CompositeElement 是通过分块处理产生的特殊元素类型,由一个或多个连续的文本元素组合而成。例如,多个列表项可能会被组合成一个单独的块。

partition 函数参数解析:

partition函数使用自动文件类型检测,内部会根据文件类型路由到对应的专用函数(如PDF文件会调用partition_pdf)。如果需要更专业的PDF处理,可以直接使用from unstructured.partition.pdf import partition_pdf,它提供更多PDF特有的参数选项,如OCR语言设置、图像提取、表格结构推理等高级功能,同时性能更优。

执行结果:
unstructured-partition-pdf.pngunstructured-partition-pdf.png

在实际应用中,针对 pdf 的处理,目前更多选用的是 PaddleOCR、MinerU 等模型或工具。后续文章会有 MinerU 文档解析示例

写在最后

数据加载是 RAG 管道的第一步,也是最容易被忽视的一步。

很多人在这一步犯的错不是”做得不对”,而是”做得不够”。以为把文档读进来就完事了,没有检查提取质量,没有处理格式噪声,没有抽取元数据——然后在后面的环节里花大量时间排查”为什么检索效果差”。

我的建议是:在这个环节多花点时间。

读几份真实的文档样本,看清楚里面有什么噪声;设计清洗策略,不要拍脑袋;元数据能抽多少抽多少,后续用得上;解析质量用人工抽检的方式验证,不要只看代码跑通了就当没问题。

这件事做扎实了,后面的优化才是有意义的。

下一节我们来聊 RAG 里另一个关键环节:文本分块——文档切成多大、怎么切、要不要有重叠,这些问题直接决定检索质量。