跳到主要内容
版本:1.0.7

Document ETL

Document ETL(提取-转换-加载)将外部文档(PDF、HTML、Markdown、JSON 等)读取、分割为适合向量化的文本块,最终写入向量数据库,构成 RAG 知识库构建的完整流水线。

DocumentReader(读取) → DocumentTransformer(分割) → DocumentWriter(写入 VectorStore)

1. 读取文档

Spring AI 内置多种 DocumentReader,覆盖常见文件格式。TextReaderJsonReader 位于 spring-ai-commons 模块,无需额外依赖。

1.1 纯文本

TextReader reader = new TextReader("classpath:/docs/readme.txt");
List<Document> documents = reader.get();

1.2 JSON

// 读取整个 JSON,每个元素一个 Document
JsonReader reader = new JsonReader(new ClassPathResource("data/products.json"));
List<Document> documents = reader.get();

// 仅提取指定 key 的内容
JsonReader keyReader = new JsonReader(
new ClassPathResource("data/faq.json"),
"question", "answer"
);
List<Document> documents = keyReader.get();

1.3 Markdown

按标题层级将 Markdown 文档拆分为多个 Document,保留标题、代码块等元数据。

pom.xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-markdown-document-reader</artifactId>
</dependency>
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(true)
.withIncludeCodeBlock(true)
.withAdditionalMetadata("source", "api-docs")
.build();

MarkdownDocumentReader reader = new MarkdownDocumentReader(
"classpath:/docs/spring-ai-guide.md", config);
List<Document> documents = reader.get();

配置项:

方法默认值说明
withHorizontalRuleCreateDocumentfalse遇到分隔线时拆分为新 Document
withIncludeCodeBlockfalse将代码块内容保留到文本中
withIncludeBlockquotefalse将引用块内容保留到文本中
withAdditionalMetadata为所有 Document 追加自定义元数据

1.4 HTML

通过 CSS 选择器提取 HTML 中指定元素的内容。

pom.xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-jsoup-document-reader</artifactId>
</dependency>
JsoupDocumentReaderConfig config = JsoupDocumentReaderConfig.builder()
.selector("article, .content")
.includeLinkUrls(true)
.metadataTag("author")
.additionalMetadata("type", "webpage")
.build();

JsoupDocumentReader reader = new JsoupDocumentReader(
"classpath:/pages/spec.html", config);
List<Document> documents = reader.get();

配置项:

方法默认值说明
selector"body"CSS 选择器,指定提取目标元素
allElementsfalse设为 true 时提取 body 全部文本(覆盖 selector)
groupByElementfalse设为 true 时每个匹配元素独立拆分为一个 Document
separator"\n"元素间的连接分隔符
includeLinkUrlsfalse是否将页面中的链接 URL 收集到元数据
metadataTag提取指定 name 的 <meta> 标签值到元数据
metadataTags同上,批量设置多个 meta 标签名
additionalMetadata自定义追加元数据

1.5 PDF

支持按页或按段落两种模式。

pom.xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
PdfDocumentReaderConfig config = PdfDocumentReaderConfig.builder()
.pagesPerDocument(1)
.pageTopMargin(50)
.pageBottomMargin(50)
.build();

PagePdfDocumentReader reader = new PagePdfDocumentReader(
"classpath:/docs/manual.pdf", config);
List<Document> documents = reader.get();

配置项:

方法默认值说明
pagesPerDocument1每个 Document 包含的页数,0 表示全部页合并
pageTopMargin0页面顶部裁剪像素
pageBottomMargin0页面底部裁剪像素
reversedParagraphPositionfalse颠倒段落位置
pageExtractedTextFormatter配合 ExtractedTextFormatter 清洗提取文本

1.6 Apache Tika(通用格式)

Tika 可解析 PDF、Word、PPT、HTML 等数十种格式,适合格式不确定的场景。

