向量检索广泛应用于推荐系统、智能搜索等场景,Milvus2 作为开源云原生向量数据库,具备高吞吐、低延迟优势;Spring Boot3 简化配置、快速开发,本文通过实战讲解两者整合,实现完整向量检索应用。
一、前置知识与环境准备
1.1 核心技术栈
-
Spring Boot3:3.2.x(兼容Java17+);
-
Milvus2:2.4.x(稳定版);
-
Milvus Java SDK:客户端交互工具;
-
FastJSON2:JSON序列化;
-
JUnit5:单元测试。
1.2 环境要求
-
JDK17+、Maven3.6+/Gradle7.5+;
-
Milvus2服务(Docker部署或云服务);
-
IntelliJ IDEA2022+。
1.3 Milvus2 Docker部署
Docker Compose部署单机版:
-
创建
/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 /etcdminio:
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: 3milvus:
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 -
执行
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)
-
初始化集合:POST
http://localhost:8080/api/vector/init-collection,预期响应"集合初始化成功"; -
插入数据:POST
http://localhost:8080/api/vector/insert,请求体为VectorEntity列表JSON,预期返回插入ID列表; -
向量检索:POST
http://localhost:8080/api/vector/search,传入768维查询向量,参数topK=2、metricType=L2,预期返回2条相似结果; -
删除集合: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 性能优化
-
索引优化:百万级以下用IVF_FLAT(精准),千万级以上用IVF_SQ8/HNSW(高速);
-
批量操作:批量插入减少交互,检索时动态调整nprobe参数(平衡准确率与速度);
-
连接池优化:配置超时时间、最大连接数;
-
数据分片:增加分片数实现分布式存储。
5.2 部署注意事项
-
部署模式:生产环境用Milvus集群版(高可用);
-
数据备份:定期备份,借助Milvus工具或MinIO;
-
监控告警:部署Prometheus+Grafana监控资源与延迟;
-
权限管理:配置用户名密码、SSL加密;
-
版本兼容:确保Milvus服务与SDK版本匹配。
六、总结
本文完成Spring Boot3与Milvus2整合的全流程实战,实现向量检索核心功能。借助两者优势可快速构建非结构化数据相似匹配应用,适用于多个AI场景。后续可扩展文本自动向量化(集成BERT)、可视化展示等功能。