Spring Boot3 + Milvus2 实战:向量检索应用开发指南

向量检索广泛应用于推荐系统、智能搜索等场景,Milvus2 作为开源云原生向量数据库,具备高吞吐、低延迟优势;Spring Boot3 简化配置、快速开发,本文通过实战讲解两者整合,实现完整向量检索应用。

一、前置知识与环境准备

1.1 核心技术栈

  • Spring Boot3:3.2.x(兼容Java17+);

  • Milvus2:2.4.x(稳定版);

  • Milvus Java SDK:客户端交互工具;

  • FastJSON2:JSON序列化;

  • JUnit5:单元测试。

1.2 环境要求

  1. JDK17+、Maven3.6+/Gradle7.5+;

  2. Milvus2服务(Docker部署或云服务);

  3. IntelliJ IDEA2022+。

1.3 Milvus2 Docker部署

Docker Compose部署单机版:

  1. 创建/data/milvus目录及docker-compose.yml,内容如下:

    version: '3.5'
    services:
    etcd:
    container_name: milvus-etcd
    image: quay.io/coreos/etcd:v3.5.5
    environment:
    - ETCD_AUTO_COMPACTION_MODE=revision
    - ETCD_AUTO_COMPACTION_RETENTION=1000
    - ETCD_QUOTA_BACKEND_BYTES=4294967296
    - ETCD_SNAPSHOT_COUNT=50000
    volumes:
    - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
    command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd

    minio:
    container_name: milvus-minio
    image: minio/minio:RELEASE.2023-03-20T20-16-18Z
    environment:
    MINIO_ACCESS_KEY: minioadmin
    MINIO_SECRET_KEY: minioadmin
    volumes:
    - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/data
    command: server /data
    healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
    interval: 30s
    timeout: 20s
    retries: 3

    milvus:
    container_name: milvus-standalone
    image: milvusdb/milvus:v2.4.5
    command: ["milvus", "run", "standalone"]
    environment:
    - ETCD_ENDPOINTS=etcd:2379
    - MINIO_ADDRESS=minio:9000
    - MINIO_ACCESS_KEY=minioadmin
    - MINIO_SECRET_KEY=minioadmin
    volumes:
    - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
    ports:
    - "19530:19530"
    - "9091:9091"
    depends_on:
    - etcd
    - minio

  2. 执行docker-compose up -d部署,docker ps | grep milvus验证启动(Up状态即为成功),默认连接地址localhost:19530

二、项目搭建与依赖配置

2.1 初始化项目

通过Spring Initializr创建Maven项目:选择Java17、Spring Boot3.2.5,Group为com.example,Artifact为spring-boot3-milvus2-demo,添加Spring Web、Lombok依赖,下载导入IDEA。

2.2 添加依赖

pom.xml添加Milvus SDK及FastJSON2依赖:

复制代码
<!-- Milvus Java SDK -->
<dependency>
    <groupId>io.milvus</groupId>
    <artifactId>milvus-sdk-java</artifactId>
    <version>2.4.4</version>
</dependency>

<!-- FastJSON2 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.44</version>
</dependency>

2.3 配置Milvus连接

复制代码
spring:
  application:
    name: spring-boot3-milvus2-demo

milvus:
  host: localhost
  port: 19530
  database: default
  collection-name: demo_collection
  dimension: 768  # 768维向量(适配BERT文本向量)

三、核心功能实现

3.1 Milvus客户端配置类

复制代码
package com.example.milvusdemo.config;

import io.milvus.client.MilvusClient;
import io.milvus.client.MilvusClientV2;
import io.milvus.param.ConnectParam;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "milvus")
@Data
public class MilvusConfig {
    private String host;
    private Integer port;
    private String database;
    private String collectionName;
    private Integer dimension;

    @Bean
    public MilvusClient milvusClient() {
        ConnectParam connectParam = ConnectParam.newBuilder()
                .withHost(host)
                .withPort(port)
                .withDatabase(database)
                .build();
        MilvusClient client = new MilvusClientV2(connectParam);
        // 退出时关闭连接
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            if (client != null) client.close();
        }));
        return client;
    }
}

3.2 向量实体类

