Milvus向量数据库:AI时代的向量搜索利器

一、什么是 Milvus 向量数据库?

Milvus 是一款开源的向量数据库(2019年提出),其唯一目标是存储、索引和管理由深度神经网络和其他机器学习(ML)模型生成的大规模嵌入向量

作为一个专门设计用于处理输入向量查询的数据库,它能够处理万亿级别的向量索引。与现有的关系型数据库主要处理遵循预定义模式的结构化数据不同,Milvus 从底层设计用于处理从非结构化数据转换而来的嵌入向量。

随着互联网的发展和演变,非结构化数据变得越来越常见,包括电子邮件、论文、物联网传感器数据、Facebook 照片、蛋白质结构等等。为了使计算机能够理解和处理非结构化数据,使用嵌入技术将它们转换为向量。Milvus 存储和索引这些向量。Milvus 能够通过计算它们的相似距离来分析两个向量之间的相关性。如果两个嵌入向量非常相似,则意味着原始数据源也很相似。

二、关键概念

(一)非结构化数据

非结构化数据包括图像、视频、音频和自然语言等信息,这些信息不遵循预定义的模型或组织方式。这种数据类型占据了世界数据的约 80%,可以使用各种人工智能(AI)和机器学习(ML)模型将其转换为向量。

(二)嵌入向量

嵌入向量是对非结构化数据(如电子邮件、物联网传感器数据、Instagram 照片、蛋白质结构等)的特征抽象。数学上,嵌入向量是一个浮点数或二进制数的数组。现代的嵌入技术被用于将非结构化数据转换为嵌入向量。

(三)向量相似度搜索

向量相似度搜索是将向量与数据库进行比较,以找到与查询向量最相似的向量的过程。使用近似最近邻搜索算法加速搜索过程。如果两个嵌入向量非常相似,那么原始数据源也是相似的。

(四)Collection和Field

与传统数据库引擎类似,您也可以在 Milvus 中创建数据库,并为某些用户分配权限来管理它们。那么这些用户就有权管理数据库中的集合。一个 Milvus 集群最多支持 64 个数据库.

在关系数据库中,表和字段的结构可以与Milvus中的CollectionField进行对应:

Milvus 关系数据库 描述
Collection 集合相当于关系数据库中的表,用于组织数据
Field 字段 字段Schema相当于表中的列
is_primary 主键 Field Schema中标记为主键对应该列的主键
dtype 数据类型 字段的数据类型,如INT, VARCHAR
max_length 最大长度 对应VARCHAR类型字段的最大字符数
dim - 向量字段的维度没有直接对应,但可以视为特殊数据处理

注意:1个collection最多支持4个向量Field

1、Filed schema

Field schema 是字段的逻辑定义。我们在定义集合架构和管理集合之前需要定义的第一件事就是定义 Field schema

Milvus 集合中仅支持一个主键字段。

属性 描述 备注
name 要创建的集合中的字段名称 String,必填
dtype 字段的数据类型 必填
description 字段描述 String,选填
is_primary 是否设置该字段为主键字段 Boolean (true or false) 主键字段必填
auto_id(主键字段必填) 切换以启用或禁用自动 ID(主键)分配 TrueFalse
max_lengthVARCHAR 字段必需) 允许插入的字符串的最大长度。 [1, 65,535]
dim 向量的维数 ∈[1, 32768]
is_partition_key 该字段是否是分区键字段 布尔值(truefalse

2、Collection schema

collection schemacollection 的逻辑定义。我们需要在定义 collection schema 之前定义 field schema

属性 描述 备注
field 集合中要创建的字段 必填
description 集合描述 String,选填
partition_key_field 设计用作分区键的字段的名称。 String, 选填
enable_dynamic_field 是否启用动态模式 Boolean (true or false)

三、为什么选择Milvus?

  • 在处理大规模数据集的向量搜索时具有高性能。

  • 开发者优先的社区,提供多语言支持和工具链。

  • 云扩展性和高可靠性,即使出现故障也不会受到影响。

  • 通过将标量过滤与向量相似度搜索配对,实现混合搜索。

四、支持哪些索引和度量?

索引是数据的组织单位。在搜索或查询插入的实体之前,必须声明索引类型和相似度度量。如果您未指定索引类型,则 Milvus 将默认使用暴力搜索。

(一)索引类型

1、FLAT(暴力搜索)

FLAT:适合在小型、百万级数据集上寻求完全准确和精确搜索结果的场景

  • 这是最简单的索引方式,进行暴力搜索(brute-force),可以保证精确度,但效率低,尤其在数据量大时。适合场景:在小型、百万级数据集上寻求完全精确的搜索结果。

FLAT:意味着不对原始的向量数据进行压缩,保持向量的原始精度,所以在进行搜索的时候,计算是精确的

2、IVF_FLAT

(1)介绍

IVF_FLAT:通过先分类,后精确查找的策略,极大提高检索效率。是一种基于倒排索引的方法,核心是在精度和速度之间取得一个平衡。

核心思想:分而治之

IVF:是Inverted File Index的缩写,即倒排索引

(2)工作原理
第一步:聚类
  • 使用聚类算法(如k-means)将数据集中所有的向量进行"分类(簇)"

  • 算法会计算出来指定数量(比如1024个)的中心点,每个中心点代表一个类别(称为簇)

  • 最终每个向量都会被划分到举例它最近那个中心点所代表的簇中

第二步:创建倒排索引
  • 为第一步中得到的每个簇(书架)建立一个倒排列表

  • 这个列表记录着所有属于这个簇的向量的id(以及向量本身,因为用的是flat,所以数据不压缩)

每个向量会被映射到它所属的簇,这样在查询时,系统只需关注与查询向量相似的簇,而不需要搜索整个高维空间,从而显著降低搜索的时间复杂度。

第三步:查询阶段

1:确定搜索范围(找到可能相关的书架区域)

当新查询向量到达,系统先计算它与所有簇中心点的距离,找到距离最近的若干个簇

nprobe决定了查询几个最近的书架区域

假设总共有1024个区域(簇),设置nprobe=6,查找向量最接近的6个簇中心点对应的区域

2:在范围内进行精确查找

系统从创建的倒排索引列表中,取出来nprobe个簇中的所有向量,然后在这个子集中进行精确的线性搜索(暴力比较),找出来最相似的K个结果

  • 为了优化查询,IVF_FLAT使用一个参数 nprobe 来控制搜索的簇数。 nprobe 控制搜索时考虑的簇的数量,从而平衡查询精度和查询速度:

    • 增大 nprobe 可以搜索更多簇,返回更多候选向量,提高结果的精确度,但查询时间也会增加。

    • 减少 nprobe 可以缩小搜索范围,降低计算时间,查询速度更快,但可能会牺牲一些精度

3、IVF_SQ8

(1)介绍

IVF_SQ8:是在 IVF_FLAT 基础上增加了量化步骤的一种索引方法,其核心思想与 IVF_FLAT 类似;IVF_SQ8通过 标量量化 (Scalar Quantization)将每个维度的 4 字节浮点数表示压缩为 1 字节整数表示。

核心思想:压缩

(2)关键技术

IVF_SQ8包含两个关键的技术:

1:Inverted File Index:继承了IVF_FLAT的聚类搜索框架

2:Scalar Quantization to 8 bit:新增的标量量化技术,将数据压缩到原来的1/4;

量化过程:

0.0,0.2\]-\>映射到整数0 \[0.2,0.4\]-\>映射到整数1 \[0.4,0.6\]-\>映射到整数2 \[0.6,0.8\]-\>映射到整数3 \[0.8,1.0\]-\>映射到整数4 映射过程: 如:0.9落在\[0.8,1.0\]之间,映射为4 ... 压缩效益:存储空间变为原来的1/4

