Spring AI整合Milvus向量数据库实战

一、技术栈选型

依赖配置 (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        # 自动创建集合

⚠️ 重要配置说明:

  1. 向量维度配置:

    • 当前配置为 1024 维,对应 DashScope 的 text-embedding-v2 模型
    • 如果使用 text-embedding-v1 需改为 1536
    • 修改维度后必须删除旧集合,让 Spring AI 重新创建
  2. 索引类型 : IVF_FLAT 适合中小规模数据,平衡查询性能和精度

  3. 度量方式 : 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("文件上传成功");
}

详细步骤:

  1. 文件上传到对象存储

    java 复制代码
    Map<String, Object> uploadResult = storageService.upload(file, "knowledge").getData();
    String fileUrl = (String) uploadResult.get("fileUrl");
  2. 文档解析 (支持 PDF/Word/Excel/TXT)

    java 复制代码
    TikaDocumentReader reader = new TikaDocumentReader(file.getResource());
    List<Document> documents = reader.read();
  3. 添加元数据

    java 复制代码
    documents.forEach(document -> {
        document.getMetadata().put("source", originalFilename);  // 来源文件名
        document.getMetadata().put("fileUrl", fileUrl);          // OSS链接
    });
  4. 文本分词

    java 复制代码
    List<Document> splitDocuments = tokenTextSplitter.apply(documents);
  5. 向量化并存储到 Milvus

    java 复制代码
    vectorStore.add(splitDocuments);  // 自动调用 DashScope API 生成向量
  6. 保存数据库记录

    java 复制代码
    KnowledgeFile 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

八、🔧 待完善功能:

  1. 删除向量数据

    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);
    }
  2. 向量检索接口 (RAG 问答)

    java 复制代码
    @GetMapping("/search")
    public Result<List<Document>> search(@RequestParam String query) {
        List<Document> results = vectorStore.similaritySearch(query);
        return Result.success(results);
    }
  3. 向量维度校验

    • 启动时检查 DashScope 模型与 Milvus 维度是否匹配
    • 提供友好的错误提示
  4. 批量上传优化

    • 增加异步处理避免超时
    • 添加进度回调通知前端
  5. 元数据增强

    • 记录上传用户ID
    • 添加文档类型标签
    • 支持自定义分类

九、环境依赖

必需服务:

  1. Milvus 向量数据库

    bash 复制代码
    # Docker 快速启动
    docker run -d --name milvus-standalone \
      -p 19530:19530 \
      -p 9091:9091 \
      milvusdb/milvus:latest standalone
  2. DashScope API Key

    yaml 复制代码
    spring:
      ai:
        dashscope:
          api-key: ${ALI_AI_KEY}  # 环境变量配置
  3. 对象存储服务 (阿里云 OSS / MinIO / 本地)

    yaml 复制代码
    file:
      uploadType: alioss  # local/minio/alioss

相关推荐
jiayou641 小时前
KingbaseES 表级与列级加密完全指南
数据库·后端
古茗前端团队3 小时前
急招!前端|测试|后端|产品(名额多,速来)
前端·后端·架构
喵个咪4 小时前
Go-Wind HTTP 服务器从入门到精通
后端·http·go
hunterandroid4 小时前
Hilt 依赖注入:从手动 new 到自动装配
后端
喵个咪4 小时前
Go-Wind gRPC 服务器从入门到精通
后端·go·grpc
喵个咪4 小时前
Go-Wind GraphQL 服务器从入门到精通
后端·graphql
青青子衿悠悠我心4 小时前
Docker与Kubernetes的十年战争与融合
后端