Milvus 使用小记

1,629 阅读19分钟

什么是Milvus向量数据库?


Milvus 是一个开源的、分布式的、高性能的向量数据库,它支持多种数据类型和索引类型,如浮点数、二进制、IVF_FLAT、IVF_SQ8、HNSW、ANNOY 等。Milvus 还支持多种度量类型,如 L2 距离、内积距离、余弦相似度等。Milvus 可以在毫秒级别内返回数百万条数据的搜索结果,同时保证高可用性和弹性扩展性。Milvus 还提供了丰富的 API 和 SDK,让你可以使用 Python、Java、Go 等语言来操作 Milvus。

Milvus 的应用场景和价值非常广泛,它可以用于图像检索、视频检索、语音检索、推荐系统、自然语言处理、计算机视觉等领域。

架构


Milvus 遵循数据平面和控制平面分解的原则,由四层组成,在可扩展性和灾难恢复方面相互独立。

用一张图表示架构图就是

Untitled.png

访问层

访问层由一组无状态的代理组成,是系统的前端层,也是用户的终端。它验证客户端请求并减少返回的结果:

  • 代理本身是无状态的。它使用负载均衡组件(如Nginx、Kubernetes Ingress、NodePort和LVS)提供统一的服务地址。
  • 由于Milvus采用了大规模并行处理(MPP)架构,代理在将最终结果返回给客户端之前,会聚合和后处理中间结果。

协调器服务

协调器服务为工作节点分配任务,充当系统的大脑。它承担的任务包括集群拓扑管理、负载均衡、时间戳生成、数据声明和数据管理。

有四种类型的协调器:根协调器(root coord)、数据协调器(data coord)、查询协调器(query coord)和索引协调器(index coord)。