4、IVF_PQ

(1)介绍

IVF_PQ:是一种高效的向量索引方式,结合了倒排文件索引乘积量化(Product Quantization)技术,旨在加速大规模高维数据集的检索。

  • 倒排文件索引:IVF_PQ首先将数据集划分为多个簇,每个簇由一个聚类中心表示。查询时,系统首先计算查询向量与这些聚类中心的距离,选择最接近的几个簇进行详细搜索,从而减少计算量。

  • 乘积量化:在每个簇内,向量被进一步量化为多个子向量,这些子向量通过独立的量化过程进行编码。这样可以显著降低存储需求,并加快相似度计算。

  • 存储与速度:IVF_PQ通过减少存储空间的占用,同时保持较高的查询速度和准确性,适用于处理大规模高维向量数据。

(2)工作原理介绍
第一部分:场景与数据定义

我们先来定义一个非常具体、简化的问题场景,以便后续逐步推演。

任务:从 12 个 4 维向量中,找与查询向量 ( Q ) 最相似的 3 个向量。

数据表示 :每张图片由一个 4维向量 表示。这显然不现实,但低维度能让我们在演示时看清所有计算细节。

数据集(12 个 4 维向量):

  • V1: [1.0, 2.0, 1.5, 3.0]

  • V2: [1.2, 1.8, 1.6, 2.9]

  • V3: [9.0, 8.0, 8.5, 7.0]

  • V4: [9.1, 8.2, 8.4, 7.2]

  • V5: [2.0, 1.0, 3.0, 1.5]

  • V6: [2.1, 1.1, 3.1, 1.4]

  • V7: [8.0, 9.0, 7.0, 8.5]

  • V8: [8.2, 9.1, 7.1, 8.6]

  • V9: [1.5, 2.5, 1.0, 2.5]

  • V10: [9.5, 8.5, 9.0, 7.5]

  • V11: [2.5, 1.5, 2.5, 1.0]

  • V12: [8.5, 9.5, 7.5, 9.0]

查询向量:( Q: [1.1, 2.1, 1.4, 2.8] )(预期与 ( V_1, V_2, V_9 ) 最相似)。

面临的挑战:即使只有12个向量,为了找到最近邻,暴力搜索需要计算12次距离。如果向量是100万、1000万,甚至10亿个呢?我们需要IVF_PQ。

第二部分:倒排文件索引(IVF)------ 分簇粗筛

1. 核心思想回顾:分簇,建立"书架"。

2. 具体步骤演示:

  • 步骤1:聚类

    • 我们使用k-means算法,将12个向量聚成3个簇 (nlist = 3)。

    • 经过聚类计算后(过程略),我们假设得到了3个聚类中心(即每个"书架"的标签):

      • 簇0中心 C0: [1.5, 2.0, 1.5, 2.5] (这个中心可能靠近 V1, V2, V5, V6, V9, V11)

      • 簇1中心 C1: [9.0, 8.5, 8.0, 7.5] (这个中心可能靠近 V3, V4, V7, V8, V10, V12)

      • 簇2中心 C2: [5.0, 5.0, 5.0, 5.0] (这个中心在中间,但可能没有向量离它最近)

    • 分配向量:计算每个向量到三个中心的距离,将其分配到最近的簇。

      • 簇0:V1, V2, V5, V6, V9, V11

      • 簇1:V3, V4, V7, V8, V10, V12

      • 簇2:(空)

  • 步骤2:建立倒排索引

    • 现在我们有了一个"图书目录":

      • 簇0 包含:V1, V2, V5, V6, V9, V11

      • 簇1 包含:V3, V4, V7, V8, V10, V12

