大规模向量检索场景下的数据管理(下篇)

 

错过上篇的同学可以点击标题回顾

 

三、Milvus 是什么?

Milvus 是 一款开源的、针对海量特征向量的相似性搜索引擎。Milvus能够很好地应对海量向量数据,它集成了目前在向量相似性计算领域比较知名的几个开源库(Faiss, SPTAG等),通过对数据和硬件算力的合理调度,以获得最优的搜索性能。 

用户只需要从docker hub上下载一个Milvus的最新镜像,一行命令即可启动,然后可以通过Python SDK或者Java SDK进行向量插入以及搜索操作,非常方便。更重要的是,Milvus是开源的!这意味着用户可以参与到这个产品的开发以及设计中来,把自己的想法带到产品中,打造出更符合自己使用习惯的向量检索数据库。 

向量相似度搜索引擎 Milvus 架构图

640?wx_fmt=png

四、Milvus 是如何管理数据的?

先来说一些基本概念。

表:向量数据的集合,每条向量必须有一个唯一的ID来区分,每一条向量以及它的ID就是表里的一行数据,每个表里的所有向量必须是同一维度的。

一张维度为10的表的示意图

640?wx_fmt=png

索引:建立索引的过程可以理解为通过某种算法把大批向量分成很多簇。索引需要额外使用磁盘空间,有些类型的索引会使得磁盘占用量翻倍。

用户可以有这些操作:创建表,插入向量,建立索引,搜索向量,获取表信息,获取索引信息,删除表,删除表里部分数据,删除索引,等等。

现在假设我们有一亿条512维度的向量,需要把这些数据管理起来,并且进行向量检索。

1

Insert

向量数据的录入

我们先看向量数据是怎么录入的。我们之前算过每条这样的向量要占据2KB的空间,一亿条就是200GB,显然想一次性导入不现实,写入磁盘的数据文件也不可能只有一个文件,肯定要分成多个文件。插入性能也是主要性能指标之一,Milvus允许批量地插入向量,一次性地插入几百甚至几万条向量都是允许的,对于512维的高维向量,通常可以达到每秒三万条的插入速度。

并不是每次插入向量数据都去写磁盘,系统会给每个表在内存里开辟一块空间作为可写缓冲(mutable buffer),数据可以很快速地直接写入mutable buffer里,当积累到一定数据量之后,这个可写缓冲就会被标记为只读的(immutable buffer),并且会自动开辟新的可写缓冲等待新的数据。immutable buffer会被定时写入磁盘,写入完成后这块内存会被释放,这里的定时写磁盘机制与Elasticsearch类似(Elasticsearch默认是每隔1秒将缓冲数据写入磁盘)。另外,熟悉LevelDB/RocksDB的读者能看出来这里面有MemTable的影子。这样做的目的主要是为了兼顾以下几点:

  1. 数据导入效率高

  2. 数据导入后尽快可见

  3. 数据文件不过于碎片化

640?wx_fmt=png

2

Raw Data File

原始数据文件

数据写入磁盘后 ,成为原始数据文件(Raw Data File),保存的是向量的原始数据。前面我们说过,海量的数据肯定是分散成多个文件来管理的。Milvus有一个配置选项,用来确定每个原始数据文件的大小,默认是1GB,也就是说,一亿条512维向量会被分散成大约200个文件来存储。

插入向量的数据量是可大可小的,用户可能一次插入10条向量,也可能一次插入100万条向量。而写磁盘的操作是间隔一秒持续进行的,并不会等到插入满1GB的数据之后再写磁盘,因此会形成很多大小不一的文件。零碎的文件并不利于管理,也不利于向量检索操作,因此系统会不断地把这些小文件合并,直到合并后的文件大小达到或超过1GB。

考虑增量库的使用场景,同时会有向量插入和向量检索操作,我们需要保证一旦数据落盘,就必须能够被检索到。所以,在小文件被合并完成之前,会使用这些小文件进行检索;一旦合并完成,就会删除这些小文件,从合并之后的文件进行检索。

合并前的检索

640?wx_fmt=png

合并成功后的检索

640?wx_fmt=png

3

Index File

索引文件