com.example.milvusdemo.entity.VectorEntity:封装向量数据

复制代码
package com.example.milvusdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class VectorEntity {
    private Long id;  // 主键
    private List<Float> vector;  // 768维向量
    private String content;  // 原始内容
    private Long timestamp;  // 时间戳
}

3.3 Milvus核心服务类

com.example.milvusdemo.service.MilvusService:封装集合管理、向量增查核心操作

复制代码
package com.example.milvusdemo.service;

import com.example.milvusdemo.config.MilvusConfig;
import com.example.milvusdemo.entity.VectorEntity;
import io.milvus.client.MilvusClient;
import io.milvus.param.R;
import io.milvus.param.collection.*;
import io.milvus.param.dml.InsertParam;
import io.milvus.param.dml.SearchParam;
import io.milvus.response.InsertResp;
import io.milvus.response.SearchResp;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class MilvusService {
    private final MilvusClient milvusClient;
    private final MilvusConfig milvusConfig;

    // 检查集合是否存在
    public boolean checkCollectionExists() {
        HasCollectionParam param = HasCollectionParam.newBuilder()
                .withCollectionName(milvusConfig.getCollectionName())
                .build();
        R<Boolean> response = milvusClient.hasCollection(param);
        return response.getData() != null && response.getData();
    }

    // 创建集合(含Schema)
    public boolean createCollection() {
        FieldType idField = FieldType.newBuilder()
                .withName("id").withDataType(DataType.Int64).withPrimaryKey(true).withAutoID(false).build();
        FieldType vectorField = FieldType.newBuilder()
                .withName("vector").withDataType(DataType.FloatVector).withDimension(milvusConfig.getDimension()).build();
        FieldType contentField = FieldType.newBuilder()
                .withName("content").withDataType(DataType.VarChar).withMaxLength(2048).build();
        FieldType timestampField = FieldType.newBuilder()
                .withName("timestamp").withDataType(DataType.Int64).build();

        CreateCollectionParam param = CreateCollectionParam.newBuilder()
                .withCollectionName(milvusConfig.getCollectionName())
                .addFieldType(idField).addFieldType(vectorField).addFieldType(contentField).addFieldType(timestampField)
                .withShardsNum(2).build();
        return milvusClient.createCollection(param).getStatus() == R.Status.Success;
    }

    // 创建向量索引(IVF_FLAT)
    public boolean createIndex() {
        CreateIndexParam param = CreateIndexParam.newBuilder()
                .withCollectionName(milvusConfig.getCollectionName())
                .withFieldName("vector").withIndexType(IndexType.IVF_FLAT)
                .withIndexParam("{\"nlist\": 128}").withSyncMode(SyncMode.SYNC).build();
        return milvusClient.createIndex(param).getStatus() == R.Status.Success;
    }

    // 插入向量数据
    public List<Long> insertVectorData(List<VectorEntity> vectorEntities) {
        List<List<Object>> fieldsData = new ArrayList<>();
        fieldsData.add(vectorEntities.stream().map(VectorEntity::getId).collect(Collectors.toList()));
        fieldsData.add(vectorEntities.stream().map(VectorEntity::getVector).collect(Collectors.toList()));
        fieldsData.add(vectorEntities.stream().map(VectorEntity::getContent).collect(Collectors.toList()));
        fieldsData.add(vectorEntities.stream().map(VectorEntity::getTimestamp).collect(Collectors.toList()));

        InsertParam param = InsertParam.newBuilder()
                .withCollectionName(milvusConfig.getCollectionName()).withFieldsData(fieldsData).build();
        R<InsertResp> response = milvusClient.insert(param);
        return response.getStatus() == R.Status.Success ? response.getData().getInsertIds() : new ArrayList<>();
    }

    // 向量相似性检索(L2/IP度量)
    public List<VectorEntity> searchVector(List<Float> queryVector, int topK, String metricType) {
        // 加载集合到内存
        milvusClient.loadCollection(LoadCollectionParam.newBuilder()
                .withCollectionName(milvusConfig.getCollectionName()).build());

        String searchParam = String.format("{\"metric_type\": \"%s\", \"params\": {\"nprobe\": 10}}", metricType);
        SearchParam param = SearchParam.newBuilder()
                .withCollectionName(milvusConfig.getCollectionName()).withFieldName("vector")
                .withQueryVectors(List.of(queryVector)).withTopK(topK)
                .withParams(searchParam).withOutputFields(List.of("id", "content", "timestamp")).build();

        R<SearchResp> response = milvusClient.search(param);
        List<VectorEntity> result = new ArrayList<>();
        if (response.getStatus() == R.Status.Success) {
            SearchResp.ResultWrapper wrapper = response.getData().getResultWrapper();
            for (int i = 0; i < wrapper.getRowCount(); i++) {
                result.add(new VectorEntity(
                        wrapper.getFieldValue(i, "id", Long.class),
                        queryVector,
                        wrapper.getFieldValue(i, "content", String.class),
                        wrapper.getFieldValue(i, "timestamp", Long.class)
                ));
            }
        }
        return result;
    }

    // 删除集合
    public boolean dropCollection() {
        DropCollectionParam param = DropCollectionParam.newBuilder()
                .withCollectionName(milvusConfig.getCollectionName()).build();
        return milvusClient.dropCollection(param).getStatus() == R.Status.Success;
    }
}