3. 查询过程(IVF阶段):

  • 接收查询向量 Q: [1.1, 2.1, 1.4, 2.8]

  • 粗筛 - 选择候选簇

    • 计算Q到C0、C1、C2的距离(例如欧氏距离)。

    • distance(Q, C0) = sqrt((1.1-1.5)^2 + (2.1-2.0)^2 + (1.4-1.5)^2 + (2.8-2.5)^2) ≈ 0.5(很小)

    • distance(Q, C1) ≈ 很大

    • distance(Q, C2) ≈ 很大

    • 我们设定 nprobe = 1,即只搜索距离最近的一个簇。

    • 结果 :我们选择簇0作为候选簇。搜索范围瞬间从12个向量缩小到了6个向量(V1, V2, V5, V6, V9, V11)。

IVF阶段小结 :我们通过快速的"簇间比较",排除了完全不相关的簇1(V3, V4, V7, V8, V10, V12),避免了在这6个向量上浪费计算资源。

第三部分:乘积量化(PQ)------ 簇内精算

这是最需要详细解释的部分。现在,我们只在簇0内部对那6个向量进行PQ量化。

1. 核心思想回顾:分割向量,分段量化,用编码代替向量。

2. 具体步骤演示:

  • 步骤1:分割向量

    • 我们的向量是4维。我们将其切分成 2段 (m = 2),每段2维。

    • 例如,向量 V1 [1.0, 2.0, 1.5, 3.0] 被切分为:

      • 子向量1: [1.0, 2.0]

      • 子向量2: [1.5, 3.0]

  • 步骤2:为每个子空间创建码本

    • 我们为每个子空间独立进行聚类,每个子空间聚成 2类 (k = 2)。这意味着每个子码本只有2个码字。

    • 针对簇0的所有6个向量进行操作

      • 第一个子空间(所有向量的前两维):

        • 数据点: [1.0,2.0], [1.2,1.8], [2.0,1.0], [2.1,1.1], [1.5,2.5], [2.5,1.5]

        • 聚类后得到2个码字(中心点):

          • 码本1 - 码字0: [1.5, 2.0] (代表[1.0,2.0], [1.2,1.8], [1.5,2.5] 这组的中心)

          • 码本1 - 码字1: [2.2, 1.2] (代表[2.0,1.0], [2.1,1.1], [2.5,1.5] 这组的中心)

      • 第二个子空间(所有向量的后两维):

        • 数据点: [1.5,3.0], [1.6,2.9], [3.0,1.5], [3.1,1.4], [1.0,2.5], [2.5,1.0]

        • 聚类后得到2个码字:

          • 码本2 - 码字0: [1.5, 2.8] (代表[1.5,3.0], [1.6,2.9], [1.0,2.5] 的中心)

          • 码本2 - 码字1: [2.8, 1.3] (代表[3.0,1.5], [3.1,1.4], [2.5,1.0] 的中心)

  • 步骤3:编码向量

    • 现在,我们用码本为簇0里的每个向量生成一个编码(一个由码字索引组成的序列)。

    • V1 [1.0, 2.0, 1.5, 3.0]:

      • 子向量1 [1.0,2.0] -> 离码本1的码字0 [1.5,2.0]更近 -> 索引0

      • 子向量2 [1.5,3.0] -> 离码本2的码字0 [1.5,2.8]更近 -> 索引0

      • V1的PQ编码是 [0, 0]

    • V2 [1.2, 1.8, 1.6, 2.9]:

      • 子向量1 [1.2,1.8] -> 离码字0 [1.5,2.0]更近 -> 索引0

      • 子向量2 [1.6,2.9] -> 离码字0 [1.5,2.8]更近 -> 索引0

      • V2的PQ编码是 [0, 0] (注意,V1和V2被编码成了完全相同的形式!因为它们本来就很相似)

    • V5 [2.0, 1.0, 3.0, 1.5]:

      • 子向量1 [2.0,1.0] -> 离码字1 [2.2,1.2]更近 -> 索引1

      • 子向量2 [3.0,1.5] -> 离码字1 [2.8,1.3]更近 -> 索引1

      • V5的PQ编码是 [1, 1]

    • ... 以此类推编码完所有向量。

存储收益:原始一个向量是4个float = 16字节。PQ编码后是2个索引,每个索引只需1字节(因为只有0和1两种可能)= 2字节。压缩了8倍!

3. 查询过程(PQ阶段)- 非对称距离计算