上面说的对Raw Data File的检索是一种暴力计算(brute-force search),即用目标向量和原始向量一条一条地计算距离,得出最近的k条向量,这是很低效的。建立索引可以大幅提高检索效率,之前我们说过索引是要占用额外磁盘空间的,而且建立索引也是比较耗时。

原始数据文件和索引文件的格式有何区别?简单来说,原始数据文件就是把n个向量一个接一个地记录下来,包括向量的ID以及向量数据;索引文件则记录了聚类运算后的结果,包括索引的类型,每个簇的中心向量,以及每个簇分别有哪些向量。下图是这两种文件格式上的简化表示:

一张维度为10的表的示意图

640?wx_fmt=png

总的来说索引文件包含的信息比原始数据文件要多,不过有的索引类型对向量数据进行了简化或者压缩,所以总的文件size会小很多。

当用户建了一张新表,这张表的索引类型默认是无索引,检索都是通过暴力计算的方式进行;一旦建立了索引之后,系统会对合并后达到1GB的原始数据文件自动建立索引。建立好索引后,会生成一个新的索引文件,而原来的原始数据文件不会被删掉,以便以后切换成别的索引类型。

系统自动对达到1GB的原始数据文件建立索引

640?wx_fmt=png

1GB的原始数据文件索引完成

640?wx_fmt=png

未达到1GB的文件不会被自动建立索引,会影响检索速度。如果想得到更好的检索效率,就要对该表进行强制建立索引:

640?wx_fmt=png

强制建立索引后检索速度最快

640?wx_fmt=png

4

Meta Data 

数据的信息

前面我们说一亿条512维向量大约会有200个磁盘文件,如果建立了索引,就是大约400个文件,Milvus在运行过程中要修改这些文件的状态,删除文件,创建文件等等,所以就需要一套机制来管理文件的状态和信息,这就是元数据(metadata)。使用OLTP数据库来管理这些信息是一个很好的选择,在单机模式下,Milvus使用SQlite来管理元数据,而在分布式模式下,Milvus使用Mysql来管理元数据。程序启动后,会在SQLite/MySQL中建立两张表,分别叫 Tables和TableFiles。Tables是用来记录全部表的信息,TableFiles则是记录全部数据文件以及索引文件的信息。

如下图所示,Tables元数据信息中包括了表名(table_id),表的向量维度(dimension),创建日期(created_on),状态(state),索引类型(engine_type),聚类的分簇数量(nlist),距离计算方式(metric_type)等。TableFiles则记录了文件所属的表名(table_id),文件的索引类型(engine_type),文件名(file_id),文件类型(file_type),文件大小(file_size),向量行数(row_count),创建日期(created_on)。

640?wx_fmt=png

有了这些元数据,就可以根据元数据来进行各种操作了。

比如如果要对table_2表执行向量检索,Mega Manager先去SQLite/MySQL中做一个查询,实际上就是运行一条SQL语句:SELECT * FROM TableFiles WHERE table_id=’table_2’ ,这样就得到了table_2表所有的文件信息,然后这些文件就会被查询调度器(Query Scheduler)调入内存,就可以进行运算了。

如果要删除table_2表,我们是不能立即把这个表以及它的文件全部删除的,因为这个时候有可能正在执行对这张表的检索,因此删除操作有软删除(soft-delete)和硬删除(hard-delete)。执行了删除表的操作后,该表会被标记为soft-delete状态,之后对该表的检索或修改操作都是不允许的。但删除之前所执行的查询操作仍在进行,只有当所有对该表的查询操作完成之后,该表才会被真正地删除,包括它的元数据信息,包括它的各种文件。

5

Query Scheduler 

查询调度

下图表示了一个有若干文件(包括原始数据文件和索引文件)的表做一次top-k查询时,数据在磁盘、内存、显存中发生拷贝,分别在CPU和GPU进行向量搜索得出最终结果的过程。

640?wx_fmt=png

查询调度算法对性能的影响极为明显,其基本原则是最大程度地利用硬件资源获得最好的查询性能,我们会有专门文章讲解Milvus的查询调度机制。

我们把对某张表的第一次查询称为冷查询,其后的查询叫热查询。当Milvus服务启动后,第一次查询时数据都在硬盘上,需要把数据加载到内存,另外还有部分数据会加载到显存,从硬盘加载数据到内存相对是比较耗时的;而第二次查询时,部分或者全部数据已经在内存里,就省去了读硬盘的时间,查询就会变得很快。