根协调器(Root coordinator

根协调器处理数据定义语言(DDL)和数据控制语言(DCL)请求,比如创建或删除集合、分区或索引,以及管理TSO(时间戳Oracle)和时间刻度发放。

查询协调器(Query coordinator

查询协调器负责查询节点的拓扑和负载均衡,以及从增长段切换到已封存段。

数据协调器(Data coordinator

数据协调器管理数据节点的拓扑,维护元数据,并触发刷写、压缩和其他后台数据操作。

索引协调器(Index coordinator

索引协调器管理索引节点的拓扑,构建索引并维护索引元数据。


工作节点

工作节点是系统的执行器,遵循协调器服务的指令,并执行代理发送的数据操作语言(DML)命令。由于存储和计算的分离,工作节点是无状态的,可以在Kubernetes上实现系统的横向扩展和灾难恢复。工作节点分为三种类型:

查询节点

查询节点通过订阅日志代理,检索增量日志数据并将其转换为增长段,从对象存储加载历史数据,并在向量数据和标量数据之间运行混合搜索。

数据节点

数据节点通过订阅日志代理,检索增量日志数据,处理变更请求,并将日志数据打包成日志快照并存储在对象存储中。

索引节点

索引节点用于构建索引。索引节点不需要驻留在内存中,可以使用无服务器框架实现。


存储

存储是系统的基础,负责数据的持久化存储。它由元数据存储、日志代理和对象存储组成。

元数据存储

元数据存储存储元数据的快照,如集合模式、节点状态和消息消费检查点。存储元数据要求具有极高的可用性、强一致性和事务支持,因此Milvus选择了etcd作为元数据存储。Milvus还使用etcd进行服务注册和健康检查。

对象存储

对象存储用于存储日志的快照文件、标量和向量数据的索引文件以及中间查询结果。Milvus使用MinIO作为对象存储,并可以轻松部署在AWS S3和Azure Blob等全球最流行、成本效益高的存储服务上。然而,对象存储具有较高的访问延迟,并按查询次数计费。为了提高性能并降低成本,Milvus计划在基于内存或SSD的缓存池上实现冷热数据分离。

日志代理

日志代理是一个支持回放的发布-订阅系统(pub-sub system),负责流式数据的持久化存储、可靠异步查询的执行、事件通知和查询结果的返回。当工作节点从系统故障中恢复时,它还确保增量数据的完整性。Milvus集群使用Pulsar作为日志代理;Milvus独立版使用RocksDB作为日志代理。此外,日志代理还可以轻松替换为Kafka和Pravega等流数据存储平台。

Milvus围绕日志代理构建,并遵循“日志即数据”的原则,因此Milvus不维护物理表,但通过日志持久化和快照日志来保证数据的可靠性。

Untitled 1.png

日志代理是Milvus的支柱,负责数据持久化和读写分离,这得益于其固有的发布-订阅机制。上图以简化方式展示了该机制,系统被分为两个角色,即日志代理(用于维护日志序列)和日志订阅者。前者记录改变集合状态的所有操作,后者订阅日志序列以更新本地数据,并提供只读副本形式的服务。发布-订阅机制还为变更数据捕获(change data capture (CDC))和全球分布式部署等系统扩展性提供了空间。

单机部署 milvus


2.3.0 版本为例子

下面是一键脚本,适用于 Ubuntu 20.04

#!/usr/bin/env bash

# docker
curl -Lso- https://get.docker.com | bash

# docker compose
COMPOSE_VERSION=`git ls-remote https://github.com/docker/compose | grep refs/tags | grep -oE "[0-9]+\.[0-9][0-9]+\.[0-9]+$" | sort --version-sort | tail -n 1`
sudo sh -c "curl -L https://github.com/docker/compose/releases/download/v${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose"
sudo chmod +x /usr/local/bin/docker-compose

# milvus services
wget -O 'docker-compose.yml' 'https://github.com/milvus-io/milvus/releases/download/v2.3.0/milvus-standalone-docker-compose.yml'
sudo docker-compose up -d

使用指南


本文以 Python SDK (pymilvus)作为连接基础框架,可以用过下面脚本直接安装

pip install pymilvus

如果你是在 colab 上使用,因为自带有些库的版本跟要求不一致

推荐完整下载项目后根据 requirements.txt 文件强制安装固定版本

!git clone https://github.com/milvus-io/pymilvus.git
!pip install -U -q -r /content/pymilvus/requirements.txt

安装完成之后,需要在 RuntimeRestart Runtime 才能生效

下面是Milvus 版本和推荐使用的 PyMilvus 版本对照表

Milvus versionRecommended PyMilvus version
1.0.*1.0.1
1.1.*1.1.2
2.0.*2.0.2
2.1.*2.1.3
2.2.*2.2.15
2.3.*2.3.0


连接方式

连接方式大体上可以分为两类:

使用HTTPS + Token + URL 进行验证连接(适用于zilliz和对外服务)

from pymilvus import connections
URI = "https://***.zliizcloud.com"
TOKEN = "db_admin:xxxxx"
connections.connect(alias="default",
                    uri=URI,
                    token=TOKEN,
                    secure=True)
  • alias: 连接的别名,用于区分不同连接渠道,默认为 default,可以根据alias 来区分不同的连接渠道

  • uri: 连接的目标地址,如果是使用zilliz 的服务,会提供一个URL 用于连接

  • token: 身份验证密钥

    • 如果是servless 服务会直接提供一个 token 用来验证

Untitled 2.png

*   如果是付费服务,需要提前设置好密码,`token` 就是 `userName:password` (默认的 `username``db_admin`)

Untitled 3.png

使用 Host + PORT 直接进行无验证连接(适用本地网络)

from pymilvus import connections
HOST = "127.0.0.1"
PORT = "19530"

connections.connect(host=HOST, port=PORT, alias="default")
  • alias: 同上,连接的别名,用于区分不同连接渠道
  • HOST: IP地址
  • PORT:默认端口是**19530**

创建集合

创建集合需要四要素:

  • collection_name: 集合名称
  • dimension: 向量的维度
  • fieldSchema: 字段描述
  • fieldType: 字段类型

下面是一段创建集合的实际例子

from pymilvus import FieldSchema, CollectionSchema, DataType, Collection

# Create collection which includes the id, filepath of the image, and image embedding
COLLECTION_NAME = "example1"
fields = [
    FieldSchema(name='id', dtype=DataType.INT64, is_primary=True, auto_id=True, description="customized primary id"),
    FieldSchema(name='filepath', dtype=DataType.VARCHAR, max_length=65535, description="image file name"),  # VARCHARS need a maximum length, so for this example they are set to 65536 characters
    FieldSchema(name='vector', dtype=DataType.FLOAT_VECTOR, dim=1024, description="vector of image by deepdanbooru"),
    FieldSchema(name='meta', dtype=DataType.JSON,description = "images meta,like image tags")
]
schema = CollectionSchema(fields=fields, description="This is a collection for cosine similarity search")
collection = Collection(name=COLLECTION_NAME, schema=schema, enable_dynamic_field=False, using = "default", num_shards=2 , consistency_level="Strong")

如果你有使用过DBMS软件,应该对上面的构建语句感到很熟悉

  1. 首先是主键

    • 支持类型为 INT64 或者 VARCHAR 类型,前者是有符号整数,后者是字符串
    • is_primary=True 表明该字段为主键
    • auto_id=True 表示这是自增ID
    • description 这是字段描述信息
  2. 其次是VARCHAR 类型的文件路径,长度的范围是 [1, 65,535]

  3. 再有是 FLOAT_VECTOR 类型的向量,目前支持 BINARY_VECTORFLOAT_VECTOR 两种类型,目前一个集合里面只能有一个向量类型的字段,维度范围是 [1, 32768]

  4. 最后是比较特殊的 JSON 类型,可以目前存储Dict | List 类型的数据,并且还支持 JsonArrayOps 查询操作

  5. CollectionSchema 用来收集这些字段,并且给这个集合添加一段描述(可选)

通过 Collection(**kwargs) 来创建或者连接一个集合

下面讲解每个参数的作用

  • name: 集合的名称

  • schema: 字段的集合

  • enable_dynamic_field: 是否开启动态字段。如果插入的数据有部分字段没有在创建集合的时候申明,则会自动创建该数据的字段,可选默认是 False

  • using: 表明使用的哪个连接,对应上面连接所用使用的 alias,可选默认是 default

  • num_shards: 对于每次插入操作,milvus 都会根据 num_shards 将插入数据分成若干块。每块数据都将被放入到一个分段中,如果 num_shards = 2,那么每次插入的数据将被两个分段消耗掉。

    如果集群中有多个数据节点,比如有4个数据节点,可以设置 num_shard = 4 这样每个节点就能平均处理数据。

    设置范围是 [1,256]。

    单机状态无需更改,保持默认即可。

  • consistency_level: 一致性等级,有四个等级,他们分别是:

    • Strong: 一致性是最高、最严格的一致性级别。它确保用户可以读取最新版本的数据(默认选项)
    • **Bounded :**有限过时性,顾名思义,允许在特定时间段内出现数据不一致。然而,一般来说,该时间段内的数据始终是全局一致的。
    • Session: 确保所有数据写入都能在同一会话中立即被读取。换句话说,当你通过一个客户端写入数据时,新插入的数据可立即被搜索到。
    • Eventually: 读取和写入的顺序没有保证,如果不再进行写入操作,副本最终会趋同于相同的状态。在最终一致性条件下,副本会使用最新更新的值开始处理读取请求。最终一致性是四种一致性中最弱的一种。

创建索引

向量索引是用于加速向量相似性搜索的元数据的组织单元。

如果没有基于向量构建索引,Milvus 将默认执行暴力搜索。

下面是一段创建索引的示例代码

index_params = {
  "index_type":"IVF_FLAT",
  "metric_type":"COSINE",
  "params":{
    "nlist":1024
  }
}

# 为 "vector" 字段创建索引
collection.create_index("vector", index_params)
collection.load()

索引类型(index_type)

首先是定义索引类型index_type,因为之前定义的向量字段 vectorFLOAT_VECTOR 这里有几种选择:

这些都是 Milvus 支持的索引类型,它们的本质上的不同是使用了不同的算法和数据结构来组织和检索向量数据,它们各自的使用场景如下:

  • FLAT: 是一种无索引的类型,它会对所有的向量进行暴力搜索,保证 100% 的召回率,但是查询速度较慢,适用于数据量较小或者对精度要求很高的场景

  • IVF_FLAT: 是一种基于倒排文件(Inverted File)的索引类型,它会先对向量进行聚类,然后在每个聚类中建立 FLAT 索引,查询时只需要在最近的聚类中搜索,可以加速查询,但是需要指定聚类数量和查询数量,适用于数据量较大或者对速度要求较高的场景

  • IVF_SQ8: 是一种基于倒排文件和量化(Quantization)的索引类型,它会先对向量进行聚类,然后在每个聚类中使用乘积量化(Product Quantization)将向量压缩为 8 位整数,查询时只需要在最近的聚类中搜索,并使用查找表(Lookup Table)进行距离计算,可以节省存储空间和加速查询,但是会损失一定的精度,适用于数据量很大或者对存储空间要求较高的场景

  • IVF_PQ: 是一种基于倒排文件和量化的索引类型,它会先对向量进行聚类,然后在每个聚类中使用乘积量化将向量压缩为 8 位整数,并使用倒排表(Inverted List)存储压缩后的向量,查询时只需要在最近的聚类中搜索,并使用查找表进行距离计算,可以节省存储空间和加速查询,但是会损失一定的精度,适用于数据量很大或者对存储空间要求较高的场景

  • GPU_IVF_FLAT: 是一种基于 GPU 的倒排文件索引类型,它与 IVF_FLAT 索引相似,但是可以利用 GPU 的并行计算能力来加速查询,适用于数据量较大或者对速度要求很高的场景

  • GPU_IVF_PQ: 是一种基于 GPU 的倒排文件和量化索引类型,它与 IVF_PQ 索引相似,但是可以利用 GPU 的并行计算能力来加速查询,适用于数据量很大或者对速度要求很高的场景

  • HNSW :是一种基于层次化导航图(Hierarchical Navigable Small World)的索引类型,它会构建一个多层次的图结构来表示向量之间的近邻关系,并使用启发式搜索算法来快速找到最近邻向量,可以实现高速高召回的查询,但是需要指定树的数量和搜索深度等参数,适用于数据维度较高或者对精度要求较高的场景

  • DISKANN :是一种基于磁盘分区和近似最近邻搜索(Disk-based Partitioned Approximate Nearest Neighbor Search)的索引类型,它会将向量划分为多个磁盘分区,并在每个分区中构建 HNSW 索引,并使用多线程并行搜索算法来快速找到最近邻向量,可以实现高效地检索海量数据,但是需要指定分区数量和线程数量等参数,适用于数据量极大或者对存储空间要求较高的场景。

这里我们选择 IVF_FLAT 算是在性能和准确度上取得一个比较好的平衡值,更多类型的对比可以参考官方文档

度量类型(metric_type)

度量类型表示向量之间相似度的计算方式,浮点型向量主要使用以下距离计算公式:

  • IP(内积)是一种表示向量之间相似程度的度量方式,它的计算方式是将两个向量中对应位置的元素相乘后求和。

    IP 的值越大,表示两个向量越相似;IP 的值越小,表示两个向量越不相似。IP 适用于只需要考虑向量之间的相似度,而不需要考虑各个维度之间的权重关系的场景,例如自然语言处理、计算机视觉等领域。

  • L2(欧氏距离)是一种表示向量之间直线距离的度量方式,它的计算方式是将两个向量中对应位置的元素相减后平方,再将平方和求和并开平方

    L2 的值越小,表示两个向量越相似;L2 的值越大,表示两个向量越不相似。L2 适用于需要考虑各个维度之间的权重关系,或者需要将向量转化为单位向量进行相似度计算的场景,例如数值计算、信号处理、图像处理、机器学习等领域。

  • COSINE(余弦相似度) 是一种表示向量之间夹角余弦值的度量方式,它的计算方式是将两个向量的点积除以它们的模长乘积。

    COSINE 的值越接近 1,表示两个向量越相似;COSINE 的值越接近 -1,表示两个向量越不相似。COSINE 适用于需要考虑向量之间的方向关系,而不需要考虑向量的长度或者大小的场景,例如文本分析、信息检索、推荐系统等领域

如果输入的向量和查询向量进行了归一化处理,则 IP 和 L2 在搜索结果上跟 COSINE 是一致的

额外参数(params)

nlist 是一个参数,用于设置 IVF 类型的索引的聚类数量,即将向量数据划分为多少个子集或单元。

nlist 的值越大,表示聚类越细致,索引构建的时间和空间消耗越大,但是查询的精度和召回率可能会提高

一般来说,nlist 的值可以参考一个经验公式:4 × sqrt (n),其中 n 是每个分段中的向量数量

加载(load)

在创建完毕索引之后,需要执行 collection.load() 加载到内存中

至此,创建索引篇幅已经差不多讲完了,更多内容可以参考官方文档


插入数据

在此之前,我们需要准备一些数据

import numpy as np
import json

# Generate 10 random vectors of dimension 1024
vectors = np.random.rand(10, 1024).tolist()

# Generate 10 random filepaths of the format "image_XXX.jpg"
filepaths = [f"image_{np.random.randint(1000)}.jpg" for _ in range(10)]

# Generate 10 random meta data of the format {"tags": ["tag1", "tag2", ...]}
meta = [{"tags": [f"tag{np.random.randint(10)}" for _ in range(np.random.randint(1, 5))]} for _ in range(10)]

# Create a list of dictionaries to store the data
data = [{"filepath": filepath, "vector": vector, "meta": json.dumps(m)} for filepath, vector, m in zip(filepaths, vectors, meta)]

然后直接执行 collection.insert(data = data) 就行了

collection.insert(data = data)
# (insert count: 10, delete count: 0, upsert count: 0, timestamp: 444029420256624641, success count: 10, err count: 0)

此刻还是处于流式传输数据中,我们需要显式声明并且将数据落盘

collection.flush()

查询数据

可以用 search 方法来在集合中搜索相似的向量数据,指定查询向量、查询参数、返回字段

比如下面这个查询代码

offset = 0
query_params = {
  "metric_type":"COSINE",
  "offset":offset,
  "params":{
    "nprobe":1024
  }
}

query_vector = np.random.rand(1024).tolist()
TOP_K = 10

res = collection.search(data=[query_vector], 
anns_field='vector', 
param=query_params, 
limit=TOP_K, 
output_fields=['filepath'],
consistency_level="Strong")

# Print the ids, distances and filepaths of the results
for hits in res:
    for hit in hits:
        print(f"Vector id: {hit.id}, distance: {hit.distance}, filepath: {hit.entity.get('filepath')}")
  1. 首先是查询参数
    • metric_type: 类型跟上面定义的索引类型一致

    • offset: 偏移量,用来实现翻页功能

    • params.nprobe: 用于设置查询时需要搜索的聚类数量,即从所有的子集或单元中选择最相似的几个进行搜索。nprobe 的值越大,表示搜索的范围越广,查询的时间消耗越大,但是查询的精度和召回率可能会提高

      如果想搜索全部范围,则直接设置为索引的nlist 相同值,范围是 [1,nlist]

  2. 生成随机的查询向量
query_vector = np.random.rand(1024).tolist()
  1. 执行查询

    查询可以同时查询多个向量,这里只生成了一个查询向量,所以查询列表里面只有一个 query_vector

    • anns_field: 指定搜索的字段的名称
    • limit: 返回的最相似结果的数量。跟 params 中的 offset 相加要少于 16384
    • expr: 用于过滤属性的布尔表达式,默认为 None。详细规则可以查看 布尔表达式规则
    • output_fields: 可选,要返回的字段的名称。当前版本不支持向量字段。
    • consistency_level: 跟上面的索引字段相同,设置为搜索的一致性

Json查询条件

search 或者 query 时都可以对 JSON field 或者 dynamic field 进行表达式过滤,过滤的方式与之前的标量列类似。但需要注意的几点是:

  1. 不可以直接对 json 列进行过滤,如表达式 json == 1 是非法的。
  2. 如果要访问 json 列的 key,需要明确写清楚列名+[key], 如 json["a"]

这里分别介绍下三种Json List 匹配方式

JSON_CONTAINS

是用于检查 JSON 字段中的数组类型的值是否包含指定的元素,例如 **json_contains(meta["tags"], "**tag1**")** 表示检查 meta 字段中的 tags 数组是否包含 “tag1” 元素。如果包含,返回 True,否则返回 False。

JSON_CONTAINS_ALL

是用于检查 JSON 字段中的数组类型的值是否包含指定的所有元素,例如 json_contains_all(meta["tags"], ["tag1", "tag2"]) 表示检查 meta 字段中的 tags 数组是否包含 “tag1tag2” 元素。如果都包含,返回 True,否则返回 False。

JSON_CONTAINS_ANY

是用于检查 JSON 字段中的数组类型的值是否包含指定的任意一个元素,例如 json_contains_any(meta["tags"], ["tag1", "tag2"]) 表示检查 meta 字段中的 tags 数组是否包含 “tag1” “tag2” 元素。如果至少包含一个,返回 True,否则返回 False。


集合管理

辅助工具

在成功连接之后,可以用 utility 库可以使用一些辅助工具用来查看集合等信息

下面列举一些常见的

列举出所有的集合名称

from pymilvus import utility
utility.list_collections()

是否存在某个集合

utility.has_collection(collection_name)

删除某个集合

utility.drop_collection(collection_name)

等待索引构建完成

utility.wait_for_index_building_complete(collection_name)

获取集合数据数量

collection.num_entities
# 20

数据紧凑

可以将大量小段紧凑为一大段,节省容量降低占用

collection.compact()

因为紧凑过程是异步,需要显式的查看是否已经执行完成

collection.get_compaction_state()

#CompactionState
# - compaction id: 444008515030057217
# - State: Completed
# - executing plan number: 0
# - timeout plan number: 0
# - complete plan number:

总结

Milvus 算是年轻人的第一款向量数据库,如果熟悉常规的DBMS比如Mysql 上手速度会大大加快,并且一直在不断推出新功能,架构方面符合云原生,可以进行很方便针对性能侧重向进行扩容和缩容。有空的话建议可以看看架构设计的文档,对于分布式架构下,如何实现高并发、高可用的理解有新提升