这是最精妙的一步。我们现在要查询Q [1.1, 2.1, 1.4, 2.8] 与簇0内6个向量的距离。

  • 步骤1:预处理查询向量,构建距离表

    • 同样分割Q: 子向量Q1 [1.1,2.1], 子向量Q2 [1.4,2.8]

    • 计算距离表:我们预先计算Q的每一段与对应码本中所有码字的距离。

      • 子表1 (针对第一段):

        • d(Q1, 码本1-码字0 [1.5,2.0])= sqrt((1.1-1.5)^2 + (2.1-2.0)^2) ≈ sqrt(0.16+0.01) ≈ 0.41

        • d(Q1, 码本1-码字1 [2.2,1.2])= sqrt((1.1-2.2)^2 + (2.1-1.2)^2) ≈ sqrt(1.21+0.81) ≈ 1.42

      • 子表2 (针对第二段):

        • d(Q2, 码本2-码字0 [1.5,2.8])= sqrt((1.4-1.5)^2 + (2.8-2.8)^2) = 0.1

        • d(Q2, 码本2-码字1 [2.8,1.3])= sqrt((1.4-2.8)^2 + (2.8-1.3)^2) ≈ sqrt(1.96+2.25) ≈ 2.05

    • 现在我们有了一个神奇的距离表

      bash 复制代码
      段1: [ 0.41, 1.42 ]
      段2: [ 0.10, 2.05 ]
  • 步骤2:查表计算近似距离

    • 现在计算Q和V1的距离。V1的PQ编码是 [0, 0]。

    • 近似距离 = 子表1[0] + 子表2[0] = 0.41 + 0.10 = 0.51

    • 计算Q和V2的距离。V2的编码也是 [0, 0] -> 距离也是 0.51

    • 计算Q和V5的距离。V5的编码是 [1, 1]。

    • 近似距离 = 子表1[1] + 子表2[1] = 1.42 + 2.05 = 3.47

  • 步骤3:排序并返回结果

    • 我们对簇0内所有6个向量完成上述查表计算后,得到距离列表,取最小的3个:

      1. V1/V2: ~0.51

      2. V9: (假设其编码也是[0,0]或类似,距离也会很小)

      3. ...

    • 最终返回Top3结果:V1, V2, V9。

PQ阶段小结 :我们通过预先计算好的距离表简单的查表加法,替代了原始的浮点距离计算。虽然得到的是近似距离,但排序结果与真实情况一致,而计算速度却快了几个数量级。

第四部分:结果排序与总结

IVF_PQ的整体工作流就像一场高效的搜捕行动:

  1. IVF(确定搜索区域):警长根据线报(查询向量),在地图上划出最可能的几个街区(候选簇),而不是全城搜捕。

  2. PQ(快速盘查):在目标街区内,警察不是拿着一张详细的照片去比对每个人(计算原始向量距离),而是拿着一个"特征清单"(距离表),快速核对身高段、发型段等编码特征(查表计算),迅速锁定嫌疑人(最相似向量)。

关键参数与权衡

  • nlist(簇数量):越大,每个簇越小,IVF粗筛越精确,但聚类和存储索引开销越大。

  • nprobe(搜索的簇数):越大,精度越高,速度越慢。是速度与精度之间最直接的调节旋钮

  • m(分段数):越多,量化越精细,精度越高,但距离表越大,计算量略有增加。

  • k(每段码本大小):越大,量化误差越小,精度越高,但存储每个索引所需的字节数越多(log2(k) bits)。

5、HNSW

HNSW是数据世界的智能导航系统,专门解决高维空间中的海量数据相似性搜索设计的。

核心思想:分层导航

将HNSW想象成一个多层次的地图:

  • 顶层:世界地图,只显示大洲和主要的国家,节点稀疏,用于宏观定位

  • 中层:国家地图,显示主要城市和高速公路,节点密集增加

  • 底层:市区详图,显示所有街道和建筑,节点最密集,用于精确定位

在一个图书馆中,请找出所有与"人工智能理论"相关书籍,怎么实现?

HNSW类似于一个分层导航系统,先在顶层的"总索引图"上快速的定位到一个区域,比如定位到"计算机科学"区,然后到下一层"人工智能"分区,再精确到"AI理论"书架,最后在书架上仔细的比较,整个过程需要几分钟

(二)相似度度量

在 Milvus 中,相似度度量用于衡量向量之间的相似性。选择一个好的距离度量方法可以显著提高分类和聚类的性能。根据输入数据的形式,选择特定的相似度度量方法可以获得最优的性能。

对于浮点嵌入,通常使用以下指标:

  • 欧几里得距离(L2)

  • 内积(IP)

  • 余弦相似度 (COSINE)

1、欧式距离

定义:计算向量在欧几里得空间中的直线距离,即两点间的几何距

特点:

  • 值越小越相似(距离越近)

  • 对向量的绝对大小敏感,适用于需要衡量物理距离的场景,比如图像特征匹配

2、内积

定义:计算两个向量的点积,反应他们的对齐程度

特点:

  • 值越大表示越相似

  • 对向量的模长敏感,模长大的向量容易获得更高的内积,可能需要对向量进行归一化

场景:推荐系统(用户-物品向量匹配),语义相似性计算

3、余弦相似度

定义:衡量向量方向的相似性,忽略模长的影响

特点:

  • 值范围在【-1,1】,1表示完全相同方向,-1表示相反方向

场景:自然语言处理(文本相似度),人脸识别等

五、Milvus数据库操作

我们使用 Milvus Lite,它是pymilvus 中包含的一个 python 库,可以嵌入到客户端应用程序中。Milvus 还支持在DockerKubernetes上部署,适用于生产用例。

开始之前,请确保本地环境中有 Python 3.8+ 可用。安装pymilvus ,其中包含 python 客户端库和 Milvus Lite:

bash 复制代码
pip install pymilvus

(一)设置向量数据库

要创建本地的 Milvus 向量数据库,只需实例化一个MilvusClient ,指定一个存储所有数据的文件名,如 "milvus_demo.db"。

