在Elasticsearch中进行相似向量搜索/相似图像搜索

(目录在这里)

首先

在之前的文章中,我们准备了大约一百万个1280维的图像特征向量,并将其投入到Amazon Elasticsearch Service中,但是我们得到了远离实用的结果,查询的响应时间达到了15秒。非常感谢Amazon ES团队给予我们的建议,因此我们按照他们的建议进行了重新验证。

如果对Elasticsearch非常熟悉的人来说,这个步骤可能是微不足道的,但对于我这个没有在实际服务中操作过Elasticsearch的人来说,这是非常有用的。

分割

查看上一次从Elasticsearch返回的响应,我们可以发现hits为1,203。此时,由于我们正在搜索最近的10个向量,所以k=10,并且我们可以知道结果至少来自于120个Elasticsearch Index。

{
  "took": 15471,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1203,
      "relation": "eq"
...

Elasticsearch 索引以 Shard 为单位进行分割,每个 Shard 都是 Lucene 索引。Lucene 索引在内部被分割成多个文件,称为 Segment。由于 Segment 是按顺序进行搜索的,所以 Segment 的数量越少,搜索效率就越高。

为了考虑搜索效率,Amazon ES的默认情况下,分片数为5,因此希望段数也为5。

设定

为了提高搜索效率,下面的设置建议被提出。

    • index.refresh_interval = -1 (default: 1 sec)

 

    • index.translog.flush_threshold_size = ‘10gb’ (default: 512mb)

 

    index.number_of_replicas = 0 (default: 1)

此外,也有建议针对索引时的线程数量进行改进,以提高索引效率。

    • Tune for indexing speed

 

    • Improving Indexing Performance in Amazon Elasticsearch Service

 

    Open Distro for Elasticsearch KNN

以下是用于插入改进后数据的代码。

import time
import math
import numpy as np
import json
import certifi
from elasticsearch import Elasticsearch, helpers
from sklearn.preprocessing import normalize

dim = 1280
fvecs = np.memmap('fvecs.bin', dtype='float32', mode='r').view('float32').reshape(-1, dim)

idx_name = 'imsearch'
es = Elasticsearch(hosts=['https://vpc-xxxxxxxxxxx.us-west-2.es.amazonaws.com'],
                   ca_certs=certifi.where())

res = es.cluster.put_settings({'persistent': {'knn.algo_param.index_thread_qty': 2}})
print(res)

mapping = {
    'settings' : {
        'index' : {
            'knn': True,
            'knn.algo_param' : {
                'ef_search' : 256,
                'ef_construction' : 128,
                'm' : 48
            },
            'refresh_interval': -1,
            'translog.flush_threshold_size': '10gb',
            'number_of_replicas': 0
        },
    },
    'mappings': {
        'properties': {
            'fvec': {
                'type': 'knn_vector',
                'dimension': dim
            }
        }
    }
}

res = es.indices.create(index=idx_name, body=mapping, ignore=400)
print(res)

bs = 200
nloop = math.ceil(fvecs.shape[0] / bs)
for k in range(nloop):
    rows = [{'_index': idx_name, '_id': f'{i}',
             '_source': {'fvec': normalize(fvecs[i:i+1])[0].tolist()}}
             for i in range(k * bs, min((k + 1) * bs, fvecs.shape[0]))]
    s = time.time()
    helpers.bulk(es, rows, request_timeout=30)
    print(k, time.time() - s)

合并片段

即使是以上的代码,段落数也不会成为5,所以需要调用以下终点,主动进行合并。

POST /imsearch/_forcemerge?max_num_segments=1

由于这个操作需要一些时间,所以需要每隔几分钟调用一次,直到收到状态码200为止,以确认是否完成。当然,也可以暂时不定期地轮询一段时间。最终应该有5个片段。

刷新

由于设置 index.refresh_interval = -1,刷新操作未被执行,因此需要调用以下端点。(也可以将其恢复为默认的1秒)

POST /imsearch/_refresh

当刷新完成后,就可以进行已插入向量的搜索。

所以,当我实际执行并测试的时候,结果是…7秒…虽然花了相当长的时间,但这真是令人沮丧。当然,与上一次的15秒相比,这已经有了显著的改善,但7秒并不实用。另外,预热也没有特别的效果。此外,在除了搜索时间以外的方面,每个分片有50个命中结果,这实际上是理想的情况,因为每个分片有一个段。

{
  "took": 7269,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 50,
      "relation": "eq"
    },
...

记忆

在本次实验中使用的是r5.large实例,拥有2核CPU和16GB内存。在Amazon ES的默认设置中,服务器内存的50%被分配给Elasticsearch,上限为32GB,并且剩余内存的一部分可以用于kNN。将所有剩余内存都分配给kNN是危险的,因此引入了断路器,限制了内存使用量。这可以通过knn.memory.circuit_breaker.limit进行设置,默认值为60%。因此,16GB * 50% * 60% = 4.8GB 是kNN的内存分配上限。

在HNSW中保持图/索引需要4*d + 8*M字节。我们在实验中使用了一百万个1280维的向量,而根据Amazon ES团队的建议,考虑到图的构建等问题,我们需要将需要量增加1.5倍。由于本次实验中M=48,所以需要(4*1280 + 8*48)*1.5*1M = 7.7GB的存储空间。

由于r5.large不足够,因此我重新创建了一个r5.xlarge(4个核心CPU,32GB内存)的集群并进行了新的尝试。在这种实例类型下,可以分配 32GB * 50% * 60% = 9.6GB的内存,因此满足了7.7GB的需求。由于CPU核心数量增加了,我也顺便将knn.algo_param.index_thread_qty参数调整为4,然后按照上述步骤生成了索引。

然后,发送了1000个搜索请求,计算搜索时间的平均值得到了14毫秒的结果,达到了实用水平的改善。

具有nmslib的ANN模型

作为ANN库,通常我们使用Faiss,所以在上一次实验中我们也用Faiss来处理HNSW。然而,Elasticsearch使用的是nmslib,所以为了确认,我们也做了比较。实例类型是r5.xlarge,用于集群。

image.png

考虑到它不是一个向量搜索引擎,而是一个全文搜索引擎,与使用Elasticsearch kNN相比,速度大约快了两倍。因此,可以说14毫秒的结果已经足够实用水平。

总结

多亏了亚马逊西班牙团队的支持,我们能够在Elasticsearch中实现类似搜索并达到实际可用的搜索时间。具体而言,通过适当设置段数和内存分配,我们看到了不可同日而语的改进与上一次相比。尽管HNSW是一种出色的ANN算法,但在内存效率方面,与采用倒排文件和产品量化(IVFPQ)等方法比较,它并不好。对于处理高维向量的Elasticsearch情况,我得到的印象是,最好在可能的范围内进行维度压缩。可以说,HNSW是一种适用于百万级数据的优秀算法,通过将其与Elasticsearch进行集群化,我们可以处理十亿级的数据,并实现可扩展性。

广告
将在 10 秒后关闭
bannerAds