Document ETL
Document ETL(提取-转换-加载)将外部文档(PDF、HTML、Markdown、JSON 等)读取、分割为适合向量化的文本块,最终写入向量数据库,构成 RAG 知识库构建的完整流水线。
DocumentReader(读取) → DocumentTransformer(分割) → DocumentWriter(写入 VectorStore)
1. 读取文档
Spring AI 内置多种 DocumentReader,覆盖常见文件格式。TextReader 和 JsonReader 位于 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();
配置项:
| 方法 | 默认值 | 说明 |
|---|---|---|
withHorizontalRuleCreateDocument | false | 遇到分隔线时拆分为新 Document |
withIncludeCodeBlock | false | 将代码块内容保留到文本中 |
withIncludeBlockquote | false | 将引用块内容保留到文本中 |
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 选择器,指定提取目标元素 |
allElements | false | 设为 true 时提取 body 全部文本(覆盖 selector) |
groupByElement | false | 设为 true 时每个匹配元素独立拆分为一个 Document |
separator | "\n" | 元素间的连接分隔符 |
includeLinkUrls | false | 是否将页面中的链接 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();
PdfDocumentReaderConfig config = PdfDocumentReaderConfig.builder()
.pagesPerDocument(1)
.build();
ParagraphPdfDocumentReader reader = new ParagraphPdfDocumentReader(
"classpath:/docs/manual.pdf", config);
List<Document> documents = reader.get();
配置项:
| 方法 | 默认值 | 说明 |
|---|---|---|
pagesPerDocument | 1 | 每个 Document 包含的页数,0 表示全部页合并 |
pageTopMargin | 0 | 页面顶部裁剪像素 |
pageBottomMargin | 0 | 页面底部裁剪像素 |
reversedParagraphPosition | false | 颠倒段落位置 |
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 配置项:
| 方法 | 默认值 | 说明 |
|---|---|---|
withNumberOfTopPagesToSkipBeforeDelete | 0 | 跳过开头的页数(如封面) |
withNumberOfTopTextLinesToDelete | 0 | 跳过每页开头行(如页眉) |
withNumberOfBottomTextLinesToDelete | 0 | 跳过每页末尾行(如页脚) |
withLeftAlignment | false | 统一文本左对齐,去除行首空白 |
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);
配置项:
| 方法 | 默认值 | 说明 |
|---|---|---|
withChunkSize | 800 | 每块最大 token 数 |
withMinChunkSizeChars | 350 | 最小字符数,低于此值的碎片丢弃 |
withMinChunkLengthToEmbed | 5 | 最短可嵌入长度(字符) |
withMaxNumChunks | 10000 | 最大输出块数 |
withKeepSeparator | true | 是否在截断处保留分隔符 |
分割算法:按 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())) + "...");
}
}
}