python 复制代码
# 1 数据库的操作
def operate_db():
    # 如果uri为数据库名称路径,代表本地操作数据库
    client = MilvusClient(uri="milvus_demo.db")
    # 如果uri为链接地址,代表Milvus属于单机服务,需要开启Milvus后台服务操作
    # client = MilvusClient(uri="http://localhost:19530")
    # # 创建名称为milvus_demo的数据库
    # 
    # databases = client.list_databases()
    # if "milvus_demo" not in databases:
    #     client.create_database(db_name="milvus_demo")
    # else:
    #     client.using_database(db_name="milvus_demo")
    return client

(二)Collections操作

在 Milvus 中,我们需要一个 Collections 来存储向量及其相关元数据。你可以把它想象成传统 SQL 数据库中的表格。创建 Collections 时,可以定义 Schema 和索引参数来配置向量规格,如维度、索引类型和远距离度量。此外,还有一些复杂的概念来优化索引以提高向量搜索性能。

python 复制代码
# 2 collection集合的操作
def operate_table():
    # 定义schema
    ## 注意:在定义集合 Schema 时,enable_dynamic_field=True 使得您可以插入未定义的字段。一般动态字段以 JSON 格式存储,通常命名为 $meta。在插入数据时,所有未定义的字段及其值将被保存为键值对。
    ## 在定义集合 Schema 时,auto_id=True 可以对主键自动增长id。
    schema = client.create_schema(auto_id=False, enable_dynamic_field=True)
    # # schema添加字段id, vector
    schema.add_field(field_name='id', datatype=DataType.INT64, is_primary=True)
    schema.add_field(field_name='vector', datatype=DataType.FLOAT_VECTOR, dim=5)
    schema.add_field(field_name='scalar1', datatype=DataType.VARCHAR, max_length=256, description='标量字段')
​
    # # 创建集合
    client.create_collection(collection_name='demo_v1', schema=schema)
    # # 设置索引
    index_params = client.prepare_index_params()
    # # 在向量字段vector上面添加一个索引;
    # index_type='',  # 留空以使用自动索引
    # 对于向量字段,常见的默认索引类型包括IVF_FLAT或HNSW等,具体取决于数据的特性和查询需求。
    # 对于标量字段,常见的默认索引可能是INVERTED等。
    index_params.add_index(field_name='vector', metric_type="COSINE", index_type='', index_name="vector_index")
    client.create_index(collection_name='demo_v1', index_params=index_params)
    #
    # 查看索引信息
    res = client.list_indexes(collection_name='demo_v1')
    print(f'索引信息--》{res}')
​
    res = client.describe_index(collection_name='demo_v1', index_name='vector_index')
    print(f'指定索引详细信息-->{res}')
​
    # 查看索引状态
    # client.load_collection(collection_name='demo_v1')
    # print(client.get_load_state(collection_name='demo_v1'))
    # 如果不需要索引,可以删除相关索引
    # client.release_collection(collection_name='demo_v1')
    # client.drop_index(collection_name='demo_v1', index_name='vector_index')
​
    # 检索标量字段
    index_params1 = client.prepare_index_params()
    index_params1.add_index(field_name='scalar1', index_type='', index_name='default_index')
    client.create_index(collection_name='demo_v1', index_params=index_params1)
    #
    # 查看索引信息
    res = client.list_indexes(collection_name='demo_v1')
    print(f'索引信息--》{res}')
    #
    res = client.describe_index(collection_name='demo_v1', index_name='vector_index')
    print(f'指定索引详细信息-->{res}')

(三)Entity实体数据操作

在 Milvus 中,实体指的是Collections中共享相同Schema 的数据记录,行中每个字段的数据构成一个实体。因此,同一 Collections 中的实体具有相同的属性(如字段名称、数据类型和其他约束)

1、数据的增、删、改

python 复制代码
def operate_entity():
    # # todo:1. 创建集合collection
    # 这种方式: collection 只包括两个字段. id 作为主键, vector 作为向量字段,以及自动设置 auto_id、enable_dynamic_field 为 True
    # auto_id 启用此设置可确保主键自动递增。在数据插入期间无需手动提供主键。
    # enable_dynamic_field 启用后,要插入的数据中除 id 和 vector 之外的所有字段都将被视为动态字段。
    # # 这些附加字段作为键值对保存在名为 $meta 的特殊字段中。此功能允许在数据插入期间包含额外的字段。
    # client.create_collection(collection_name='demo_v2', dimension=5, metric_type='IP')
    #
    # # todo:2. 插入数据(也叫实体)
    data = [
        {"id": 0, "vector": [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354,
                             0.9029438446296592], "color": "pink_8682"},
        {"id": 1, "vector": [0.19886812562848388, 0.06023560599112088, 0.6976963061752597, 0.2614474506242501,
                             0.838729485096104], "color": "red_7025"},
        {"id": 2, "vector": [0.43742130801983836, -0.5597502546264526, 0.6457887650909682, 0.7894058910881185,
                             0.20785793220625592], "color": "orange_6781"},
        {"id": 3, "vector": [0.3172005263489739, 0.9719044792798428, -0.36981146090600725, -0.4860894583077995,
                             0.95791889146345], "color": "pink_9298"},
        {"id": 4, "vector": [0.4452349528804562, -0.8757026943054742, 0.8220779437047674, 0.46406290649483184,
                             0.30337481143159106], "color": "red_4794"},
        {"id": 5, "vector": [0.985825131989184, -0.8144651566660419, 0.6299267002202009, 0.1206906911183383,
                             -0.1446277761879955], "color": "yellow_4222"},
        {"id": 6, "vector": [0.8371977790571115, -0.015764369584852833, -0.31062937026679327, -0.562666951622192,
                             -0.8984947637863987], "color": "red_9392"},
        {"id": 7, "vector": [-0.33445148015177995, -0.2567135004164067, 0.8987539745369246, 0.9402995886420709,
                             0.5378064918413052], "color": "grey_8510"},
        {"id": 8, "vector": [0.39524717779832685, 0.4000257286739164, -0.5890507376891594, -0.8650502298996872,
                             -0.6140360785406336], "color": "white_9381"},
        {"id": 9, "vector": [0.5718280481994695, 0.24070317428066512, -0.3737913482606834, -0.06726932177492717,
                             -0.6980531615588608], "color": "purple_4976"}
    ]
    res = client.insert(collection_name='demo_v2', data=data)
    # print(res)
