Milvus 是一个开源的、高性能的、可扩展的向量数据库,用于存储、索引和快速检索海量的向量数据,广泛应用于 AI / ML 场景中的相似性搜索(Similarity Search),比如图像检索、语义搜索、推荐系统等。
核心概念
核心的层次关系
Milvus实例
│
├── 数据库(Database)
│ │
│ └── 集合(Collection)
│ │
│ ├── 字段定义(Schema)
│ │ ├── 主键字段(Primary Key Field)
│ │ ├── 向量字段(Vector Field)
│ │ └── 标量字段(Scalar Field)
│ │
│ ├── 分区(Partition,可选的,逻辑分区,用于提高查询效率)
│ │ └── 数据段(Segment, 数据持久化的最小单元,后台自动合并/压缩)
│ │ └── 实体(Entity)→ 包含各字段的实际数据
│ │
│ └── 索引(Index,加速向量相似性搜索)
│ └── 索引类型(IVF_FLAT, HNSW等)
│
└── 元数据存储(如集合名称、分区键、创建时间、数据量统计等,用于 Milvus 管理数据组织形式)
└── 对象存储(即向量数据,可以用本地文件或对象存储如MinIO)
Entity的示例:
{
"img_id": 1,
"img_vector": [0.1, 0.2, 0.3, ...],
"label": "tiger",
"age": 4
}
Milvus vs MySQL 概念对比
| 层级 | MySQL | Milvus | 说明 |
|---|---|---|---|
| 1 | 数据库实例 (Instance) | Milvus 实例 (Instance) | 整个服务实例 |
| 2 | 数据库 (Database) | 数据库 (Database) | 逻辑隔离的数据集合 |
| 3 | 表 (Table) | 集合 (Collection) | 数据组织的基本单位 |
| 4 | 列 (Column) | 字段 (Field) | 数据的结构定义 |
| 5 | 行 (Row) | 实体 (Entity) | 单条数据记录 |
主要操作流程
| 顺序 | 操作 | 说明 |
|---|---|---|
| 1 | 创建 Database | |
| 2 | 创建 Collection | 定义Schema |
| 3 | 插入数据 Entity | |
| 4 | 创建索引 Index | 目的是加速向量的查询效率 |
| 5 | 加载 Load | 在执行搜索之前需要将 Collection 加载到内存中 |
| 6 | 搜索 Search | 基于向量相似度,返回TopK条记录 |
| 6 | 查询 Query | 基于字段值的精确过滤(非向量检索),如筛选出年龄=18的记录 |
Spring中使用
环境依赖
使用docker搭建测试环境,all in one,跑一条命令就行。
JDK21,Spring Boot 3.5.7,主要用到的依赖:
<!-- 和milvus的版本一样 -->
<dependency>
<groupId>io.milvus</groupId>
<artifactId>milvus-sdk-java</artifactId>
<version>2.6.4</version>
</dependency>
<!-- 插入数据等会用到 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
application.yaml配置
milvus:
uri: "http://mydevcvm:19530" # 单实例 https://milvus.io/docs/zh/install_standalone-docker.md
token: "root:Milvus" # 用install_standalone-docker.md中方法起的实例是不鉴权的,这里随便写的
database: "database_test" # Database
collection: "collection_test" # Collection
dimension: 10 # 某个schema中的某个字段的维度,名字可以定义成 imgVectorDimension的话含义更清楚一些
创建客户端
通过配置类创建客户端,全局唯一实例
@Configuration
public class MilvusConfig {
@Value("${milvus.uri}")
private String uri;
@Value("${milvus.token}")
private String token;
@Bean
public MilvusClientV2 milvusClient() {
// 构建连接配置:传入地址和鉴权令牌
ConnectConfig config = ConnectConfig.builder().uri(uri).token(token).build();
// 创建客户端实例并返回
MilvusClientV2 client = new MilvusClientV2(config);
return client;
}
}
创建 Database、Collection
类比于mysql中先创建库、表,之后才能塞进数据去
@Test
void testCreateDatabase() throws InterruptedException {
try {
// 若已存在会抛出异常
client.createDatabase(CreateDatabaseReq.builder().databaseName(database).build());
System.out.println("create database succeed: " + database);
} catch (Exception e) {
if ((e.getMessage()).contains("database already exists")) {
System.out.println("database already exists: " + database);
} else {
throw e;
}
}
// 切换到目标数据库
client.useDatabase(database);
}
创建Collection。类似于mysql的数据库中创建table。useDatabase方法名有mysql那味了。
@Test
void testCreateCollection() throws InterruptedException {
// 切换到目标数据库
client.useDatabase(database);
boolean exists = client.hasCollection(HasCollectionReq.builder().collectionName(collection).build());
if (!exists) {
// 创建集合Schema:定义字段结构
CreateCollectionReq.CollectionSchema schema = client.createSchema();
// 字段1:主键(img_id)- Int64类型,自动生成(autoID=true)
schema.addField(AddFieldReq.builder()
.fieldName("img_id")
.dataType(DataType.Int64)
.isPrimaryKey(true)
.autoID(true)
.description("图像主键(自动生成)")
.build());
// 字段2:向量字段(img_vector)- FloatVector类型
schema.addField(AddFieldReq.builder()
.fieldName("img_vector")
.dataType(DataType.FloatVector)
.dimension(dimension)
.description("图像向量")
.build());
// 字段3:标量字段(label)- Int64类型,用于标记图像类别(0=猫,1=狗,2=鸟)
schema.addField(AddFieldReq.builder()
.fieldName("label")
.dataType(DataType.Int64)
.description("图像类别标签(0=猫,1=狗,2=鸟)")
.build());
// 5. 创建集合:传入集合名和Schema
client.createCollection(CreateCollectionReq.builder()
.collectionName(collection)
.collectionSchema(schema)
.build());
System.out.println("create collection succeed: " + collection);
} else {
System.out.println("collection already exist: " + collection);
}
}
创建索引,目的是加速查询。跟mysql中为table的字段创建索引一样,不过这里是给向量创建。
@Test
void testCreateIndex() throws InterruptedException {
client.useDatabase(database);
// 向量字段img_vector创建索引
IndexParam indexParam = IndexParam.builder()
.fieldName("img_vector") // 索引关联的向量字段
.indexType(IndexParam.IndexType.IVF_FLAT) // 索引类型
.metricType(IndexParam.MetricType.L2) // 相似度计算方式:L2距离
.extraParams(Map.of("nlist", 128)) // 聚类数量:默认128,可按数据量调整
.build();
// 创建索引:传入集合名和索引参数
client.createIndex(CreateIndexReq.builder()
.collectionName(collection) // 指定了在哪个Collection上创建
.indexParams(List.of(indexParam))
.build());
}
插入数据
// 工具方法:将float数组转为List<Float>(适配Milvus字段类型)
private List<Float> toFloatList(float[] a) {
List<Float> list = new ArrayList<>(a.length);
for (float v : a)
list.add(v);
return list;
}
@Test
void testInsert() throws InterruptedException {
client.useDatabase(database);
// 准备测试数据:5条图像向量 + 对应标签
List<float[]> vectors = List.of(
new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 3.0f, 0.0f, 0.0f, 0.0f, 0.0f}, // 标签0(猫)
new float[]{0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 3.0f, 0.1f, 0.0f, 0.0f, 0.0f}, // 标签0(猫)
new float[]{0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f}, // 标签1(狗)
new float[]{0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.0f, 0.0f, 3.0f, 0.0f, 0.0f}, // 标签1(狗)
new float[]{0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 3.0f, 0.0f, 0.0f, 3.0f, 3.0f} // 标签2(鸟)
);
List<Integer> labels = List.of(0, 0, 1, 1, 2);
// 组装数据:转为Milvus支持的Map格式(key=字段名,value=字段值)
List<Map<String, Object>> rows = new ArrayList<>();
for (int i = 0; i < vectors.size(); i++) {
Map<String, Object> row = new HashMap<>();
row.put("img_vector", toFloatList(vectors.get(i))); // 向量字段
row.put("label", labels.get(i)); // 标量字段(标签)
// 主键img_id无需手动传入(autoID=true)
rows.add(row);
}
// 转为JsonObject格式:Milvus SDK要求的入参类型
Gson gson = new Gson();
List<JsonObject> jsonObjects = rows.stream()
.map(m -> gson.toJsonTree(m).getAsJsonObject())
.toList();
// 执行插入操作
client.insert(InsertReq.builder()
.collectionName(collection)
.data(jsonObjects)
.build());
// Milvus 2.x默认开启自动flush,插入后无需手动调用(数据会定期落盘)
}
向量相似检索
基于插入的测试数据,执行TopK 检索(返回最相似的前 3 条结果),通过 L2 距离判断相似度(距离越小越相似)。
前面说了,Milvus 检索时需将集合加载到内存(仅需执行一次),若不加载会导致检索失败。如果集合很大就分区加载。
// 工具方法:将float数组转为List<Float>(适配Milvus字段类型)
private List<Float> toFloatList(float[] a) {
List<Float> list = new ArrayList<>(a.length);
for (float v : a)
list.add(v);
return list;
}
public void loadCollectionToMemory() throws InterruptedException {
// 加载集合到内存:检索前必须执行
client.loadCollection(LoadCollectionReq.builder()
.collectionName(collection)
.build());
System.out.println("集合已加载到内存: " + collection);
}
@Test
void testSearch() throws InterruptedException {
// 待检索的查询向量(模拟一张“猫”的图像向量)
final float[] queryVector = new float[]{0.15f, 0.25f, 0.35f, 0.45f, 0.55f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f};
client.useDatabase(database);
loadCollectionToMemory();
// 构建查询向量:转为Milvus支持的FloatVec类型
List<BaseVector> queryVectors = List.of(new FloatVec(queryVector));
// 构建检索请求:配置核心参数
SearchReq searchReq = SearchReq.builder()
.collectionName(collection) // 目标集合
.annsField("img_vector") // 检索的向量字段
.metricType(IndexParam.MetricType.L2) // 相似度计算方式(与索引一致)
.data(queryVectors) // 查询向量列表(本文仅1条)
.limit(3) // TopK:返回前3条最相似结果
.searchParams(Map.of("nprobe", 10)) // 检索参数:nprobe越大精度越高(默认10)
.outputFields(List.of("label")) // 需返回的标量字段(如标签)
.build();
// 4. 执行检索并获取结果
SearchResp resp = client.search(searchReq);
// 5. 解析检索结果:遍历输出ID、距离、标签
List<List<SearchResp.SearchResult>> results = resp.getSearchResults();
for (List<SearchResp.SearchResult> perQueryResult : results) {
for (SearchResp.SearchResult result : perQueryResult) {
Object imgId = result.getId(); // 图像主键(autoID生成)
Float distance = result.getScore(); // L2距离(越小越相似)
Map<String, Object> fields = result.getEntity(); // 标量字段(如label)
System.out.printf("ID:%s 距离:%.3f 标签:%s%n",
imgId, distance, fields.get("label"));
}
}
// 6. 检索完成后释放集合内存(可选:避免占用过多内存)
client.releaseCollection(ReleaseCollectionReq.builder()
.collectionName(collection)
.build());
client.close();
}
索引的类型比较
- FLAT
- IVF_FLAT
- HNSW