图为 ZSearch 基础架构负责人十倍 2019 Elastic Dev Day 现场分享html
ElasticSearch(简称 ES)是一个很是受欢迎的分布式全文检索系统,经常使用于数据分析,搜索,多维过滤等场景。蚂蚁金服从2017年开始向内部业务方提供 ElasticSearch 服务,咱们在蚂蚁金服的金融级场景下,总结了很多经验,这次主要给你们分享咱们在向量检索上的探索。git
ElasticSearch 普遍应用于蚂蚁金服内部的日志分析、多维分析、搜索等场景。当咱们的 ElasticSearch 集群愈来愈多,用户场景愈来愈丰富,咱们会面临愈来愈多的痛点:github
为了解决这些痛点,咱们开发了 ZSearch 通用搜索平台:算法
基于 ElasticSearch 的通用搜索平台 ZSearch 日趋完善,用户愈来愈多,场景更加丰富。数组
随着业务的飞速发展,对于搜索的需求也会增长,好比:搜索图片、语音、类似向量。网络
为了解决这个需求,咱们是加入一个向量检索引擎仍是扩展 ElasticSearch 的能力使其支持向量检索呢?架构
咱们选择了后者,由于这样咱们能够更方便的利用 ElasticSearch 良好的插件规范、丰富的查询函数、分布式可扩展的能力。app
接下来,我来给你们介绍向量检索的基本概念和咱们在这上面的实践。框架
向量从表现形式上就是一个一维数组。咱们须要解决的问题是使用下面的公式度量距离寻找最类似的 K 个向量。less
欧式距离:
余弦距离:
汉明距离:
杰卡德类似系数:
由于向量检索场景的向量都是维度很高的,好比256,512位等,计算量很大,因此接下来介绍相应的算法去实现 topN 的类似度召回。
KNN 算法表示的是准确的召回 topK 的向量,这里主要有两种算法,一种是 KDTtree,一种是 Brute Force。咱们首先分析了 KDTree 的算法,发现 KDTree 并不适合高维向量召回,因而咱们实现的 ES 的 Brute Force 插件,并使用了一些 Java 技巧进行加速运算。
简单来说,就是把数据按照平面分割,并构造二叉树表明这种分割,在检索的时候,能够经过剪枝减小搜索次数。
构建树
以二维平面点(x,y)的集合(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)为例:
查找最近点
搜索(3,5)的最近邻:
Lucene 中实现了 BKDTree,能够理解为分块的 KDTree,而且从源码中能够看到 MAX_DIMS = 8,由于 KDTree 的查询复杂度为 O(kn^((k-1)/k)),k 表示维度,n 表示数据量。说明 k 越大,复杂度越接近于线性,因此它并不适合高维向量召回。
顾名思义,就是暴力比对每一条向量的距离,咱们使用 BinaryDocValues 实现了 ES 上的 BF 插件。更进一步,咱们要加速计算,因此使用了 JAVA Vector API 。JAVA Vector API 是在 openJDK project Panama 项目中的,它使用了 SIMD 指令优化。
使用 avx2 指令优化,100w 的 256 维向量,单分片比对,RT 在 260ms,是常规 BF 的 1/6。ElasticSearch 官方在7.3版本也发布了向量检索功能,底层也是基于 Lucene 的 BinaryDocValues,而且它还集成入了 painless 语法中,使用起来更加灵活。
能够看到 KNN 的算法会随着数据的增加,时间复杂度也是线性增加。例如在推荐场景中,须要更快的响应时间,容许损失一些召回率。
ANN 的意思就是近似 K 邻近,不必定会召回所有的最近点。ANN 的算法较多,有开源的 ES ANN 插件实现的包括如下这些:
ZSearch 依据本身的业务场景也开发了 ANN 插件(适配达摩院 Proxima 向量检索引擎的 HNSW 算法)。
Local Sensitive Hashing 局部敏感 hash,咱们能够把向量经过平面分割作 hash。例以下面图例,0表示点在平面的左侧,1表示点在平面的右侧,而后对向量进行屡次 hash,能够看到 hash 值相同的点都比较靠近,因此在 hash 之后,咱们只须要计算 hash 值相似的向量,就能较准确的召回 topK。
PQ 是一种编码,例如图中的128维向量,先把向量分红4份,对每一份数据作 kmeans 聚类,每份聚类出256个聚类中心,这样,原始向量就可使用聚类中心的编号从新编码,能够看出,如今表示一个向量,只须要用4个字节就行。而后固然要记录下聚类中心的向量,它被称之为码本。
PQ 编码压缩后,要取得好的效果,查询量仍是很大,因此前面能够加一层粗过滤,如图,把向量先用 kmeans 聚类成1024个类中心,构成倒排索引,而且计算出每一个原始向量与其中心的残差后,对这个残差数据集进行 PQ 量化。用 PQ 处理残差,而不是原始数据的缘由是残差的方差能量比原始数据的方差能量要小。
这样在查询的时候,咱们先找出查询出靠近查询向量的几个中心点,而后再在这些中心点中去计算 PQ 量化后的 top 向量,最后把过滤出来的向量再作一次精确计算,返回 topN 结果。
讲 HNSW 算法以前,咱们先来说 NSW 算法,以下图,它是一个顺序构建图流程:
查找流程包含在了插入流程中,同样的方式,只是不须要构建边,直接返回结果。
HNSW 算法是 NSW 算法的分层优化,借鉴了 skiplist 算法的思想,提高查询性能,开始先从稀疏的图上查找,逐渐深刻到底层的图。
以上这3类算法都有 ElasticSearch 的插件实现:
LSH 插件 | IVSPQ 插件 | HNSW 插件 | |
---|---|---|---|
概述 | 在建立 index 时传入抽样数据,计算出 hash 函数。写入时增长 hash 函数字段。召回用 minimum_should_match 控制计算量 | 在建立索引时传入聚类点和码本,写入数据就增长聚类点和编码字段,召回先召回编码后距离近的再精确计算 | 扩展 docvalue,在每次生成 segment 前,获取 docvalue 里的原始值,并调用开源 hnsw 库生成索引 |
优势 | 1.实现简单,性能较高;2.无需借助其余 lib 库;3.无需考虑内存; | 1.性能较高;2.召回率高 >90%;3.无需考虑内存; | 1.查询性能最高;2.召回率最高 >95%; |
缺点 | 1.召回率较其余两种算法较差,大概在85%左右;2.召回率受初始抽样数据影响;3.ES 的 metadata很大; | 1.须要提早使用 faiss 等工具预训练;2. ES 的 metadata很大; | 1.在构建的时候,segment 合并操做会消耗巨大的 CPU;2.多 segment 下查询性能会变差;3.全内存; |
咱们根据本身的场景(轻量化输出场景),选择了在 ES 上实现 HNSW 插件。由于咱们用户都是新增数据,更关心 top10 的向量,因此咱们使用了经过 seqNo 去 join 向量检索引擎方式,减小 CPU 的消耗和多余 DocValues 的开销。
对接 Porxima 向量检索框架:
实现 ProximaEngine
写入流程,扩展 ElasticSearch 自己的 InternalEngine,在写完 Lucene 之后,先写 proxima 框架,proxima 框架的数据经过 mmap 方式会直接刷到磁盘,一次请求的最后,Translog 刷入磁盘。就是一次完整的写入请求了。至于内存中的 segment,ElasticSearch 会异步到达某个条件是刷入磁盘。
Query 流程
查询的时候,经过 VectorQueryPlugin,先从 proxima 向量检索引擎中查找 topN 的向量,得到 seqNo 和类似度,再经过构造 newSetQuery 的 FunctionScoreQuery,去 join 其余查询语句。
这里的数字型 newSetQuery 底层是经过 BKDTree 去一次遍历所得,性能仍是很高效的。
Failover 流程
固然咱们还要处理各类的 Failover 场景:
数据从远端复制过来时:
sift-128-euclidean 100w 数据集(https://github.com/erikbern/a...
HNSW 插件 | ZSearch-HNSW 插件 | |
---|---|---|
数据写入(单线程1000个 bulk 写入) | 1.初始写入 5min,25个 segment,最大 CPU 300%;2.合并成1个 segment 5min,最大 CPU 700%(本地最大); | 构建索引 15min,由于单线程,因此最大CPU 100% |
查询 | 1. Top 10,召回率98%;2.rt 20ms,合并成1个 segment 后,5ms; | 1. Top 10,召回率98%;2.rt 6ms; |
优势 | 兼容 failover | CPU 消耗少,无额外存储 |
缺点 | CPU 消耗大,查询性能跟 segment 有关 | 主副分片全挂的状况下会有少许数据重复 |
100w 256维向量占用空间,大概是0.95GB,比较大:
设置 max_concurrent_shard_requests:
KNN 适合场景:
ANN 场景:
深度学习里的算法模型都会转化成高维向量,在召回的时候就须要用类似度公式来召回 topN,因此向量检索的场景会愈来愈丰富。
咱们会继续探索在 ElasticSearch 上的向量召回功能,增长更多的向量检索算法适配不一样的业务场景,将模型转化成向量的流程下沉到 ZSearch 插件平台,减小网络消耗。但愿能够和你们共同交流,共同进步。
吕梁(花名:十倍),2017年加入蚂蚁金服数据中间件,通用搜索平台 ZSearch 基础架构负责人,负责 ZSearch 组件在 K8s 上的落地及基于 ES 的高性能查询插件开发,对 ES 性能调优有着丰富的经验。
公众号:金融级分布式架构(Antfin_SOFA)