​
    ## todo:2.1 将数据插入到特定分区,可以在插入请求中指定分区名称,如下所示:
    data = [
        {"id": 10, "vector": [-0.5570353903748935, -0.8997887893201304, -0.7123782431855732, -0.6298990746450119,
                              0.6699215060604258], "color": "red_1202"},
        {"id": 11, "vector": [0.6319019033373907, 0.6821488267878275, 0.8552303045704168, 0.36929791364943054,
                              -0.14152860714878068], "color": "blue_4150"},
        {"id": 12, "vector": [0.9483947484855766, -0.32294203351925344, 0.9759290319978025, 0.8262982148666174,
                              -0.8351194181285713], "color": "orange_4590"},
        {"id": 13, "vector": [-0.5449109892498731, 0.043511240563786524, -0.25105249484790804, -0.012030655265886425,
                              -0.0010987671273892108], "color": "pink_9619"},
        {"id": 14, "vector": [0.6603339372951424, -0.10866551787442225, -0.9435597754324891, 0.8230244263466688,
                              -0.7986720938400362], "color": "orange_4863"},
        {"id": 15, "vector": [-0.8825129181091456, -0.9204557711667729, -0.935350065513425, 0.5484069690287079,
                              0.24448151140671204], "color": "orange_7984"},
        {"id": 16, "vector": [0.6285586391568163, 0.5389064528263487, -0.3163366239905099, 0.22036279378888013,
                              0.15077052220816167], "color": "blue_9010"},
        {"id": 17, "vector": [-0.20151825016059233, -0.905239387635804, 0.6749305353372479, -0.7324272081377843,
                              -0.33007998971889263], "color": "blue_4521"},
        {"id": 18, "vector": [0.2432286610792349, 0.01785636564206139, -0.651356982731391, -0.35848148851027895,
                              -0.7387383128324057], "color": "orange_2529"},
        {"id": 19, "vector": [0.055512329053363674, 0.7100266349039421, 0.4956956543575197, 0.24541352586717702,
                              0.4209030729923515], "color": "red_9437"}
    ]
​
    # ##  todo:3. 创建分区
    client.create_partition(collection_name='demo_v2', partition_name='partitionA')
    #
    # # # # todo: 3.1 分区中插入数据
    res = client.insert(collection_name='demo_v2', data=data, partition_name='partitionA')
    # print(res)
    ## todo:4. 更新插入数据
    # 在 Milvus 中,upsert 操作执行数据级操作,根据集合中是否已存在主键来插入或更新实体。具体来说:
    # 如果集合中已存在该实体的主键,则现有实体将被覆盖。
    # 如果集合中不存在主键,则将插入一个新实体。
    data = [
        {"id": 0, "vector": [-0.619954382375778, 0.4479436794798608, -0.17493894838751745, -0.4248030059917294,
                             -0.8648452746018911], "color": "black_9898"},
        {"id": 1, "vector": [0.4762662251462588, -0.6942502138717026, -0.4490002642657902, -0.628696575798281,
                             0.9660395877041965], "color": "red_7319"},
        {"id": 2, "vector": [-0.8864122635045097, 0.9260170474445351, 0.801326976181461, 0.6383943392381306,
                             0.7563037341572827],"color": "white_6465"},
        {"id": 3, "vector": [0.14594326235891586, -0.3775407299900644, -0.3765479013078812, 0.20612075380355122,
                             0.4902678929632145], "color": "orange_7580"},
        {"id": 4, "vector": [0.4548498669607359, -0.887610217681605, 0.5655081329910452, 0.19220509387904117,
                             0.016513983433433577], "color": "red_3314"},
        {"id": 5, "vector": [0.11755001847051827, -0.7295149788999611, 0.2608115847524266, -0.1719167007897875,
                             0.7417611743754855], "color": "black_9955"},
        {"id": 6, "vector": [0.9363032158314308, 0.030699901477745373, 0.8365910312319647, 0.7823840208444011,
                             0.2625222076909237], "color": "yellow_2461"},
        {"id": 7, "vector": [0.0754823906014721, -0.6390658668265143, 0.5610517334334937, -0.8986261118798251,
                             0.9372056764266794], "color": "white_5015"},
        {"id": 8, "vector": [-0.3038434006935904, 0.1279149203380523, 0.503958664270957, -0.2622661156746988,
                             0.7407627307791929], "color": "purple_6414"},
        {"id": 9, "vector": [-0.7125086947677588, -0.8050968321012257, -0.32608864121785786, 0.3255654958645424,
                             0.26227968923834233], "color": "brown_7231"}
    ]