pom.xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
// 基本用法
TikaDocumentReader reader = new TikaDocumentReader("classpath:/docs/report.pdf");

// 配合文本清洗
ExtractedTextFormatter formatter = ExtractedTextFormatter.builder()
.withNumberOfTopPagesToSkipBeforeDelete(1)
.withNumberOfTopTextLinesToDelete(5)
.withNumberOfBottomTextLinesToDelete(3)
.withLeftAlignment(true)
.build();
TikaDocumentReader reader = new TikaDocumentReader(
"classpath:/docs/report.pdf", formatter);
List<Document> documents = reader.get();

ExtractedTextFormatter 配置项:

方法默认值说明
withNumberOfTopPagesToSkipBeforeDelete0跳过开头的页数(如封面)
withNumberOfTopTextLinesToDelete0跳过每页开头行(如页眉)
withNumberOfBottomTextLinesToDelete0跳过每页末尾行(如页脚)
withLeftAlignmentfalse统一文本左对齐,去除行首空白
overrideLineSeparator系统换行符指定换行符

2. 文本分割

文档读取后通常过长,需要分割为适合向量化的短文本块。TokenTextSplitter 基于 token 数量按语义边界智能分割。

TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(800)
.withMinChunkSizeChars(350)
.withMinChunkLengthToEmbed(5)
.withMaxNumChunks(10000)
.withKeepSeparator(true)
.build();

List<Document> chunks = splitter.apply(documents);

配置项:

方法默认值说明
withChunkSize800每块最大 token 数
withMinChunkSizeChars350最小字符数,低于此值的碎片丢弃
withMinChunkLengthToEmbed5最短可嵌入长度(字符)
withMaxNumChunks10000最大输出块数
withKeepSeparatortrue是否在截断处保留分隔符

分割算法:按 token 编码 → 取 chunkSize 个 token → 回溯到最近的句号/问号/感叹号/换行处截断 → 保证语义完整性。


3. ETL 组合使用

Reader、Transformer、Writer 可直接串联,一条链完成从文件到向量库的全流程:

// 1. 读取 PDF
DocumentReader reader = new PagePdfDocumentReader("classpath:/knowledge/programming-guide.pdf",
PdfDocumentReaderConfig.builder().pagesPerDocument(1).build());

// 2. 分割为文本块
DocumentTransformer splitter = new TokenTextSplitter();

// 3. 写入向量库
VectorStore vectorStore = new SimpleVectorStore(embeddingModel);
vectorStore.accept(splitter.apply(reader.get()));

// 4. 检索验证
List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder().query("REST API 设计规范").topK(3).build());

4. 完整示例

完整示例:DocumentEtlExample.java
public class DocumentEtlExample {

public static void main(String[] args) {
// 1. 创建 EmbeddingModel
OllamaApi ollamaApi = OllamaApi.builder()
.baseUrl("http://localhost:11434")
.build();
OllamaEmbeddingModel embeddingModel = OllamaEmbeddingModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(OllamaOptions.builder().model("mxbai-embed-large").build())
.build();

// 2. 创建 VectorStore
SimpleVectorStore vectorStore = new SimpleVectorStore(embeddingModel);

// 3. ETL 流水线:读取 Markdown → 分割 → 写入
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(true)
.withIncludeCodeBlock(true)
.build();
MarkdownDocumentReader reader = new MarkdownDocumentReader(
"classpath:/docs/api-reference.md", config);

TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(800)
.withMinChunkSizeChars(200)
.build();

List<Document> chunks = splitter.apply(reader.get());
vectorStore.accept(chunks);
System.out.println("知识库写入完成,共 " + chunks.size() + " 个文本块");

// 4. 检索
List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder().query("配置项说明").topK(3).build());
for (Document doc : results) {
System.out.println("---");
System.out.println("来源: " + doc.getMetadata().get("source"));
System.out.println("内容: " + doc.getText().substring(0,
Math.min(100, doc.getText().length())) + "...");
}
}
}