大家好,我是冰点,今天我们继续聊SpringAI的基本用法和特性
建议先阅读第五篇《RAG 核心原理》,了解文档 ETL 在 RAG 流程中的定位。
第二阶段·RAG 检索增强
文章目录
- 【Spring AI 实战】六、文档 ETL 实战:PDF/Word/Markdown 解析与文本分割
- 一、文档 ETL 在 RAG 中的角色
- 二、Spring AI 文档读取体系
- 三、PDF 解析:PdfReader 详解
- 3.1 依赖
- 3.2 基本用法
- 3.3 带分页和元数据的读取
- 3.4 提取元数据
- 四、Word 文档解析:WordDocumentReader
- 4.1 依赖
- 4.2 基本用法
- 4.3 读取表格内容
- 五、Markdown 解析:MarkdownReader
- 六、HTML 网页解析
- 七、JSON 文档解析
- 八、批量文档读取工厂
- 九、文本分割:TextSplitter 核心原理
- 9.1 为什么不能直接喂整篇文档?
- 9.2 Spring AI 内置分割器
- 9.3 语义感知分割:RecursiveCharacterTextSplitter
- 9.4 Markdown 语义分割:保留标题层级
- 十、自定义分割器:SentenceTransformers 语义分割
- 十一、分割策略实战参数建议
- 十二、完整 ETL 流程示例
- 十三、本章小结

一个完整的 RAG 问答系统,数据处理流程如下:
原始文档(PDF/Word/HTML/TXT)
↓
读取(Read) ← ETL 的 E(Extract)
↓
解析(Parse) ← ETL 的 T(Transform)
↓
分割(Split) ← ETL 的 L(Load 前置步骤)
↓
向量化(Embed) → 存入 VectorStore
ETL = Extract(抽取) + Transform(转换) + Load(加载),其中”Load”在 RAG 语境下就是向量化存储。本篇文章重点覆盖前三步。

