一、技术栈选型
依赖配置 (pom.xml):
xml
<!-- Spring AI Milvus Vector Store -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-milvus</artifactId>
<version>${spring-ai.version}</version> <!-- 1.1.8 -->
</dependency>
<!-- Spring AI Tika Document Reader (文档解析) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- Spring AI Vector Store Advisors -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
<version>${spring-ai.version}</version>
</dependency>
二、Milvus 配置详解
配置文件 : application.yml
yaml
spring:
ai:
# 向量数据库配置
vectorstore:
milvus:
client:
host: "localhost" # Milvus 服务地址
port: 19530 # Milvus 端口
username: "" # 用户名(空表示无需认证)
password: "" # 密码
databaseName: "default" # 数据库名称
collectionName: "vector_store" # 集合名称
embeddingDimension: 1024 # ⚠️ 关键:向量维度
indexType: IVF_FLAT # 索引类型
metricType: COSINE # 相似度度量:余弦相似度
initializeSchema: true # 自动创建集合
⚠️ 重要配置说明:
-
向量维度配置:
- 当前配置为
1024维,对应 DashScope 的text-embedding-v2模型 - 如果使用
text-embedding-v1需改为1536维 - 修改维度后必须删除旧集合,让 Spring AI 重新创建
- 当前配置为
-
索引类型 :
IVF_FLAT适合中小规模数据,平衡查询性能和精度 -
度量方式 :
COSINE余弦相似度,适合文本语义检索
三、核心组件架构
1. Bean 配置 (AiBaseConfig.java)
java
@Configuration
public class AiBaseConfig {
/**
* 文本分割器 Bean
* 将长文档切分为多个段落
*/
@Bean
public TokenTextSplitter tokenTextSplitter() {
return new TokenTextSplitter();
}
}
2. VectorStore 注入
Spring AI 的 @EnableAutoConfiguration 会自动根据 application.yml 配置创建 VectorStore Bean,可直接注入使用:
java
@Resource
private VectorStore vectorStore;
四、知识库业务流程
文件上传与向量化流程 (KnowledgeFileServiceImpl.java)
用户上传文件 → OSS存储 → 文档解析 → 添加元数据 → 文本分词 → 向量化入库 → 数据库记录
java
@Override
public Result<String> upload(List<MultipartFile> files) {
if (files == null || files.isEmpty()) {
return Result.error(ErrorCodeEnum.PARAMS_ERROR.getCode(), "请上传文件");
}
StorageService storageService;
if (storageType != null && !storageType.isEmpty()) {
storageService = storageServiceFactory.getStorageService(storageType);
} else {
storageService = storageServiceFactory.getStorageService();
}
for (MultipartFile file : files) {
try {
// 原文件名
String originalFilename = file.getOriginalFilename();
if (!StringUtils.hasText(originalFilename)) {
log.warn("文件名为空,跳过该文件");
continue;
}
// 文件后缀
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
// 随机文件名(OSS)
String objectName = UUID.randomUUID() + extension;
Map<String, Object> uploadResult = storageService.upload(file, "knowledge").getData();
String fileUrl = (String) uploadResult.get("fileUrl");
// 向量化
// 1. 读取文档 txt pdf docx doc
TikaDocumentReader reader = new TikaDocumentReader(file.getResource());
List<Document> documents = reader.read();
if (documents.isEmpty()) {
log.warn("文件 {} 解析后无内容,跳过", originalFilename);
continue;
}
// 为每个文档添加元数据(来源文件名)
documents.forEach(document -> {
document.getMetadata().put("source", originalFilename);
document.getMetadata().put("fileUrl", fileUrl);
});
//documents.forEach(document -> {document.getMetadata().put("source",originalFilename)});
// 2. 分词
List<Document> splitDocuments = tokenTextSplitter.apply(documents);
// 3. 向量化并保存到向量库 自动调用向量模型向量化方法
vectorStore.add(splitDocuments);
List<String> vectorIds = splitDocuments.stream()
.map(Document::getId)
.collect(Collectors.toList());
// 持久化到数据库
KnowledgeFile knowledgeFile = new KnowledgeFile();
knowledgeFile.setFileName(originalFilename);
knowledgeFile.setUrl(fileUrl);
knowledgeFile.setVectorId(JSONUtil.toJsonStr(vectorIds));
knowledgeFile.setCreateTime(new Date());
knowledgeFile.setUpdateTime(new Date());
this.save(knowledgeFile);
log.info("文件上传成功: {}, URL: {}", originalFilename, fileUrl);
} catch (IOException e) {
log.error("上传文件失败: {}", file.getOriginalFilename(), e);
return Result.error("上传文件失败!");
} catch (Exception e) {
log.error("向量化失败: {}", file.getOriginalFilename(), e);
return Result.error("向量化失败!");
}
}
return Result.success("文件上传成功");
}
详细步骤:
-
文件上传到对象存储
javaMap<String, Object> uploadResult = storageService.upload(file, "knowledge").getData(); String fileUrl = (String) uploadResult.get("fileUrl"); -
文档解析 (支持 PDF/Word/Excel/TXT)
javaTikaDocumentReader reader = new TikaDocumentReader(file.getResource()); List<Document> documents = reader.read(); -
添加元数据
javadocuments.forEach(document -> { document.getMetadata().put("source", originalFilename); // 来源文件名 document.getMetadata().put("fileUrl", fileUrl); // OSS链接 }); -
文本分词
javaList<Document> splitDocuments = tokenTextSplitter.apply(documents); -
向量化并存储到 Milvus
javavectorStore.add(splitDocuments); // 自动调用 DashScope API 生成向量 -
保存数据库记录
javaKnowledgeFile knowledgeFile = new KnowledgeFile(); knowledgeFile.setFileName(originalFilename); knowledgeFile.setUrl(fileUrl); knowledgeFile.setVectorId(JSONUtil.toJsonStr(vectorIds)); // 记录向量ID列表 this.save(knowledgeFile);
五、数据模型
KnowledgeFile 实体 (KnowledgeFile.java)
java
@TableName("ai_ali_oss_file")
public class KnowledgeFile {
private String id; // 主键ID
private String fileName; // 文件名
private String url; // OSS文件链接
private String vectorId; // JSON数组:该文件分割出的向量ID列表
private Date createTime; // 创建时间
private Date updateTime; // 更新时间
}
数据库表结构:
sql
CREATE TABLE `ai_ali_oss_file` (
`id` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,
`file_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文件名',
`url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '链接地址',
`vector_id` text COLLATE utf8mb4_unicode_ci COMMENT '该文件分割出的多段向量文本ID',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='阿里云OSS文件表';
六、API 接口
KnowledgeController 提供以下接口:
| 接口 | 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|---|
| 分页查询 | GET | /knowledge/page |
knowledge:query |
按文件名模糊搜索 |
| 文件上传 | POST | /knowledge/upload |
knowledge:upload |
批量上传并自动向量化 |
| 删除文件 | DELETE | /knowledge/delete |
knowledge:delete |
删除单条记录 |
| 批量删除 | DELETE | /knowledge/batch |
knowledge:batch:delete |
批量删除 |
| 查询详情 | GET | /knowledge/detail |
knowledge:detail |
获取单条记录 |
⚠️ 注意:
- 删除操作目前仅删除 MySQL 记录,未同步删除 Milvus 中的向量数据
- 建议补充向量清理逻辑
七、关键技术点
1. TikaDocumentReader 使用要点
java
// ✅ 正确用法:直接使用 MultipartFile.getResource()
TikaDocumentReader reader = new TikaDocumentReader(file.getResource());
// ❌ 错误用法:不要使用 MultipartFile.getInputStream()
2. TokenTextSplitter 默认参数
- 基于 Token 数量进行分词
- 默认配置适用于大多数场景
- 可根据需求自定义参数:
java
// 单分片最大 Token 上限 800
private static final int DEFAULT_CHUNK_SIZE = 800;
//分片最少需要 350 个字符,低于该长度会直接丢弃、不生成向量入库。
private static final int MIN_CHUNK_SIZE_CHARS = 350;
// 分片最少字符达到 5 才执行向量化,兜底过滤纯空行、符号碎片,这个参数无需改动,通用安全值。
private static final int MIN_CHUNK_LENGTH_TO_EMBED = 5;
// 单篇文档最大分片数量 1 万,日记月度文档最多几十条分片,完全够用,无需修改。
private static final int MAX_NUM_CHUNKS = 10000;
// 切割时保留分割符(换行 / 标点),能完整保留日期换行、段落结构,不会把日期行和正文粘连在一起,维持语义可读性,保持true不动。
private static final boolean KEEP_SEPARATOR = true;
// 分片器优先用来切割文本的分隔标点
private static final List<Character> DEFAULT_PUNCTUATION_MARKS = List.of('.', '?', '!', '\n');
3. 自动向量化机制
java
vectorStore.add(documents);
// Spring AI 内部自动调用:
// 1. DashScope Embedding API 生成向量
// 2. 插入 Milvus 集合
// 3. 返回生成的向量 ID
八、🔧 待完善功能:
-
删除向量数据
java@Override @Transactional public boolean removeById(Long id) { KnowledgeFile file = this.getById(id); if (file != null && StringUtils.hasText(file.getVectorId())) { List<String> vectorIds = JSONUtil.toList(file.getVectorId(), String.class); // TODO: 调用 vectorStore.delete(vectorIds) 删除 Milvus 中的数据 } return this.removeById(id); } -
向量检索接口 (RAG 问答)
java@GetMapping("/search") public Result<List<Document>> search(@RequestParam String query) { List<Document> results = vectorStore.similaritySearch(query); return Result.success(results); } -
向量维度校验
- 启动时检查 DashScope 模型与 Milvus 维度是否匹配
- 提供友好的错误提示
-
批量上传优化
- 增加异步处理避免超时
- 添加进度回调通知前端
-
元数据增强
- 记录上传用户ID
- 添加文档类型标签
- 支持自定义分类
九、环境依赖
必需服务:
-
Milvus 向量数据库
bash# Docker 快速启动 docker run -d --name milvus-standalone \ -p 19530:19530 \ -p 9091:9091 \ milvusdb/milvus:latest standalone -
DashScope API Key
yamlspring: ai: dashscope: api-key: ${ALI_AI_KEY} # 环境变量配置 -
对象存储服务 (阿里云 OSS / MinIO / 本地)
yamlfile: uploadType: alioss # local/minio/alioss