​
    res = client.upsert(collection_name='demo_v2', data=data)
    # print(res)
    # 注意如果分区中不存在更新数据的id,就不会受影响,但是会影响集合里已经存在的相同id的实体
    # res = client.upsert(collection_name='demo_v2', data=data, partition_name="partitionA")
    # todo:5. 删除实体(数据)
    # 按照过滤器删除;如果不指定分区,默认情况下会在整个集合中进行删除
    res = client.delete(collection_name='demo_v2', filter='id in [12, 5, 6]')
    print(res)
    # 按照id进行删除;指定分区删除数据
    # res = client.delete(collection_name='demo_v2', ids=[1, 2, 3, 4], partition_name='partitionA')
    print(res)

2、数据的查询

(1)简单查询
python 复制代码
# entity实体数据的操作:查询
def query_operation():
    # # todo: 1. 单向量搜索
    res = client.search(collection_name='demo_v2',
                        data=[[0.19886812562848388, 0.06023560599112088, 0.6976963061752597, 0.2614474506242501, 0.838729485096104]],
                        limit=2,
                        search_params={"metric_type": "IP"},
                        output_fields=["id", 'vector']) # search_params是在查询时执行距离计算方式,如果定义索引的时候,已经制定了方式可以不写
    print(res)
    # todo: 2. 批量向量搜索
    res = client.search(collection_name='demo_v2',
                        data=[[0.19886812562848388, 0.06023560599112088, 0.6976963061752597, 0.2614474506242501, 0.838729485096104],
                              [0.3172005263489739, 0.9719044792798428, -0.36981146090600725, -0.4860894583077995, 0.95791889146345]],
                        limit=2,
                        search_params={"metric_type": "IP"},
                        output_fields=["id", 'vector']) # search_params是在查询时执行距离计算方式,如果定义索引的时候,已经制定了方式可以不写
    print(res)
    # todo: 3. 分区搜索
    # 要进行分区搜索,只需在搜索请求的 partition_names 中包含目标分区的名称即可。这指定search操作仅考虑指定分区内的向量。
    res = client.search(
        collection_name="demo_v2",
        data=[[0.02174828545444263, 0.058611125483182924, 0.6168633415965343, -0.7944160935612321, 0.5554828317581426]],
        limit=5,
        search_params={"metric_type": "IP", "params": {}},
        partition_names=["partitionA"]  # 这里指定搜索的分区
    )
    print(res)
    # todo: 4.使用输出字段进行搜索
    # 使用输出字段进行搜索允许您指定搜索结果中应包含匹配向量的哪些属性或字段。
    res = client.search(
        collection_name="demo_v2",
        data=[[0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]],
        limit=5,
        search_params={"metric_type": "IP", "params": {}},
        output_fields=['vector', "color"]  # 返回定义的字段
    )
    print(res)
    # todo: 5.过滤搜索
    # 过滤器搜索:筛选搜索将标量筛选器应用于矢量搜索,允许我们根据特定条件优化搜索结果。
    # 例如,要根据字符串模式优化搜索结果,可以使用 like 运算符。此运算符通过考虑前缀、中缀和后缀来启用字符串匹配:
    # 筛选颜色以红色为前缀的结果:
    res = client.search(
        collection_name="demo_v2",
        data=[[0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]],
        limit=5,
        search_params={"metric_type": "IP", "params": {}},
        output_fields=["color"],
        filter='color like "red%"'
    )
    print(res)
    # todo: 6.范围搜索
    # 范围搜索允许查找距查询向量指定距离范围内的向量。
    # 范围搜索:radius:定义搜索空间的外边界。只有距查询向量在此距离内的向量才被视为潜在匹配。
    # range_filter:虽然radius设置搜索的外部限制,但可以选择使用range_filter来定义内部边界,创建一个距离范围,在该范围内向量必须落下才被视为匹配。
    search_params = {
        "metric_type": "IP",
        "params": {
            "radius": 0.8,  # 搜索圆的半径
            "range_filter": 1  # 范围过滤器,用于过滤出不在搜索圆内的向量。
        }
    }
​
    res = client.search(
        collection_name="demo_v2",
        data=[[0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]],
        limit=3,  # 返回的搜索结果最大数量
        search_params=search_params,
        output_fields=["color"],
    )
    #
    #
    print(res)
    result = json.dumps(res, indent=4)
    print(result)
(2)复杂查询
  • 混合检索:要对两组 ANN 搜索结果进行合并和重新排序,有必要选择适当的重新排序策略。支持两种重排策略:加权排名策略(WeightedRanker )和重排序策略RRFRanker)。在选择重排策略时,需要考虑的一个问题是,在向量场中是否需要强调一个或多个基本 ANN 搜索。
  • 加权排名:如果您要求结果强调特定的向量场,建议使用该策略。通过 WeightedRanker,您可以为某些向量场分配更高的权重,从而更加强调这些向量场。例如,在多模态搜索中,图片的文字描述可能比图片的颜色更重要。

    • 使用 WeightedRanker 策略时,需要在WeightedRanker 函数中输入权重值。混合搜索中的基本 ANN 搜索次数与需要输入的值的次数相对应。输入值的范围应为 [0,1],数值越接近 1 表示重要性越高。

      python 复制代码
      from pymilvus import WeightedRanker
      rerank= WeightedRanker(0.8, 0.3) 
  • RRFRanker(倒数排序融合):在没有特定重点的情况下,建议采用这种策略。RRF 可以有效平衡每个向量场的重要性。

    • RRFRanker的核心思想是根据每个结果在其检索列表中的排名位置来计算分数。具体而言,算法使用以下公式为每个结果分配分数:
  • d:表示文档。

  • N:表示不同检索路径的数量。

  • ranki(d):表示文档 d 在第 i 个检索器中的排名位置,从0开始计数。

  • k:是一个平滑参数,用于控制随着排名增加分数的降低速度。默认值通常设置为60。

  • 使用 RRFRanker 策略时,需要将参数值k 输入 RRFRanker。k 的默认值为 60。该参数有助于确定如何组合来自不同 ANN 搜索的排名,目的是平衡和混合所有搜索的重要性

    python 复制代码
    from pymilvus import RRFRanker
    ​
    ranker = RRFRanker(100)

