前篇笔记我们介绍了Milvus环境搭建以及数据库内部的一些基本概念,这篇笔记我们继续学习如何创建Collection、插入数据以及如何检索数据。操作Milvus数据库一般是通过各个编程语言的SDK实现的,不过相关的客户端库还在快速迭代中,Milvus官方目前只提供了Python、Java、Go、NodeJS四种开发环境的SDK支持,使用Milvus数据库时我们也应优先考虑在这四种环境中集成。我们这里以Milvus Python SDK为例进行介绍。
执行以下命令使用pip
安装Milvus Python SDK,这里要注意的是推荐安装和Milvus数据库版本相同的SDK版本。
pip install pymilvus==2.5.10
前面我们介绍过,Milvus中Collection的字段分为主字段、向量字段和标量字段。在具体编写代码前,这里我们先详细学习下Milvus支持哪些数据类型。
类型 | 说明 |
---|---|
INT64 | INT64类型的主字段,可以指定为自增长主字段,不过其生成方式不保证连续性或从1 开始,仅保证唯一性和递增趋势 |
VARCHAR | 可变字符串类型的主字段 |
类型 | 说明 |
---|---|
FLOAT_VECTOR | 32位浮点向量 |
FLOAT16_VECTOR | 16位半精度浮点向量 |
BFLOAT16_VECTOR | 另一种16位半精度浮点向量。BF16是一种半精度浮点格式,它相比FP16降低和精度但有更大的指数范围,能在不明显影响精度的情况下减少内存使用量 |
BINARY_VECTOR | 只有0和1的向量,用于某些图像处理和信息检索场景中表示数据的紧凑特征 |
SPARSE_FLOAT_VECTOR | 这种类型没有直接存储向量,而是存储非零数字及其序号的列表,用于高效存储高维稀疏向量 |
对于FLOAT_VECTOR
、FLOAT16_VECTOR
、BFLOAT16_VECTOR
这三种向量字段类型,支持以下索引类型:
FLAT:穷举式的“平面”搜索,其实就是没有索引的暴力搜索,即直接将查询向量与数据中的全部向量字段逐一比较计算,没有额外的预处理和数据结构,简单可靠,但速度最慢。
IVF_FLAT:这种索引利用了倒排文件(IVF,Inverted File),向量会被通过K-Means等算法聚类到nlist
个簇(centroid)中,查询向量时,首先找到距离最近的nprobe
个簇,然后在这些簇中的所有向量中进行精确距离计算和排序。
IVF_SQ8:这种索引是在IVF_FLAT
基础上对原始向量进行了标量量化(SQ,Scalar Quantization),相比IVF_FLAT
内存占用大幅降低。速度也更快,但会损失一定精度。
IVF_PQ:和IVF_SQ8
的区别是使用了乘积量化(PQ,Product Quantization),内存占用更低,但精度损失更大,通常用于内存无法装下的超大数据集。
GPU_IVF_FLAT:IVF_FLAT
的GPU实现。
GPU_IVF_PQ:IVF_PQ
的GPU实现。
HNSW:HNSW(Hierarchical Navigable Small World)也叫可导航小世界图,它采用了一种基于图的近似最近邻(ANN)搜索算法。这种索引会建立多层图结构,底层包含所有数据节点,上层节点是下层的子集,层数越高节点越稀疏,长距离连接“高速公路”越多。搜索时将从顶层开始并利用“高速公路”快速跳到目标区域,然后逐层向下搜索并缩小范围,最终在底层找到最近邻的数据点。构建HNSW索引时每个节点连接最多M
个邻居,M
默认值为30
,M
越大图越稠密,查询更准确,但构建速度更慢、内存消耗更大;构建时寻找插入点的搜索深度由efConstruction
参数控制,它是索引构建过程中考虑连接的候选邻居数量,默认值360
,这个参数越大索引便越精确,但会增加构建索引的时间和内存使用量;搜索搜索深度由ef
设置,ef
越大搜索精度越高,但速度越慢。
DISKANN:DISKANN(Disk Accelerated Nearest Neighbor)是微软提出的近似最近邻方法,这是一种为固态硬盘(SSD)设计的索引结构,这种索引可以不将数据全部载入内存,但十分依赖磁盘的IO性能,适用于需要支持亿级数据量且查询延迟要求相对宽松的场景。
这三种向量字段支持以下度量方式:
L2:欧氏距离
IP:内积
COSINE:余弦相似度
对于BINARY_VECTOR
向量字段类型,支持索引类型包括:BIN_FLAT、BIN_IVF_FLAT,度量方式包括:JACCARD、HAMMING。
对于SPARSE_FLOAT_VECTOR
向量字段类型,支持索引类型为SPARSE_INVERTED_INDEX,度量方式包括:IP、BM25。
类型 | 说明 |
---|---|
VARCHAR | 字符串字段 |
INT8/INT16/INT32/INT64/FLOAT/DOUBLE | 数字字段 |
BOOL | 布尔类型字段 |
JSON | JSON数据字段,检索时可以基于JSON内部字段进行筛选 |
ARRAY | 数组字段,存储相同数据类型元素的有序集合 |
Milvus中对于标量类型也支持索引,类型包括:
INVERTED:索引的默认值,使用倒排索引。
BITMAP:位图索引,一种存储字段中所有唯一值的位图的索引类型。
STL_SORT:排序索引,支持数字类型。
Trie:字典树索引,适用于VARCHAR
字段。
下面例子中我们使用pymilvus
库,在default
库中创建一个名叫t_demo
的Collection并建立索引。
from pymilvus import connections, Collection, FieldSchema, DataType, CollectionSchema, utility
# 连接Milvus的default库
connections.connect('default', host='localhost', port='19530', db_name='default')
if 't_demo' not in utility.list_collections(using='default'):
# 创建Collection
fields = [
FieldSchema(name='id', dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name='embedding', dtype=DataType.FLOAT_VECTOR, dim=4)
]
schema = CollectionSchema(fields, description='测试向量数据')
collection = Collection(name='t_demo', schema=schema, using='default')
# 创建索引
index_params = {
'index_type': 'HNSW',
'metric_type': 'L2',
'params': {
'M': 30,
'efConstruction': 360
}
}
collection.create_index(field_name='embedding', index_params=index_params)
代码都很容易理解,其中fields
中定义了字段信息,id
是INT64类型的主键字段,我们将其设置为了自增长的;embedding
是向量字段,维度为4
。索引我们使用的是HNSW(Hierarchical Navigable Small World)索引,这是一种用于近似最近邻(ANN)搜索的图结构索引,它在处理高维向量数据时,具有高搜索精度和较快的查询速度,在文本语义检索等场景下较为常用,L2表示使用欧几里得距离作为相似度度量方式,后面的M
参数和efConstruction
参数分别是每个节点连接的最大邻居数和构建索引时的候选数。
执行以上代码后,我们可以在Attu中看到default
数据库内创建了Collection。
pymilvus
中插入数据比较简单,我们创建一个dict
类型的数据即可,如果主键字段指定了auto_id=True
自增长则不可以手动插入,基于前面的例子,我们需要手动设置的字段只有embedding
这个向量字段。
from pymilvus import connections, Collection
# 连接Milvus的default库
connections.connect('default', host='localhost', port='19530', db_name='default')
# 插入数据
collection = Collection(name='t_demo', using='default')
test_data = [
{'embedding': [0.1, 0.2, 0.3, 0.4]}
]
insert_result = collection.insert(test_data)
print('插入结果:', insert_result)
insert_result
中包含了插入的结果,包括插入条数、删除条数、更新条数,以及成功、失败统计等信息。我们可以将其打印出来查看结果。
对于数据更新,pymilvus
中提供的其实是Upsert操作,即插入与更新结合在了一起。
from pymilvus import connections, Collection
# 连接Milvus的default库
connections.connect('default', host='localhost', port='19530', db_name='default')
# 更新数据
collection = Collection(name='t_demo', using='default')
collection.load()
mutation_result = collection.upsert([{'id': 458545569821100002, 'embedding': [1, 2, 3, 4]}])
print('更新结果:', mutation_result)
Milvus的Upsert操作设计的非常反直觉,虽然名字叫Upsert但实际上底层是先插入再删除,它实际上是这样工作的:Milvus允许主键字段相同的数据记录存在,Upsert时并非直接更新一条数据记录,而是会先插入一条指定主键的数据,然后删除旧的数据。这里又分为是否使用了auto_id
主键两种情况,如果使用了自增主键,插入数据时新增的主键会发生变化且生成的值是不可控的,这会导致你看到Upsert后似乎主键值也变了;而如果没有使用自增主键,则Upsert最终的结果主键不会发生变化。
Milvus中可以通过指定筛选条件删除数据,下面是一个例子,我们删除id
值为458545569821100000
的字段。
from pymilvus import connections, Collection
# 连接Milvus的default库
connections.connect('default', host='localhost', port='19530', db_name='default')
# 更新数据
collection = Collection(name='t_demo', using='default')
collection.load()
collection.delete(expr='id == 458545569821100000')
下面例子我们使用了一个标量筛选条件查询数据(id
虽然是主字段但也可以用于筛选)。
from pymilvus import connections, Collection
# 连接Milvus的default库
connections.connect('default', host='localhost', port='19530', db_name='default')
# 查询数据
collection = Collection(name='t_demo', using='default')
collection.load()
results = collection.query(expr="id >= 0", output_fields=['*'])
print("查询结果:", results)
查询数据时有一点需要注意,我们需要调用collection.load()
将数据加载到Milvus服务端的内存中,这一步是查询前的必要步骤,如果没有提前load()
需要查询的Collection,查询可能报错。
下面例子使用向量检索数据,向量检索需要提供用于查询的向量以及检索参数等信息。
from pymilvus import connections, Collection
# 连接Milvus的default库
connections.connect('default', host='localhost', port='19530', db_name='default')
# 向量检索数据
collection = Collection(name='t_demo', using='default')
collection.load()
results = collection.search(
data=[[0.1, 0.2, 0.3, 0.4]],
anns_field='embedding',
param={
'metric_type': 'L2',
'params': {'ef': 360},
},
limit=5,
output_fields=['*']
)
print(results)
代码中,data
是检索的向量,这里可以传多个表示进行多次查询;anns_field
是检索的Collection中的向量字段;param
是检索参数,这个参数和向量字段上的索引有关,metric_type: L2
是使用欧几里得距离作为相似度度量方式,这个参数需要和向量字段上索引的定义一致,params
中ef
是HNSW索引控制搜索过程中的候选节点数量,值越大精度越高但速度越慢;limit
是查询的限制条数;output_fields
是输出字段,*
表示输出所有字段。
collection.search()
函数也支持expr
指定标量筛选表达式,实现向量字段和标量字段混合的筛选检索,下面是一个例子。
results = collection.search(
data=[[0.1, 0.2, 0.3, 0.4]],
anns_field='embedding',
param={
'metric_type': 'L2',
'params': {'ef': 360},
},
limit=5,
output_fields=['*'],
expr='id >= 0',
)