向量数据库 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依赖
xml
<!-- 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
,使用中文提示词模板生成高质量摘要。
java
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;
}
@Override
public 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;
}
}
五、配置嵌入模型和向量数据库
yaml
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.201
port: 19530
username: root
password: milvus
database-name: default
initialize-schema: true # 启动时自动创建集合
collection-name: vector_store
embedding-dimension: 1024 # 必须与 embedding model 一致
六、从文档到向量的完整ETL流程
嵌入模型对文本长度有限制,如OpenAI的嵌入模型,要求Token数量不能超过8192,对超限的文本进行向量化会抛出异常。这就要求在进行向量化之前,我们应该先将Document拆分成符合要求的多个Document。
使用Tika对文档进行抽取的话,所有内容都将抽取到同一个Document,这可能会超出嵌入模型的限制,需要对其进行拆分。
TokenTextSplitter接收5个参数:
- chunkSize:令牌中每个文本块的目标大小。默认800
- minChunkSizeChars:每个文本块的最小字符大小。默认350
- minChunkLengthToEmbed:丢弃短于此的块。默认5
- maxNumChunks:从文本生成的最大块数。默认10000
- keepSeparator:是否保留分隔符。默认true
java
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 {
@Autowired
private ChatModel chatModel;
@Autowired
private 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: 生成向量并存入 Milvus
vectorStore.add(enrichedDocs);
return "文档已成功加载并存储到 Milvus!共处理 " + enrichedDocs.size() + " 个文本块。";
} catch (Exception e) {
return "ETL 失败: " + e.getMessage();
}
}
}
这样基本构建了一个完整的文档 ETL 管道,通过摘要和关键词增强,提升 RAG 检索的准确性与可解释性;自定义中文提示词,解决 Spring AI 对中文支持不足的问题;集成 Milvus 向量数据库支持海量向量的毫秒级检索;可轻松替换为其他嵌入模型(如 BGE、M3E)或向量数据库。为构建 RAG 提供了坚实的数据基础,而高质量的数据,是高质量 AI 的前提。