开源向量数据库Milvus简介

在本教程中,我们将探索Milvus,一个高度可扩展的开源矢量数据库。它旨在存储和索引来自深度神经网络和其他机器学习模型的大量矢量嵌入。Milvus 支持跨文本、图像、语音和视频等多种数据类型进行高效的相似性搜索。我们将广泛探索Milvus Java 客户端 SDK,以便通过第三方应用程序集成和管理 Milvus DB。为了解释,我们将以一个存储书籍矢量化内容的应用程序为例,以实现相似性搜索。

关键概念
在探索 Milvus Java SDK 的功能之前,我们先来了解一下 Milvus 是如何逻辑地组织数据的:

  • Collection:存储向量的逻辑容器,类似传统数据库中的表
  • 字段Field:集合内标量和矢量实体的属性,定义数据类型和其他属性
  • 模式Schema:定义集合内数据的结构和属性
  • 索引Index:通过组织向量来优化搜索过程,以实现高效检索
  • 分区Partition:集合内的逻辑细分,用于更有效地管理和组织数据

先决条件
在探索 Java API 之前,我们将先了解运行示例代码的一些先决条件。

Milvus 数据库实例
首先,我们需要一个 Milvus DB 实例。最简单、最快捷的方法是获取Zilliz Cloud提供的完全托管的免费 Milvus DB 实例:  

为此,我们需要注册一个 Zilliz 云帐户并按照文档创建一个免费的 DB 集群。

Maven 依赖
在开始探索 Milvus Java API 之前,我们需要导入必要的Maven 依赖项:

<dependency>
  <groupId>io.milvus</groupId>
  <artifactId>milvus-sdk-java</artifactId>
  <version>2.3.6</version>
</dependency>

Milvus Java 客户端 SDK
Milvus DB 服务端点是使用 gRPC 框架编写的。因此,所有使用不同编程语言(如 Python、Go 和 Java)的客户端 SDK 都在此 gRPC 框架之上提供 API。Milvus Java 客户端 SDK 像任何数据库一样全面支持 CRUD(创建、读取、更新和删除)操作。此外,它还支持管理操作,例如创建集合、索引和分区。为了执行各种 DB 操作,API 提供了相应的请求和请求构建器类。开发人员可以使用构建器类在请求对象中设置必要的参数。最后,在客户端类的帮助下将此请求对象发送到后端服务。在我们阅读完接下来的部分后,这将变得更加清晰。

创建 Milvus DB 客户端
Java 客户端 SDK 提供了MilvusClientV2类,用于 Milvus DB 中的管理和数据操作。ConnectConfigBuilder类帮助构建父类ConnectConfig ,该类保存了创建MilvusClientV2类实例所需的连接信息。


我们来看一下创建MilvusClientV2实例的方法,以进一步了解所涉及的类:

MilvusClientV2 createConnection() {
    ConnectConfig connectConfig = ConnectConfig.builder()
      .uri(CONNECTION_URI)
      .token(API_KEY)
      .build();
    milvusClientV2 = new MilvusClientV2(connectConfig);
    return milvusClientV2;
}

ConnectConfig类支持用户名和密码验证,但我们使用了推荐的 API 令牌。 ConnectConfigBuilder类采用 URI 和 API 令牌来创建ConnectConfig对象。稍后将使用该对象创建MilvusClientV2对象。

创建Collection
在将数据存储到 Milvus Vector DB 之前,我们必须创建一个集合。这涉及创建字段架构和集合,然后形成创建集合请求对象。最后,客户端将请求对象发送到 DB 服务端点以在 Milvus DB 中创建集合。

创建字段架构和集合架构
我们先来了解一下 Milvus Java SDK 中的相关关键类:   

FieldSchema类帮助定义集合的字段,而CollectionSchema使用FieldSchema来定义集合。此外,IndexParam中的IndexParamBuilder类在集合上创建索引。我们将通过示例代码探索其他类。首先,让我们来看看在createFieldSchema()方法中创建FieldSchema对象的步骤:

CreateCollectionReq.FieldSchema createFieldSchema(String name, String desc, DataType dataType,
    boolean isPrimary, Integer dimension) {
    CreateCollectionReq.FieldSchema fieldSchema = CreateCollectionReq.FieldSchema.builder()
      .name(name)
      .description(desc)
      .autoID(false)
      .isPrimaryKey(isPrimary)
      .dataType(dataType)
      .build();
    if (null != dimension) {
        fieldSchema.setDimension(dimension);
    }
    return fieldSchema;
}

FieldSchema类中的builder ()方法返回其子FieldSchemaBuilder类的实例。此类设置集合字段的重要属性,例如名称、说明和数据类型。builder类中的方法isPrimaryKey()有助于标记主键字段,而FieldSchema类中的setDimension()方法设置向量字段的强制维度。例如,让我们在方法createFieldSchemas()中设置名为Books的集合的字段:

private static List<CreateCollectionReq.FieldSchema> createFieldSchemas() {
    List<CreateCollectionReq.FieldSchema> fieldSchemas = List.of(
      createFieldSchema("book_id", "Primary key", DataType.Int64, true, null),
      createFieldSchema("book_name", "Book Name", DataType.VarChar, false, null),
      createFieldSchema("book_vector", "vector field", DataType.FloatVector, false, 5)
    );
    return fieldSchemas;
}

该方法返回Books 集合的book_id、book_name和book_vector字段的FieldSchema对象列表。book_vector字段存储维度为5 的向量, book_id为主键。确切地说,我们将书籍的矢量化文本存储在 book_vector 字段中。每个FieldSchema对象都是使用前面定义的createFieldSchema()方法创建的。创建FieldSchema对象后,我们将在createCollectionSchema()方法中使用它们来形成Books CollectionSchema对象:

private static CreateCollectionReq.CollectionSchema createCollectionSchema(
    List<CreateCollectionReq.FieldSchema> fieldSchemas) {
    return CreateCollectionReq.CollectionSchema.builder()
      .fieldSchemaList(fieldSchemas)
      .build();
}

子CollectionSchemaBuilder设置字段模式并最终构建父CollectionSchema对象。

创建收款请求并收款
现在让我们看看创建该集合的步骤:

void whenCommandCreateCollectionInVectorDB_thenSuccess() {
    CreateCollectionReq createCollectionReq = CreateCollectionReq.builder()
      .collectionName("Books")
      .indexParams(List.of(createIndexParam("book_vector", "book_vector_indx")))
      .description("Collection for storing the details of books")
      .collectionSchema(createCollectionSchema(createFieldSchemas()))
      .build();
    milvusClientV2.createCollection(createCollectionReq);
    assertTrue(milvusClientV2.hasCollection(HasCollectionReq.builder()
      .collectionName("Books")
      .build()));
    }

}
我们使用CreateCollectionReqBuilder类通过设置CollectionSchema对象和其他参数来构建CreateCollectionReq对象,然后将此对象传递给MilvusClientV2类的createCollection()方法创建集合,最后通过调用MilvusClientV2的hasCollection(HasCollectionReq)方法进行验证。CreateCollectionReqBuilder类还使用indexParams()方法在book_vector字段上定义索引。


IndexParam createIndexParam(String fieldName, String indexName) {
    return IndexParam.builder()
      .fieldName(fieldName)
      .indexName(indexName)
      .metricType(IndexParam.MetricType.COSINE)
      .indexType(IndexParam.IndexType.AUTOINDEX)
      .build();
}

该方法使用IndexParamBuilder类来设置Milvus DB 中索引所支持的各项属性。此外, IndexPram对象的COSINE度量类型属性对于计算向量之间的相似度得分非常重要。与关系型数据库一样,索引有助于提升 Milvus Vector DB 中频繁访问字段的查询性能。


创建分区
一旦创建了Books集合,我们就可以专注于创建分区的类,以便有效地组织数据。  

子类CreatePartitionReqBuilder帮助创建父类CreateParitionReq对象,设置分区和目标集合名称。然后,将请求对象传入MilvusClientV2的createPartition()方法中。

让我们使用前面定义的类在Books集合中创建一个分区Health :

void whenCommandCreatePartitionInCollection_thenSuccess() {
    CreatePartitionReq createPartitionReq = CreatePartitionReq.builder()
        .collectionName("Books")
        .partitionName("Health")
        .build();
    milvusClientV2.createPartition(createPartitionReq);
    assertTrue(milvusClientV2.hasPartition(HasPartitionReq.builder()
        .collectionName("Books")
        .partitionName("Health")
        .build()));
}

在该方法中,createPartitionReqBuilder类为Books集合创建CreatePartitionReq对象。随后,MilvusClientV2对象使用请求对象调用其createPartition()方法。这导致在Books集合中创建分区 Health 。最后,我们通过调用MilvusClientV2类的hasPartition()方法来验证分区是否存在。

将数据插入集合
在 Milvus DB 中创建好Books集合之后,我们就可以向其中插入数据了。

  • 子类InsertReqBuilder通过设置collectionName和数据来帮助创建其父类InsertReq对象。
  • InsertReqBuilder类的方法data()将List中的文档块插入到 Milvus DB 中。
  • 最后,我们将InsertReq对象传递给MilvusClientV2对象的insert()方法,以在集合中创建条目。

为了将数据插入到集合Books的分区Health中,我们将使用 JSON 文件book_vectors.json中的一些虚拟数据:

[
  {
    "book_id": 1,
    "book_vector": [
      0.6428583619771759,
      0.18717933359890893,
      0.045491267667689295,
      0.8578131397291819,
      0.6431108625406422
    ],
    "book_name": "Yoga"
  },
  More objects...
...
]