为了避免第一次查询时间过长,Milvus 提供了预加载机制,用户可以指定服务启动后自动加载数据到内存。

对于一亿级别的表(512维),数据量是200GB,好一点的服务器有足够的内存装载全部数据。这种条件下的查询速度是最快的。而对于十亿以上级别的表,查询时就无法避免地要对数据做置换,也就是把使用完的部分数据从内存中释放,换上其他未查询的数据。目前我们使用LRU(Least Recently Used)策略作为数据的置换策略 。

如下图所示,假设某张表有六个索引文件存在服务器磁盘上,该服务器的内存只够装得下三个文件,而显存只能装一个文件。查询开始时,先从磁盘读入三个文件装进内存,对第一个文件查询完成后,把它从内存中释放,同时从磁盘装入第四个文件。同理,第二个文件交给显卡处理,对该文件查询完成后把它从显存中释放,再从内存调入其他文件。

640?wx_fmt=png

6

Result Reducer

结果集的归并

前面我们说向量检索有两个关键参数:一个是n,指n条目标向量;另一个是k,指最相似的前k个向量。对于一次查询来说,结果集是n组key-value键值对,每组键值对有k对键值。一张表包含了多个文件,不管是原始数据文件还是索引文件,都要单独做一次检索。因此,每个文件检索出n组top-k结果集。然后,将多个文件的结果集进行归并,得出全表最接近的top-k。

下图是一个示例,假设某张表有四个索引文件,对其做一个n=2, k=3的查询。示例中结果集的两列数字,左边代表相似向量的编号,右边代表欧氏距离,我们之前已经知道,欧氏距离越小,意味着和目标向量越相似。调度器先分别对四个文件检索得出四组结果集,然后分别两两归并,经过两轮归并后,得到最终的结果集。

640?wx_fmt=png

7

Optimization Potential

可能的优化方向

对于可能的优化方向目前的几个想法:

  • 目前无法检索到 immutable buffer 里的数据,只有等数据落盘了之后才会被检索到,对于有些用户来说,可能会更关心数据的即时可见性,如果能检索到 immutable buffer 甚至 mutable buffer 里的数据,那么数据插入即可见。

  • 提供分区表的功能,使得用户可以根据自己的需求把数据按照一定规则进行分区,可以针对某个分区的数据进行查询。

  • 有些需求希望能够让向量带上属性,还希望能够对属性进行过滤,比如我只想在满足某个属性条件的向量中进行查询。还要能在查询结果中返回向量的属性甚至是向量的原始数据。可能的做法是借助KV数据库(比如RocksDB)来实现。

  • 对于某些无时无刻都有数据流入的场景,数据可能存在老化现象,比如用户可能仅仅关心近一个月以来的数据,大部分查询都是在一个月以内的数据进行查询(我们是允许用户根据时间范围进行查询的)。一个月以前的数据就变得不那么有用了,但是又占用了很多磁盘空间,那么我们希望系统可以把这些数据自动迁移到更便宜的存储空间去,需要的时候再调出来,这是一个对旧数据迁移的机制。

 

通过以上的介绍,大家应该已经初步了解了向量检索的含义以及构建一个能够应对大规模向量检索场景的系统需要应对哪些问题,另外还认识了Milvus这个系统。Milvus的目标是要做成一个完备的分布式向量检索数据库,这里面还有很多涉及功能性、稳定性的事情需要做。这篇文章主要集中介绍了Milvus数据管理的策略,我们另外还有专门的文章介绍Milvus分布式、向量索引算法以及查询调度器的知识。作为一个开源项目,我们恳切邀请对此领域感兴趣的朋友一起构建一个繁荣的社区,使这个产品不断完善。有兴趣的朋友可以访问Milvus的开源项目,或者加入我们的社区进行讨论,以下是相关链接:

Milvus 源码github.com/milvus-io/milvus

Milvus 官网milvus.io

Milvus Slack社区milvusio.slack.com

Milvus CSDN博客zilliz.blog.csdn.net

© 2019 ZILLIZ™

640?wx_fmt=png
发布了19 篇原创文章 · 获赞 15 · 访问量 5255

猜你喜欢

转载自blog.csdn.net/weixin_44839084/article/details/103146014