目 录CONTENT

文章目录

Milvus 数据模型:如何组织向量数据

冰点
2025-09-30 / 0 评论 / 0 点赞 / 1 阅读 / 48161 字

大家好我是冰点,最近在做向量检索相关项目时,被 Milvus 的数据模型绕了不少弯 —— 明明知道它是专门存向量的数据库,但怎么把业务数据和向量对应起来、怎么设计结构才能高效查询,这些问题一开始真没少踩坑。今天就结合我的实战经验,用更接地气的方式聊聊 Milvus 数据模型,还会穿插 Java 代码示例,新手朋友也能跟着上手。

一、先搞懂:Milvus 数据模型的 “三层结构” 到底是什么?(新增:与 MySQL/PostgreSQL 类比)

很多人刚接触 Milvus 时,会被 Collection、Partition、Segment 这三个概念搞混,其实可以类比我们常用的 “文件夹 - 子文件夹 - 文件” 逻辑,更直观的是和 MySQL、PostgreSQL 这类关系型数据库对比,毕竟大家对 “库、表、分区” 更熟悉:

Milvus 概念

关系型数据库(MySQL/PostgreSQL)类比

通俗理解

Collection(集合)

单个数据库中的 “表(Table)”

存同一类业务数据的容器,比如商品表、用户表

Partition(分区)

表的 “分区(Partition)”(如 MySQL 的 RANGE 分区)

给表数据按规则分类,比如按时间分 2023/2024 区

Segment(段)

表的 “物理数据文件”(如 MySQL 的.idb 文件)

底层实际存数据的最小单元,自动管理

Schema(结构)

表的 “表结构(Table Schema)”

定义字段类型、主键、约束等规则

这样一对比是不是更清楚了?比如你在 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 表结构的对应关系如下:

Milvus Schema 字段

类型

对应 MySQL 表结构字段

说明

product_id

Int64(长整型)

product_id BIGINT PRIMARY KEY

主键,唯一标识商品,手动赋值

product_name

VarChar(字符串)

product_name VARCHAR(100)

商品名称,最大长度 100

put_on_time

DateTime(时间)

put_on_time DATETIME

上架时间,精确到毫秒

product_vector(核心)

FloatVector(浮点向量)

无对应字段(Milvus 特有)

768 维向量,存商品描述的 embedding

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 的使用误区相通,咱们结合关系型数据库的经验来避坑:

  1. 主键别用 AutoID,尽量用业务 ID

很多人刚开始会用 Milvus 的 AutoID 自动生成主键 —— 这就像 MySQL 里给 “product_id” 设 AUTO_INCREMENT,但后续要关联 MySQL 业务库时,会发现 “Milvus 自动生成的 ID” 和 “MySQL 里的 product_id” 对不上,还要多存一个字段做映射,很麻烦。所以建议直接用业务里的唯一 ID(比如 MySQL 里的 product_id、user_id)作为 Milvus 的主键,就像 MySQL 表之间用业务主键关联一样,避免多表关联的麻烦。

  1. 分区别乱建,小数据量不用分区

分区的核心作用是 “减少查询范围”,这和 MySQL 分区的逻辑一致,但如果你的 Collection 数据量只有几万条 —— 就像 MySQL 表只有几万行时,建分区反而会增加数据库的管理开销(比如分区切换、索引维护),查询速度可能更慢。一般建议:数据量超过 100 万条,或者有明确的 “时间 / 地域” 分类维度(比如 MySQL 里按年份分区)时,再考虑用 Milvus 的 Partition。

  1. 向量维度别随便设,和模型输出一致

比如你用 ChatGLM 生成的向量是 512 维,就不能在 Schema 里定义成 76

0

评论区