3.4 Web接口层

com.example.milvusdemo.controller.VectorController:提供RESTful接口

复制代码
package com.example.milvusdemo.controller;

import com.example.milvusdemo.entity.VectorEntity;
import com.example.milvusdemo.service.MilvusService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/vector")
public class VectorController {
    private final MilvusService milvusService;

    // 初始化集合(创建+索引)
    @PostMapping("/init-collection")
    public ResponseEntity<String> initCollection() {
        if (milvusService.checkCollectionExists()) return ResponseEntity.ok("集合已存在");
        boolean createSuccess = milvusService.createCollection();
        if (!createSuccess) return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("集合创建失败");
        return milvusService.createIndex() ? ResponseEntity.ok("集合初始化成功") :
                ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("索引创建失败");
    }

    // 插入向量
    @PostMapping("/insert")
    public ResponseEntity<List<Long>> insertVectorData(@RequestBody List<VectorEntity> vectorEntities) {
        if (vectorEntities == null || vectorEntities.isEmpty()) return ResponseEntity.badRequest().body(null);
        List<Long> insertIds = milvusService.insertVectorData(vectorEntities);
        return insertIds.isEmpty() ? ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null) :
                ResponseEntity.ok(insertIds);
    }

    // 向量检索
    @PostMapping("/search")
    public ResponseEntity<List<VectorEntity>> searchVector(
            @RequestBody List<Float> queryVector,
            @RequestParam(defaultValue = "10") int topK,
            @RequestParam(defaultValue = "L2") String metricType) {
        if (queryVector == null || queryVector.size() != 768) return ResponseEntity.badRequest().body(null);
        return ResponseEntity.ok(milvusService.searchVector(queryVector, topK, metricType));
    }

    // 删除集合
    @DeleteMapping("/drop-collection")
    public ResponseEntity<String> dropCollection() {
        if (!milvusService.checkCollectionExists()) return ResponseEntity.ok("集合不存在");
        return milvusService.dropCollection() ? ResponseEntity.ok("集合删除成功") :
                ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("集合删除失败");
    }
}

四、测试验证

4.1 接口测试(Postman)

  1. 初始化集合:POST http://localhost:8080/api/vector/init-collection,预期响应"集合初始化成功";

  2. 插入数据:POST http://localhost:8080/api/vector/insert,请求体为VectorEntity列表JSON,预期返回插入ID列表;

  3. 向量检索:POST http://localhost:8080/api/vector/search,传入768维查询向量,参数topK=2、metricType=L2,预期返回2条相似结果;

  4. 删除集合:DELETE http://localhost:8080/api/vector/drop-collection,预期响应"集合删除成功"。

4.2 单元测试(JUnit5)

com.example.milvusdemo.service.MilvusServiceTest:验证核心服务方法

复制代码
package com.example.milvusdemo.service;