Spring AI 提供了一套统一的 DocumentReader 接口,所有文档源都实现这个接口:
// 核心接口 public interface DocumentReader {
List
read();
}
// 常用实现 // - PdfReader PDF 文件 // - WordDocumentReader Word/ DOCX 文件 // - MarkdownReader Markdown 文件 // - TxtReader 纯文本文件 // - HtmlDocumentReader HTML 网页 // - JsonReader JSON 文件 // - ApachePoiDocumentReader Apache POI 读取 Word/Excel
3.1 依赖
org.springframework.ai
spring-ai-readers-spring-boot-starter
org.apache.pdfbox
pdfbox
3.0.1
3.2 基本用法
@Bean public DocumentReader pdfReader() {
return new org.springframework.ai.reader.pdf.PdfReader( "classpath:/docs/product-manual.pdf" );
}
// 读取并打印 List
3.3 带分页和元数据的读取
@Bean public DocumentReader pdfReaderWithMetadata() of {total}“, “”)
) .build(); return new org.springframework.ai.reader.pdf.PdfReader( PathUtils.getResourceURL("classpath:/docs/product-manual.pdf"), config );
}
3.4 提取元数据
PDF 的元数据(页码、章节标题、文件路径)会自动注入到 Document 的 metadata 中:
List
docs = pdfReader().read();
Document firstPage = docs.get(0); // Spring AI 会自动填充以下元数据 firstPage.getMetadata().forEach((k, v) -> System.out.println(k + “: ” + v)); // 输出示例: // source: classpath:/docs/product-manual.pdf // page-number: 1 // total-pages: 42
4.1 依赖
org.apache.poi
poi-ooxml
5.2.5
4.2 基本用法
@Bean public DocumentReader wordReader()
4.3 读取表格内容
Word 中的表格默认不自动提取,需要配合自定义处理器:
public class TableAwareWordReader extends WordDocumentReader {
public TableAwareWordReader(Resource resource) { super(resource, WordDocumentReaderConfig.builder().build()); } @Override public List
read() { List
docs = super.read(); // 手动解析 Word 中的表格并追加到文本 // Apache POI 解析 XWPFTable 逻辑... return docs; }
}
Markdown 是最干净的格式,解析时能保留结构化语义。
@Bean public DocumentReader markdownReader()
Markdown 解析的额外优势:
- 标题层级自动映射到 Document metadata 的
headings字段 - 代码块自动识别并标记语言类型
- 支持 YAML Front Matter 元数据提取
@Bean public DocumentReader webPageReader()
适合处理结构化数据源,如日志、产品目录:
@Bean public DocumentReader jsonReader()
生产环境中通常需要一次性读取整个目录的文档:
@Component public class DocumentLoaderFactory {
@Value("${spring.ai.rag.document.path}") private String documentPath; public List
loadAllDocuments() private void loadDirectory(String path, String glob, Supplier
readerFactory) }
}
文档解析完成后,需要将长文本切分为适合检索的”块(Chunk)”。这是整个 ETL 流程中最关键的一步,切分策略直接影响检索质量。
9.1 为什么不能直接喂整篇文档?
9.2 Spring AI 内置分割器
// 按字符数分割(最简单) TextSplitter characterSplitter = new CharacterTextSplitter(
500, // 每块目标字符数 50, // 块间重叠字符数(避免割裂语义) true, // 是否保留分隔符 " " // 分隔符
);
// 按 Token 数分割(更精确,基于 LLM tokenizer) TextSplitter tokenSplitter = new TokenTextSplitter(
300, // 每块目标 Token 数 50, // 重叠 Token 数 true, // 是否保留分隔符 true // 是否追加块序号
);
9.3 语义感知分割:RecursiveCharacterTextSplitter
这是最推荐的生产级分割器,它按优先级尝试多种分隔符,逐层拆分:
尝试按 “
” (三级标题)分割
↓ 如果块太大
尝试按 “
” (段落)分割
↓ 如果块太大
尝试按 “ ” (换行)分割
↓ 如果块太大
尝试按 “ ” (句子)分割
↓ 如果块太大
按字符数硬截断(最后手段)
@Bean public DocumentReader semanticSplitterReader() {
// 推荐配置:重叠 20%,保留段落边界 return new RecursiveCharacterTextSplitterReader( pdfReader(), RecursiveCharacterTextSplitterReaderConfig.builder() .chunkSize(500) // 每块目标字符数 .chunkOverlap(100) // 重叠 20% .separators(List.of("
”, “ “, “。”, “!”, “?”, “ “))
.keepSeparator(true) .build() );
}
9.4 Markdown 语义分割:保留标题层级
对于 Markdown 文档,用 MarkdownHeaderTextSplitter 可以在标题处自然断点:
List
docs = markdownReader().read();
MarkdownHeaderTextSplitter splitter = new MarkdownHeaderTextSplitter(
List.of( // 按 二级标题分割 new HeaderMetadata("#", 1), new HeaderMetadata("", 2), new HeaderMetadata("", 3) ), 300, // fallback chunk size 50 // overlap
);
List
当对语义完整性要求极高时,可以用 Embedding 模型做语义感知分割——只在新主题出现时才断点:
@Component public class SemanticChunker
// 计算当前句子与 Chunk 最后一句的语义相似度 double similarity = computeSimilarity( currentChunk.get(currentChunk.size() - 1), sentence ); if (similarity < similarityThreshold) { // 低于阈值,创建新 Chunk chunks.add(new ArrayList<>(currentChunk)); currentChunk.clear(); } currentChunk.add(sentence); } if (!currentChunk.isEmpty()) { chunks.add(currentChunk); } return chunks.stream() .map(c -> new Document(String.join("", c))) .collect(Collectors.toList()); } private double computeSimilarity(String s1, String s2) { Embedding e1 = embeddingModel.embed(s1); Embedding e2 = embeddingModel.embed(s2); return cosineSimilarity(e1, e2); } private double cosineSimilarity(Embedding a, Embedding b) return dot / (Math.sqrt(normA) * Math.sqrt(normB)); }
}
@Service public class DocumentETLService
// Step 3: Load - 存入向量数据库 vectorStore.add(chunks); System.out.println("ETL 完成:处理文档 " + allDocs.size() + " 篇,生成 Chunk " + chunks.size() + " 个"); }
}
下一篇预告:《七、Embedding 向量化与向量数据库选型对比》—— 深度对比 Milvus、Pinecone、Redis、Chroma、Elasticsearch 的适用场景与 Spring AI 集成方式。
📌 系列导航
- ← 上一篇:【Spring AI 实战】五、RAG 核心原理
- → 下一篇:【Spring AI 实战】七、Embedding 向量化与向量数据库选型对比
- → 完整目录
📎 示例说明:本文侧重 ETL 管道与分割策略,若你准备做完整知识库系统,建议继续阅读第七、八篇。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/281087.html