代码实现:

python 复制代码
def complex_query():
    # # 定义schema
​
    schema = client.create_schema(enable_dynamic_field=False)
    schema.add_field(field_name='film_id', datatype=DataType.INT64, is_primary=True)
    schema.add_field(field_name='filmVector', datatype=DataType.FLOAT_VECTOR, dim=5) # 向量字段
    schema.add_field(field_name="posterVector", datatype=DataType.FLOAT_VECTOR, dim=5) # 向量字段
    # #
    # 定义索引
    index_params = client.prepare_index_params()
    index_params.add_index(field_name='filmVector', index_type="IVF_FLAT",
                           metric_type="L2", params={"nlist": 128})
    index_params.add_index(field_name='posterVector', index_type="",
                           metric_type="COSINE")
​
    # 创建集合
    client.create_collection(collection_name='demo_v3', schema=schema, index_params=index_params)
​
    # 向量库中插入实体
    entities = []
    for  _ in range(1000):
        # 构造实体
        film_id = random.randint(1, 10000)
        film_vector = [random.random() for _ in range(5)]
        poster_vector = [random.random() for _ in range(5)]
        entity = {"film_id": film_id, "filmVector": film_vector, "posterVector": poster_vector}
        entities.append(entity)
​
    client.insert(collection_name='demo_v3', data=entities)
​
    # 多向量查询(注意和批量向量查询不同)
    # 多向量搜索使用 hybrid_search() API 在一次调用中执行多个 ANN 搜索请求。每个 AnnSearchRequest 代表特定矢量场上的单个搜索请求。
    # 示例创建两个 AnnSearchRequest 实例以对两个向量字段执行单独的相似性搜索。
    # 创建多搜索请求 filmVector
    query_filmVector = [[0.8896863042430693, 0.370613100114602, 0.23779315077113428, 0.38227915951132996, 0.5997064603128835]]
    dense_search_params = {"data": query_filmVector,
                           "anns_field": "filmVector",# 该参数值必须与集合模式中使用的值相同。
                           "param": {"metric_type": "L2", "nprobe": 10},# nprobe代表访问簇的数量
                           "limit": 2}
    request_1 = AnnSearchRequest(**dense_search_params)
​
    # 创建多搜索请求 posterVector
    query_posterVector = [[0.02550758562349764, 0.006085637357292062, 0.5325251250159071, 0.7676432650114147, 0.5521074424751443]]
    sparse_search_params = {"data": query_posterVector,
                            "anns_field": "posterVector",
                            # 该参数值必须与集合模式中使用的值相同。
                            "param": {"metric_type": "COSINE"},
                            "limit": 2
                            }
    request_2 = AnnSearchRequest(**sparse_search_params)
​
    reqs = [request_1, request_2]
    ranker = RRFRanker(100)
​
    res = client.hybrid_search(
        collection_name="demo_v3",
        reqs=reqs,
        ranker=ranker,
        limit=2
    )
    for hits in res:
        print("TopK results:")
        for hit in hits:
            print(hit)

(四)加载现有数据

由于 Milvus Lite 的所有数据都存储在本地文件中,因此即使在程序终止后,你也可以通过创建一个带有现有文件的MilvusClient ,将所有数据加载到内存中。例如,这将恢复 "milvus_demo.db "文件中的 Collections,并继续向其中写入数据。

python 复制代码
from pymilvus import MilvusClient
​
client = MilvusClient("milvus_demo.db")

(五)删除Collections

如果想删除某个 Collections 中的所有数据,可以通过以下方法丢弃该 Collections

python 复制代码
# Drop collection
client.drop_collection(collection_name="demo_collection")
相关推荐
安思派Anspire3 小时前
AI智能体:完整课程(初级)
aigc·openai·agent
stark张宇6 小时前
别掉队!系统掌握 LLM 应用开发,这可能是你今年最值得投入的学习方向
人工智能·llm·agent
大模型教程6 小时前
全网首发!清北麻省顶级教授力荐的《图解大模型》中文版终于来了,碾压 95% 同类教材
程序员·llm·agent
用户307140958487 小时前
📢 深度解析 Dify 核心 LLM 提示模板库,揭秘 AI 交互的「幕后魔法」
人工智能·llm·agent
AI大模型7 小时前
谷歌 Agents 白皮书中文版全网首发,堪称 AI 教材的天花板级神作
程序员·llm·agent
houyhea7 小时前
AI智能体浪潮下的前端演进:一场螺旋上升的轮回
前端·agent
Elwin Wong8 小时前
本地运行LangChain Agent用于开发调试
人工智能·langchain·大模型·llm·agent·codingagent
FreeCode9 小时前
智能体设计模式解析:ReAct模式
设计模式·langchain·agent