import com.example.milvusdemo.config.MilvusConfig;
import com.example.milvusdemo.entity.VectorEntity;
import io.milvus.client.MilvusClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class MilvusServiceTest {
    @Autowired
    private MilvusService milvusService;
    private List<VectorEntity> testData;
    private List<Float> queryVector;

    // 生成随机768维向量
    private List<Float> generateRandomVector() {
        List<Float> vector = new ArrayList<>();
        Random random = new Random();
        for (int i = 0; i < 768; i++) vector.add(random.nextFloat());
        return vector;
    }

    @BeforeEach
    void setUp() {
        testData = new ArrayList<>();
        queryVector = generateRandomVector();
        testData.add(new VectorEntity(1L, generateRandomVector(), "测试文本1", System.currentTimeMillis()));
        testData.add(new VectorEntity(2L, queryVector, "测试文本2(与查询向量一致)", System.currentTimeMillis()));
        testData.add(new VectorEntity(3L, generateRandomVector(), "测试文本3", System.currentTimeMillis()));
        // 初始化集合
        if (!milvusService.checkCollectionExists()) {
            milvusService.createCollection();
            milvusService.createIndex();
        }
    }

    @AfterEach
    void tearDown() {
        // 清理数据
        if (milvusService.checkCollectionExists()) milvusService.dropCollection();
    }

    @Test
    void checkCollectionExists() {
        assertTrue(milvusService.checkCollectionExists());
    }

    @Test
    void insertVectorData() {
        List<Long> insertIds = milvusService.insertVectorData(testData);
        assertEquals(3, insertIds.size());
        assertTrue(insertIds.containsAll(List.of(1L, 2L, 3L)));
    }

    @Test
    void searchVector() {
        milvusService.insertVectorData(testData);
        List<VectorEntity> result = milvusService.searchVector(queryVector, 2, "L2");
        assertEquals(2, result.size());
        assertTrue(result.stream().anyMatch(e -> e.getContent().equals("测试文本2(与查询向量一致)")));
    }
}

五、生产环境优化与注意事项

5.1 性能优化

  1. 索引优化:百万级以下用IVF_FLAT(精准),千万级以上用IVF_SQ8/HNSW(高速);

  2. 批量操作:批量插入减少交互,检索时动态调整nprobe参数(平衡准确率与速度);

  3. 连接池优化:配置超时时间、最大连接数;

  4. 数据分片:增加分片数实现分布式存储。

5.2 部署注意事项

  1. 部署模式:生产环境用Milvus集群版(高可用);

  2. 数据备份:定期备份,借助Milvus工具或MinIO;

  3. 监控告警:部署Prometheus+Grafana监控资源与延迟;

  4. 权限管理:配置用户名密码、SSL加密;

  5. 版本兼容:确保Milvus服务与SDK版本匹配。

六、总结

本文完成Spring Boot3与Milvus2整合的全流程实战,实现向量检索核心功能。借助两者优势可快速构建非结构化数据相似匹配应用,适用于多个AI场景。后续可扩展文本自动向量化(集成BERT)、可视化展示等功能。

相关推荐
weixin_421133411 小时前
PyInstaller& Nuitka & 项目 (如 django)
后端·python·django
m0_740043731 小时前
mapState —— Vuex 语法糖
java·前端·javascript·vue.js
廋到被风吹走1 小时前
【JDK版本】JDK1.8相比JDK1.7 JVM(Metaspace 与 G1 GC)
java·开发语言·jvm
7澄11 小时前
Java Web 底层解析:Servlet 执行流程、Tomcat 工作原理与自定义 Tomcat 实现
java·前端·servlet·tomcat·自定义tomcat·tomcat执行流程·servlet执行流程
Q_Q19632884751 小时前
python+django/flask+vue的高铁火车铁路订票管理系统
spring boot·python·django·flask·node.js·php
冬夜戏雪1 小时前
【java学习日记】【2025.12.4】【4/60】
java·开发语言·学习
feathered-feathered1 小时前
网络原理——应用层协议HTTP/HTTPS(重点较为突出)
java·网络·后端·网络协议·http·https
IT_陈寒1 小时前
React 18并发渲染实战:这5个性能陷阱让我浪费了整整一周!
前端·人工智能·后端
Slow菜鸟1 小时前
Java项目基础架构(三)| 日志统一处理
java·开发语言