milvus基本概念和使用(java)

187 阅读6分钟

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 概念对比

层级MySQLMilvus说明
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,跑一条命令就行。

milvus.io/docs/zh/ins…

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

执行的向量类型

多副本和持久化机制

向量的维度选择