大家好我是冰点,最近在做向量检索相关项目时,被 Milvus 的数据模型绕了不少弯 —— 明明知道它是专门存向量的数据库,但怎么把业务数据和向量对应起来、怎么设计结构才能高效查询,这些问题一开始真没少踩坑。今天就结合我的实战经验,用更接地气的方式聊聊 Milvus 数据模型,还会穿插 Java 代码示例,新手朋友也能跟着上手。
一、先搞懂:Milvus 数据模型的 “三层结构” 到底是什么?(新增:与 MySQL/PostgreSQL 类比)
很多人刚接触 Milvus 时,会被 Collection、Partition、Segment 这三个概念搞混,其实可以类比我们常用的 “文件夹 - 子文件夹 - 文件” 逻辑,更直观的是和 MySQL、PostgreSQL 这类关系型数据库对比,毕竟大家对 “库、表、分区” 更熟悉:
这样一对比是不是更清楚了?比如你在 MySQL 里建 “product 表” 存商品数据,对应到 Milvus 就是建 “product_collection”;MySQL 里给表按 “上架时间” 做 RANGE 分区,对应 Milvus 就是建不同的 Partition;甚至 Milvus 的 Schema 和 MySQL 的表结构设计逻辑几乎一致 —— 都要明确每个字段的类型、是否为主键等。
具体再拆解每个概念:
Collection(集合) :相当于 MySQL 的 “表”,是向量数据的 “容器”。比如做图片检索,MySQL 会建 “image_table”,Milvus 就建 “image_search_collection”,所有和图片向量相关的数据(向量 + 图片 ID、拍摄时间等属性)都放这里。和 MySQL 表一样,每个 Collection 必须提前定义 Schema,明确字段类型。
Partition(分区) :完全对应 MySQL/PostgreSQL 的 “表分区”。比如 MySQL 里给 “product 表” 按 “put_on_time” 做 RANGE 分区(2023 年数据放 p2023,2024 年放 p2024),Milvus 里就给 “product_collection” 建 “2023_partition” 和 “2024_partition”。查询时,Milvus 只扫指定分区,和 MySQL 只查对应分区数据一样,能大幅减少扫描范围。
Segment(段) :类比 MySQL 的 “物理数据文件”(如 InnoDB 的.idb 文件),是 Milvus 真正存储数据的最小单元。它会自动把数据分成 “密封段”(只读,类似 MySQL 的只读数据文件,适合高频查询)和 “生长段”(可写,类似 MySQL 的写入缓存,适合新增数据),不用我们手动操作,底层会像 MySQL 优化数据文件一样自动优化。
简单说:Collection 对应 “表”,Partition 对应 “表分区”,Segment 对应 “数据文件” ,三层配合起来,既能满足业务分类需求,又能保证查询效率,和关系型数据库的设计思路本质相通。
二、实战:用 Java 定义 Milvus 数据模型(附完整代码)
光说理论太抽象,咱们拿 “商品向量检索” 场景举例 —— 假设我们要存储商品的向量(比如根据商品描述生成的 768 维向量),还要存商品 ID、商品名称、上架时间这些属性,用 Java 怎么实现?
第一步:准备依赖(Maven)
首先要在项目里加 Milvus 的 Java SDK 依赖,目前最新稳定版是 2.4.3:
<dependency>
<groupId>io.milvus</groupId>
<artifactId>milvus-sdk-java</artifactId>
<version>2.4.3</version>
</dependency>
第二步:连接 Milvus 服务
先建立和 Milvus 的连接,这里要注意替换成你的 Milvus 地址和端口(本地部署一般是 127.0.0.1:19530):
import io.milvus.client.MilvusClient;
import io.milvus.client.MilvusServiceClient;
import io.milvus.param.ConnectParam;
public class MilvusDataModelDemo {
// Milvus连接参数
private static final String MILVUS_HOST = "127.0.0.1";
private static final int MILVUS_PORT = 19530;
private static MilvusClient milvusClient;
// 初始化Milvus连接
public static void initMilvusClient() {
ConnectParam connectParam = ConnectParam.newBuilder()
.withHost(MILVUS_HOST)
.withPort(MILVUS_PORT)
.build();
// 创建客户端实例
milvusClient = new MilvusServiceClient(connectParam);
System.out.println("Milvus连接成功!");
}
public static void main(String[] args) {
// 1. 初始化连接
initMilvusClient();
// 后续步骤:创建Collection、定义Schema、新增数据...
}
}
第三步:定义 Collection 的 Schema
Schema 就像 MySQL 的 “表结构”,必须明确两个关键部分:向量字段(Milvus 特有,存向量数据)和标量字段(和 MySQL 的普通字段一样,存商品 ID、名称等属性)。
咱们的商品场景里,Schema 设计和 MySQL 表结构的对应关系如下:
Java 代码实现如下,关键步骤我加了注释:
import io.milvus.param.collection.CreateCollectionParam;
import io.milvus.param.collection.FieldType;
import java.util.ArrayList;
import java.util.List;
// 在main方法里调用这个方法,创建Collection(对应MySQL建表)
public static void createProductCollection(String collectionName) {
// 1. 定义字段列表(对应MySQL的CREATE TABLE时的字段定义)
List<FieldType> fieldTypes = new ArrayList<>();
// 字段1:商品ID(主键,对应MySQL的PRIMARY KEY)
fieldTypes.add(FieldType.newBuilder()
.withName("product_id") // 字段名
.withDataType(FieldType.DataType.Int64) // 数据类型:长整型(对应MySQL BIGINT)
.withPrimaryKey(true) // 设为主键(对应MySQL PRIMARY KEY)
.withAutoID(false) // 不自动生成(对应MySQL不设AUTO_INCREMENT,手动传值)
.build());
// 字段2:商品名称(普通标量字段,对应MySQL VARCHAR)
fieldTypes.add(FieldType.newBuilder()
.withName("product_name")
.withDataType(FieldType.DataType.VarChar) // 字符串类型(对应MySQL VARCHAR)
.withMaxLength(100) // 最大长度100(对应MySQL VARCHAR(100))
.build());
// 字段3:上架时间(普通标量字段,对应MySQL DATETIME)
fieldTypes.add(FieldType.newBuilder()
.withName("put_on_time")
.withDataType(FieldType.DataType.DateTime) // 时间类型(对应MySQL DATETIME)
.build());
// 字段4:商品向量(Milvus特有,无MySQL对应字段)
fieldTypes.add(FieldType.newBuilder()
.withName("product_vector")
.withDataType(FieldType.DataType.FloatVector) // 浮点型向量
.withDimension(768) // 向量维度:768维(必须和业务向量一致)
.build());
// 2. 创建Collection的参数(对应MySQL的CREATE TABLE语句)
CreateCollectionParam createParam = CreateCollectionParam.newBuilder()
.withCollectionName(collectionName) // Collection名称:比如"product_collection"(对应MySQL表名)
.withFieldTypes(fieldTypes) // 传入上面定义的字段列表(对应MySQL字段定义)
.withShardsNum(2) // 分片数:类似MySQL分表的分片,小数据量2个足够
.build();
// 3. 执行创建操作(对应MySQL执行CREATE TABLE)
milvusClient.createCollection(createParam);
System.out.println("Collection " + collectionName + " 创建成功!(对应MySQL表创建成功)");
}
// 在main方法里调用:(对应MySQL执行CREATE TABLE product_collection (...))
createProductCollection("product_collection");
这里有个坑要提醒大家:向量字段的维度必须和你的业务向量维度一致(比如用 BERT 生成的文本向量是 768 维,就不能写成 512 维),这和 MySQL 里 “字段长度必须匹配插入值长度” 的逻辑一样,否则会报错。
第四步:(可选)创建 Partition,给数据分类(新增:与 MySQL 分区对比)
如果你的商品数据量很大,比如按 “上架年份” 分类,就可以创建 Partition—— 这和 MySQL 给表做分区的逻辑完全一致。比如 MySQL 里给 “product 表” 按 “put_on_time” 做 RANGE 分区:
-- MySQL分区示例:按上架时间分2024年分区
CREATE TABLE product (
-- 字段定义...
)
PARTITION BY RANGE (YEAR(put_on_time)) (
PARTITION p2024 VALUES LESS THAN (2025)
);
对应到 Milvus,就是创建 “2024_put_on_partition” 分区,存 2024 年上架的商品:
import io.milvus.param.collection.CreatePartitionParam;
// 对应MySQL创建表分区
public static void createProductPartition(String collectionName, String partitionName) {
CreatePartitionParam createParam = CreatePartitionParam.newBuilder()
.withCollectionName(collectionName) // 对应MySQL的表名
.withPartitionName(partitionName) // 对应MySQL的分区名(如p2024)
.build();
milvusClient.createPartition(createParam);
System.out.println("Partition " + partitionName + " 创建成功!(对应MySQL分区创建成功)");
}
// 在main方法里调用:(对应MySQL创建p2024分区)
createProductPartition("product_collection", "2024_put_on_partition");
后续插入 2024 年的商品数据时,只需要指定这个分区 —— 就像 MySQL 插入数据时自动落入对应分区一样,Milvus 也会把数据存到指定分区,查询时只扫该分区。
第五步:插入数据(向量 + 标量属性)(与 MySQL INSERT 对比)
Schema 和 Collection 都建好后,就可以插入数据了 —— 这和 MySQL 执行 INSERT 语句的逻辑类似,只不过 Milvus 要多传 “向量字段” 的值。
比如 MySQL 插入商品数据的 SQL 是:
INSERT INTO product (product_id, product_name, put_on_time)
VALUES (1001, '无线蓝牙耳机', NOW());
对应到 Milvus,除了要传这些标量字段,还要传 “product_vector” 向量值,Java 代码示例如下:
import io.milvus.param.insert.InsertParam;
import io.milvus.response.InsertResponse;
import java.util.ArrayList;
import java.util.List;
// 对应MySQL的INSERT语句
public static void insertProductData(String collectionName, String partitionName) {
// 1. 准备各字段的数据(对应MySQL INSERT里的VALUES列表)
List<Long> productIds = new ArrayList<>();
List<String> productNames = new ArrayList<>();
List<Long> putOnTimes = new ArrayList<>();
List<List<Float>> productVectors = new ArrayList<>();
productIds.add(1001L); // 对应product_id=1001
productNames.add("无线蓝牙耳机"); // 对应product_name='无线蓝牙耳机'
putOnTimes.add(System.currentTimeMillis()); // 对应put_on_time=当前时间
productVectors.add(generateRandomVector(768)); // 对应product_vector=768维向量(Milvus特有)
productIds.add(1002L);
productNames.add("高清智能摄像头");
putOnTimes.add(System.currentTimeMillis());
productVectors.add(generateRandomVector(768));
productIds.add(1003L);
productNames.add("便携式充电宝");
putOnTimes.add(System.currentTimeMillis());
productVectors.add(generateRandomVector(768));
// 2. 组装插入数据(对应MySQL的INSERT INTO ... VALUES (...))
InsertParam insertParam = InsertParam.newBuilder()
.withCollectionName(collectionName) // 对应MySQL的表名
.withPartitionName(partitionName) // 可选:指定插入的分区(对应MySQL插入分区表)
.addField("product_id", productIds) // 对应MySQL的product_id字段
.addField("product_name", productNames) // 对应MySQL的product_name字段
.addField("put_on_time", putOnTimes) // 对应MySQL的put_on_time字段
.addField("product_vector", productVectors) // 对应Milvus的向量字段
.build();
// 3. 执行插入(对应MySQL执行INSERT语句)
InsertResponse response = milvusClient.insert(insertParam);
System.out.println("插入成功!插入行数:" + response.getInsertCount() + "(对应MySQL的受影响行数)");
}
// 辅助方法:生成指定维度的随机向量(实际项目中替换成你的业务向量,比如文本embedding)
private static List<Float> generateRandomVector(int dimension) {
List<Float> vector = new ArrayList<>(dimension);
for (int i = 0; i < dimension; i++) {
vector.add((float) Math.random()); // 随机生成0-1的浮点数
}
return vector;
}
// 在main方法里调用:(对应MySQL执行INSERT语句)
insertProductData("product_collection", "2024_put_on_partition");
这里要注意:实际项目中,generateRandomVector方法要替换成你的真实向量生成逻辑(比如用 Sentence-BERT 把商品描述转成向量),随机向量只是为了演示。
三、避坑指南:设计 Milvus 数据模型的 3 个关键建议(新增:结合 MySQL 经验)
很多坑其实和 MySQL 的使用误区相通,咱们结合关系型数据库的经验来避坑:
主键别用 AutoID,尽量用业务 ID
很多人刚开始会用 Milvus 的 AutoID 自动生成主键 —— 这就像 MySQL 里给 “product_id” 设 AUTO_INCREMENT,但后续要关联 MySQL 业务库时,会发现 “Milvus 自动生成的 ID” 和 “MySQL 里的 product_id” 对不上,还要多存一个字段做映射,很麻烦。所以建议直接用业务里的唯一 ID(比如 MySQL 里的 product_id、user_id)作为 Milvus 的主键,就像 MySQL 表之间用业务主键关联一样,避免多表关联的麻烦。
分区别乱建,小数据量不用分区
分区的核心作用是 “减少查询范围”,这和 MySQL 分区的逻辑一致,但如果你的 Collection 数据量只有几万条 —— 就像 MySQL 表只有几万行时,建分区反而会增加数据库的管理开销(比如分区切换、索引维护),查询速度可能更慢。一般建议:数据量超过 100 万条,或者有明确的 “时间 / 地域” 分类维度(比如 MySQL 里按年份分区)时,再考虑用 Milvus 的 Partition。
向量维度别随便设,和模型输出一致
比如你用 ChatGLM 生成的向量是 512 维,就不能在 Schema 里定义成 76
评论区