实际应用程序使用 BERT 和 Word2Vec 等嵌入模型来创建文本、图像、语音样本等的向量维度。让我们实际看一下之前定义的类:

void whenCommandInsertDataIntoVectorDB_thenSuccess() throws IOException {
    List<JSONObject> bookJsons = readJsonObjectsFromFile("book_vectors.json");
    InsertReq insertReq = InsertReq.builder()
      .collectionName("Books")
      .partitionName("Health")
      .data(bookJsons)
      .build();
    InsertResp insertResp = milvusClientV2.insert(insertReq);
    assertEquals(bookJsons.size(), insertResp.getInsertCnt());
}

readJsonObjectsFromFile ()方法从 JSON 文件中读取数据并存入bookJsons对象中。如前所述,我们用数据创建了InsertReq对象,然后将其传递给MilvusClientV2对象的insert()方法。最后,InsertResp对象中的getInsertCnt()方法给出了插入的记录总数。我们也可以在 Zilliz 云端控制台中验证插入的记录:  


执行向量相似性搜索
Milvus 借助一些关键类支持对集合进行向量相似性搜索 


SearchRequestBuilder设置了父类SearchReq的 topK 近邻、查询嵌入和集合名称等属性。此外,我们可以在filter()方法中设置标量表达式来获取匹配的实体。最后,MilvusClientV2类使用SearchReq对象调用search ( )方法来获取记录。

和往常一样,让我们​​看一下示例代码以了解更多信息:

void givenSearchVector_whenCommandSearchDataFromCollection_thenSuccess() {
    List<Float> queryEmbedding = getQueryEmbedding("What are the benefits of Yoga?");
    SearchReq searchReq = SearchReq.builder()
      .collectionName("Books")
      .data(Collections.singletonList(queryEmbedding))
      .outputFields(List.of("book_id", "book_name"))
      .topK(2)
      .build();
    SearchResp searchResp = milvusClientV2.search(searchReq);
    List<List<SearchResp.SearchResult>> searchResults = searchResp.getSearchResults();
    searchResults.forEach(e -> e.forEach(el -> logger.info("book_id: {}, book_name: {}, distance: {}",
      el.getEntity().get("book_id"), el.getEntity().get("book_name"), el.getDistance()))
    );
}

首先,方法getQueryEmbedding()将查询转换为向量维度或嵌入。然后SearchReqBuilder对象使用所有与搜索相关的参数创建SearchReq对象。有趣的是,我们还可以通过在构建器类的outputFields()方法中设置来控制结果实体中的字段名称。最后,我们调用MilvusClientV2的search()方法获取查询结果:

book_id: 6, book_name: Yoga, distance: 0.8046049
book_id: 3, book_name: Tai Chi, distance: 0.5370003

搜索结果中的距离属性表示相似度得分。在我们的示例中,我们考虑使用余弦相似度 (COSINE) 来衡量相似度得分。因此,余弦越大,两个向量之间的角度越小,表明这两个向量彼此更相似。此外,Milvus 在浮点类型嵌入上支持更多度量类型,例如欧几里得距离 (L2) 和内积 (IP)。

删除集合中的数据
让我们从通常的类图开始,来了解从集合中删除数据所涉及的关键类.


MilvusClientV2中的 delete() 方法删除Milvus DB 中的记录。它采用DeleteReq对象,允许使用ids和filter字段指定记录。子类DeleteRequestBuilder帮助构建父类DeleteReq对象。

让我们借助一些示例代码来深入了解。让我们看看从Books集合中删除book_id等于 1 和 2 的记录的步骤:

void givenListOfIds_whenCommandDeleteDataFromCollection_thenSuccess() {
    DeleteReq deleteReq = DeleteReq.builder()
      .collectionName("Books")
      .ids(List.of(1, 2))
      .build();
    DeleteResp deleteResp = milvusClientV2.delete(deleteReq);
    assertEquals(2, deleteResp.getDeleteCnt());
}

在MilvusClientV2对象上调用delete()方法后,我们使用DeleteResp对象中的getDeleteCnt()方法来验证删除的记录数。我们还可以使用DeleteReqBuilder对象上的filter()方法与标量表达式规则结合使用来指定要删除的匹配记录:

void givenFilterCondition_whenCommandDeleteDataFromCollection_thenSuccess() {
    DeleteReq deleteReq = DeleteReq.builder()
      .collectionName("Books")
      .filter("book_id > 4")
      .build();
    DeleteResp deleteResp = milvusClientV2.delete(deleteReq);
    assertTrue(deleteResp.getDeleteCnt() >= 1 );
}

根据filter()方法中定义的标量条件,从集合中删除book_id大于 4的记录。

结论
在本文中,我们探索了 Milvus Java SDK。它涵盖了与管理向量数据库相关的几乎所有主要操作。这些 API 设计精良且直观易懂,因此更易于采用和构建 AI 驱动的应用程序。但是,对向量的基本了解对于有效使用 API 也同样重要。