Spring AI 文档ETL实战:集成text-embedding-v4 与 Milvus
向量数据库Milvus
嵌入模型text-embedding-v4
一、Spring AI ETL 的核心组成
Spring AI 提供了一套清晰且可扩展的 API 来实现 ETL(Extract, Transform, Load) 数据处理流程,这是构建 RAG 系统中最关键的一环。整个流程可以分为三个核心阶段:
1. 抽取(Extract)
通过 DocumentReader
接口,Spring AI 支持从多种来源读取非结构化文本数据:
MarkdownDocumentReader
:读取.md
文件TikaDocumentReader
:支持 DOCX、PDF、HTML 等数十种格式PdfDocumentReader
:专为 PDF 优化- 自定义 Reader:可扩展支持数据库、网页、API 等
所有读取的内容都会被封装为 Document
对象,包含 pageContent
(正文)和 metadata
(元数据),作为后续处理的数据载体。
2. 转换(Transform)
这是提升数据质量的核心环节,通过 DocumentTransformer
接口对文档列表进行一系列处理:
- 文本分块(Splitting):使用
TokenTextSplitter
按 token 数量切分长文本,避免超出模型上下文限制。 - 元数据增强(Enrichment):调用大模型为每一块文本生成摘要、关键词、实体、分类等辅助信息,极大提升检索的语义理解能力。
- 清洗与标准化:去除噪声、统一编码、格式化日期等。
3. 加载(Load)
通过 DocumentWriter
或直接调用 VectorStore
接口,将处理后的文档写入目标存储系统:
- 向量数据库
- 搜索引擎
二、生成摘要和关键词
在传统的向量检索中,系统仅依赖原始文本的向量表示进行匹配。虽然语义相似性较高,但存在两个显著问题:
- 当系统召回某段文本时,开发者或用户无法快速理解“为什么这段被召回?” 只能看到一长串原文,难以判断其相关性。
- 分块后的文本可能只包含局部信息,丢失了前后文的逻辑关系,导致生成的回答断章取义。
通过大模型为每个文本块生成 摘要(Summary) 和 关键词(Keywords),可以有效解决上述问题:
- 摘要的作用是提供文本核心内容的浓缩表达,便于快速预览和理解,增强检索结果的可读性与可解释性。
- 关键词的作用是提取核心实体与主题,可用于标量过滤(scalar filtering),实现“向量 + 关键词”的混合检索,提高精准度。
通过保留相邻块的摘要信息,帮助模型理解上下文逻辑,避免“信息孤岛”。例如,用户提问:“如何配置 Spring Boot 的多数据源?”,如果只依赖向量匹配,可能召回一段关于“数据库连接池优化”的文本(语义相近但不精准)。如果该文本块的元数据中包含关键词 ["多数据源", "DataSource", "Spring Boot"]
和摘要 “本文介绍 Spring Boot 中配置多个数据源的方法……”,系统就能更准确地判断其相关性,显著提升回答质量。
三、Maven依赖
<!-- Markdown 文档读取器 --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-markdown-document-reader</artifactId><version>${spring.ai.version}</version></dependency><!-- Tika 文档读取器(支持 DOCX, PDF 等) --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-tika-document-reader</artifactId><version>${spring.ai.version}</version></dependency><!-- 向量存储顾问 --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-advisors-vector-store</artifactId><version>${spring.ai.version}</version></dependency><!-- Milvus 向量存储支持 --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-vector-store-milvus</artifactId><version>${spring.ai.version}</version></dependency>
四、中文摘要生成器
Spring AI 内置的 SummaryMetadataEnricher
使用英文提示词,生成的摘要对中文用户不友好。为此,我们自定义 ChineseSummaryMetadataEnricher
,使用中文提示词模板生成高质量摘要。
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentTransformer;
import org.springframework.ai.document.MetadataMode;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;public class ChineseSummaryMetadataEnricher implements DocumentTransformer {private static final String SECTION_SUMMARY_METADATA_KEY = "section_summary";private static final String NEXT_SECTION_SUMMARY_METADATA_KEY = "next_section_summary";private static final String PREV_SECTION_SUMMARY_METADATA_KEY = "prev_section_summary";private static final String CONTEXT_STR_PLACEHOLDER = "context_str";// 中文提示词模板public static final String CHINESE_SUMMARY_TEMPLATE = """请对以下中文文本进行专业摘要,要求:1. 使用简洁明了的中文2. 总结主要内容,不超过100字3. 保持专业性和准确性文本内容:{context_str}中文摘要:""";private final ChatModel chatModel;private final List<SummaryType> summaryTypes;private final MetadataMode metadataMode;private final String summaryTemplate;public enum SummaryType {CURRENT, PREVIOUS, NEXT}public ChineseSummaryMetadataEnricher(ChatModel chatModel, List<SummaryType> summaryTypes) {this(chatModel, summaryTypes, CHINESE_SUMMARY_TEMPLATE, MetadataMode.ALL);}public ChineseSummaryMetadataEnricher(ChatModel chatModel, List<SummaryType> summaryTypes,String summaryTemplate, MetadataMode metadataMode) {Assert.notNull(chatModel, "ChatModel must not be null");Assert.hasText(summaryTemplate, "Summary template must not be empty");this.chatModel = chatModel;this.summaryTypes = CollectionUtils.isEmpty(summaryTypes) ? List.of(SummaryType.CURRENT) : summaryTypes;this.metadataMode = metadataMode;this.summaryTemplate = summaryTemplate;}@Overridepublic List<Document> apply(List<Document> documents) {List<String> documentSummaries = new ArrayList<>();for (Document document : documents) {var documentContext = document.getFormattedContent(this.metadataMode);Prompt prompt = new PromptTemplate(this.summaryTemplate).create(Map.of(CONTEXT_STR_PLACEHOLDER, documentContext));String summary = chatModel.call(prompt).getResult().getOutput().getText();documentSummaries.add(summary.trim());}// 将摘要写入元数据for (int i = 0; i < documentSummaries.size(); i++) {Map<String, Object> summaryMetadata = getSummaryMetadata(i, documentSummaries);documents.get(i).getMetadata().putAll(summaryMetadata);}return documents;}private Map<String, Object> getSummaryMetadata(int i, List<String> summaries) {Map<String, Object> metadata = new HashMap<>();if (i > 0 && summaryTypes.contains(SummaryType.PREVIOUS)) {metadata.put(PREV_SECTION_SUMMARY_METADATA_KEY, summaries.get(i - 1));}if (i < summaries.size() - 1 && summaryTypes.contains(SummaryType.NEXT)) {metadata.put(NEXT_SECTION_SUMMARY_METADATA_KEY, summaries.get(i + 1));}if (summaryTypes.contains(SummaryType.CURRENT)) {metadata.put(SECTION_SUMMARY_METADATA_KEY, summaries.get(i));}return metadata;}
}
五、配置嵌入模型和向量数据库
spring:ai:# 通义千问 DashScope 配置dashscope:api-key: ${DASHSCOPE_API_KEY} # 建议通过环境变量注入embedding:options:model: text-embedding-v4 # 使用 v4 嵌入模型dimensions: 1024 # 向量维度text-type: document # 文本类型为文档# Milvus 向量数据库配置vectorstore:milvus:client:host: 192.168.0.201port: 19530username: rootpassword: milvusdatabase-name: defaultinitialize-schema: true # 启动时自动创建集合collection-name: vector_storeembedding-dimension: 1024 # 必须与 embedding model 一致
六、从文档到向量的完整ETL流程
嵌入模型对文本长度有限制,如OpenAI的嵌入模型,要求Token数量不能超过8192,对超限的文本进行向量化会抛出异常。这就要求在进行向量化之前,我们应该先将Document拆分成符合要求的多个Document。
使用Tika对文档进行抽取的话,所有内容都将抽取到同一个Document,这可能会超出嵌入模型的限制,需要对其进行拆分。
TokenTextSplitter接收5个参数:
- chunkSize:令牌中每个文本块的目标大小。默认800
- minChunkSizeChars:每个文本块的最小字符大小。默认350
- minChunkLengthToEmbed:丢弃短于此的块。默认5
- maxNumChunks:从文本生成的最大块数。默认10000
- keepSeparator:是否保留分隔符。默认true
import com.example.blog.etl.ChineseSummaryMetadataEnricher;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.document.Document;
import org.springframework.ai.model.transformer.KeywordMetadataEnricher;
import org.springframework.ai.reader.markdown.MarkdownDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;@RestController
@RequestMapping("/etl")
public class ETLController {@Autowiredprivate ChatModel chatModel;@Autowiredprivate VectorStore vectorStore;@GetMapping("/load")public String loadDocuments() {try {// Step 1: 读取文档并分块MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:agent.md");List<Document> documents = reader.get();// 使用 Token 分割器(适合长文本)TokenTextSplitter splitter = new TokenTextSplitter(2000, 1024, 10, 10000, true);List<Document> chunks = splitter.transform(documents);// Step 2: 元数据增强 —— 关键词 + 中文摘要List<Document> enrichedDocs = new KeywordMetadataEnricher(chatModel, 5).andThen(new ChineseSummaryMetadataEnricher(chatModel,List.of(ChineseSummaryMetadataEnricher.SummaryType.CURRENT,ChineseSummaryMetadataEnricher.SummaryType.NEXT))).apply(chunks);// Step 3: 生成向量并存入 MilvusvectorStore.add(enrichedDocs);return "文档已成功加载并存储到 Milvus!共处理 " + enrichedDocs.size() + " 个文本块。";} catch (Exception e) {return "ETL 失败: " + e.getMessage();}}
}
这样基本构建了一个完整的文档 ETL 管道,通过摘要和关键词增强,提升 RAG 检索的准确性与可解释性;自定义中文提示词,解决 Spring AI 对中文支持不足的问题;集成 Milvus 向量数据库支持海量向量的毫秒级检索;可轻松替换为其他嵌入模型(如 BGE、M3E)或向量数据库。为构建 RAG 提供了坚实的数据基础,而高质量的数据,是高质量 AI 的前提。