ElasticsearchBES

向量检索插件

向量检索插件由吉利智能云Elasticsearch团队研发,能够快速实现向量检索、向量计算等需求。

背景

近年来基于Text(Document) Embedding、特征向量等的向量检索在推荐系统、图片的相似度检索中得到了广泛使用。用户可以使用Word2vec等工具将图像、音频、自然语言等复杂的数据信息映射为特征向量,再通过向量检索算法检索特征向量,从而实现了对复杂的数据信息的处理。为了处理向量数据,吉利Elasticsearch向量检索插件提供了两种向量检索算法:linear算法和hnsw算法。

算法 含义 适用场景 缺点 支持距离算法
linear 线性计算所有向量数据 召回率100%。
查询时间与数据量成正比。
通常用于效果对照。
大数据量下效率较低
消耗cpu
全内存
余弦距离(cosine)
欧式距离(l2)
点积(dot_prod)
hnsw 基于hnsw算法对数据进行近似计算 单机数据量小。
对召回率要求高
对查询速度要求高。
数据膨胀比较大
写入数据后需要构建索引
全内存
余弦距离(cosine)
欧式距离(l2)

注意:目前向量检索插件支持功能发布后新创建的7.4.2版本的实例,不支持向量检索插件的集群请提交工单,BES团队会协助升级集群,升级方式参见ES版本升级

集群准备

建议 说明
套餐选择 至少16G内存以上 向量检索对集群内存要求较高,如果数据量超过10G,建议选择16核64G以上的套餐,如bes.g3.c16m64、bes.c3.c32m64等,详见套餐计费说明
单机数据量 建议不超过节点总内存的三分之一 向量检索对集群内存要求较高,如果数据量过大,可能造成内存溢出。
写入限流 以计算2型(16核64G)的节点为例,建议单节点写入限流控制在4000tps以内 向量索引的构建属于CPU密集型任务,建议不要大流量写入数据。
由于在查询过程中,会把数据全部加载到系统内存,因此在查询期间,不要同时进行大流量写入。

使用方法

用户在写入数据前,需要根据业务的向量维度信息和性能需求配置knn参数,选择距离计算算法,创建所需的knn索引。创建索引后,即可以写入数据。在索引完成构建后,可以通过下文提供的查询方式,进行向量检索查询。

创建knn索引

我们需要预先创建knn索引,创建方式如下:

如下示例,我们创建了一个名为test-index的索引,包含了field1field2字段。您也可以根据自身需求,自定义索引名称和字段名称。

PUT /test-index
{
    "settings": {
      "index": {
         "codec": "bpack_knn_hnsw",
         "bpack.knn.hnsw.space": "cosine",
         "bpack.knn.hnsw.m": 16,
         "bpack.knn.hnsw.ef_construction": 512
      }
   },
   "mappings": {
      "properties": {
         "field1": {
            "type": "bpack_vector",
            "dims": 2
         },
         "field2": {
            "type": "bpack_knn_vector",
            "dims": 2
         }
      }
   }
}
参数 描述
index.codec bpack_knn_hnsw,支持hnsw算法linear算法。否则仅支持linear算法
type 向量检索插件提供两种新的向量字段类型,bpack_vectorbpack_knn_vector
bpack_vector表示普通向量字段,支持linear算法
bpack_knn_vector表示向量检索字段,支持linear算法hnsw算法
dims 向量维度,支持2~2048维。

settings中的bpack.knn.hnsw参数含义见下文中索引级别参数优化

写入与查询数据

写入数据

我们向刚才创建的索引test-index_doc中写入数据,写入数据示例如下:

POST /test-index/_doc/
{
    "field1" : [6.5, 2.5],
    "field2" : [6.5, 2.5],
    "price" : 10
}

其中field1是我们刚设置为bpack_vector类型的字段,field2是我们刚设置为bpack_knn_vector类型的字段,price是其他普通字段。

在索引完成构建后,我们可以查询数据如下:

linear查询

linear算法可以查询bpack_knn_vector类型的字段,也可以查询bpack_vector类型的字段。下例中,我们查询的是bpack_vector类型的字段field1

POST /test-index/_search
{
   "query": {
      "script_score": {
         "query": {
            "match_all": {}
         },
         "script": {
            "source": "bpack_knn_script",
            "lang": "knn",
            "params": {
               "space": "cosine",
               "field": "field1",
               "vector": [3.5, 2.5]
            }
         }
      }
   },
   "size": 100
}
或
POST /test-index/_search
{
  "query": {
    "function_score": {
      "boost_mode": "replace",
      "script_score": {
        "script": {
          "source": "bpack_knn_script",
          "lang": "knn",
          "params": {
            "space": "cosine",
            "field": "field1",
            "vector": [3.5, 2.5]
          }
        }
      }
    }
  },
  "size": 100
}

其中查询参数含义为:

参数 描述 默认值
source 选择计算方法,这里置为bpack_knn_script 必填参数
space 距离算法参数。linear算法支持三种距离算法:余弦距离(cosine)、点积(dot_prod)、欧式距离(l2)。 cosine
field 向量字段名。 必填参数
vector 格式为float数组,数组长度必须与创建索引时该字段mapping指定的dims保持一致。 必填参数

hnsw查询

使用hnsw的方式查询,该索引必须指定index.codecbpack_knn_hnsw,且要查询的向量字段mapping指定的type必须是bpack_knn_vector。下例中,我们查询的是bpack_knn_vector类型的字段field2

POST /test-index/_search
{
    "size" : 10,
    "query": {
        "knn": {
            "field2": {
                "vector": [3, 4],
                "k": 2,
                "ef": 512
            }
        }
    }
}

其中查询参数含义为:

参数 描述 默认值
vector 格式为float数组,数组长度必须与创建索引时该字段mapping指定的dims保持一致,否则可能造成结果有误差。 必填参数
k 在hnsw算法中查询的最近邻的数量,取值为正整数。 必填参数
ef 此参数表示在搜索期间,最近邻居的动态扫描区域的大小。该值越大,查询准确率越好,查询速度越慢,取值范围为[2,1024]。 512

参数优化

索引级别的参数

创建索引时必须提供索引settings参数。如果不提供这些设置,将使用其默认值。这些设置是静态的,这意味着您不能在创建索引后修改它们。具体参数解析如下:

参数 描述 默认值
bpack.knn.hnsw.m 此参数表示构造期间为每个新元素创建的双向链接数。m的合理范围为2-100。主要影响内存、存储消耗、准确率,m值越高,意味着更高消耗的内存和存储,更慢的索引构建时间,以及更好的准确率。建议根据(向量维度 * 1.5) 取值,以保证性能,12-48可以满足大多数场景的需求。 16
bpack.knn.hnsw.space 向量检索计算的距离算法。距离算法参数。hnsw支持两种距离算法:余弦距离(cosine)、欧式距离(l2)。 cosine
bpack.knn.hnsw.ef_construction 此参数表示在索引构建过程中,最近邻居的动态扫描区域大小。该值越大,查询准确率更高,但是索引构建越慢,取值范围为[2,1024]。 512

集群级别的参数

通用参数

参数 描述 默认值
bpack.knn.hnsw.index_thread_qty 此参数表示HNSW构建图形允许使用的线程数。(默认情况下,nmslib将此值设置为内核数n。但是,由于Elasticsearch可以创建n个用于生成索引的线程,并且如果每个索引线程都调用nmslib来构建图形,也就是说每个线程都会生成n个线程,这可能导致同时n^2个线程运行,可能导致100%的CPU利用率。所以默认将此值设为1),取值范围为[1,32]。 1

缓存设置

linear算法缓存参数设置
参数 描述 默认值
bpack.knn.memory.cache.limit 此参数表示指示缓存的最大容量。当缓存尝试加载数据时超过了缓存的最大容量限制,它将触发驱逐操作。该值可以设置为百分数,代表jvm内存的百分比,也可以设置为一个带有存储容量单位的值,例如『10kb』、『10mb』、『3g』等,不建议设置为小数值,如『1.5g』。 10%
bpack.knn.memory.cache.expiry.time 此参数表示当数据持续这个时间不被访问时,它将从缓存中清除。使用TimeUnit格式表示,例如『10s』、『10m』、『3h』等,不可设置为小数值,如『1.5h』。一般来说,我们会将这个值设置超过30分钟,使缓存结果能够被接下来的查询有效命中;如果设置过小,则会很快被清除。 30m
hnsw算法缓存参数设置
参数 描述 默认值
bpack.knn.cache.item.expiry.time 此参数表示当数据持续这个时间不被访问时,它将从缓存中清除。使用TimeUnit格式表示,例如『10s』、『10m』、『3h』等,不可设置为小数值,如『1.5h』。一般来说,我们会将这个值设置超过30分钟,使缓存结果能够被接下来的查询有效命中;如果设置过小,则会很快被清除。 180m

Circuit Breaker(断路器)设置

hnsw算法会消耗大量堆外内存,而如果消耗的内存过多,Elasticsearch/Lucene可以使用的pagecache就会不足,集群性能将会下降。为了避免这种情况,我们可以配置Circuit Breaker来限制堆外内存的过量消耗。目前,当内存达到我们配置的断路器限制,则会触发驱逐机制,驱逐不常用的缓存项。

参数 描述 默认值
bpack.knn.memory.circuit_breaker.limit 此参数表示指示缓存的最大容量。当此时hnsw的缓存超过了缓存的最大容量限制,它将触发驱逐操作,并将circuit_breaker_triggered状态设置为true(可以通过查询统计信息api查询)。该值可以设置为百分数,代表除去Elasticsearch的jvm外,服务器剩余内存的百分比,也可以设置为一个带有存储容量单位的值,例如『10kb』、『10mb』、『3g』等,不建议设置为小数值,如『1.5g』。例如,一台机器拥有100GB内存,Elasticsearch的jvm使用了32GB。那么bpack.knn.memory.circuit_breaker.limit的默认值为(60% * (100 -32) = 40.8GB)。 60%
bpack.knn.circuit_breaker.unset.percentage 此参数表示Circuit Breaker的解除百分比,当缓存容量大小低于bpack.knn.circuit_breaker.unset.percentage时,Circuit Breaker将解除触发,将circuit_breaker_triggered状态设置为false(可以通过查询状态api查询)。 75

示例

PUT /_cluster/settings
{
    "persistent" : {
        "bpack.knn.hnsw.index_thread_qty" : 1,
        "bpack.knn.cache.item.expiry.time": "15m",
        "bpack.knn.memory.cache.limit": "1g",
        "bpack.knn.memory.cache.expiry.time":"10m",
        "bpack.knn.memory.circuit_breaker.limit" : "55%",
        "bpack.knn.circuit_breaker.unset.percentage": 23
    }
}

查看hnsw算法的相关统计信息

查询状态的方式如下:

GET /_bpack/_knn/stats
GET /_bpack/_knn/nodeId1,nodeId2/stats/statName1,statName2

结果示例如下:

{
   "_nodes": {
      "total": 1,
      "successful": 1,
      "failed": 0
   },
   "cluster_name": "my-application",
   "circuit_breaker_triggered": false,
   "nodes": {
      "HYMrXXsBSamUkcAjhjeN0w: {
         "eviction_count" : 0,
         "miss_count" : 1,
         "graph_memory_usage_kb" : 1,
         "cache_capacity_reached" : false,
         "load_exception_count" : 0,
         "hit_count" : 0,
         "load_success_count" : 1,
         "total_load_time_nanos" : 2878745
      }
   }
}

集群状态参数:

参数 描述
circuit_breaker_triggered 指示是否触发断路器。如果集群中的任何节点由于已达到缓存的容量,而从缓存中删除条目时,则会触发断路器。而当缓存中条目数的大小低于bpack.knn.circuit_breaker.unset.percentage时,断路器将取消触发。

节点状态参数:

参数 描述
eviction_count 表示guava cache中,缓存淘汰的次数。(由于索引删除等情况产生的不计算在内)
hit_count 节点上发生的缓存命中数。
miss_count 节点上发生的缓存未命中数。
graph_memory_usage_kb 缓存在本机内存中的总大小,以kb为单位。
cache_capacity_reached 是否达到此节点的缓存容量。
load_exception_count 加载到缓存时发生的异常数量。
load_success_count 加载到缓存时发生的成功数量。
total_load_time_nanos 加载到缓存的总耗时,单位:纳秒。

性能对比

  • 内存配置:30G
  • cpu配置:逻辑核数56,2个物理cpu,每个cpu cores : 14
  • Elasticsearch 节点:单节点

性能对比结果如下:

数据量 索引参数 集群参数 Top30召回率 hnsw平均耗时 linear平均耗时
100万32维向量
1shards
"bpack.knn.hnsw.space": "cosine",
"bpack.knn.hnsw.m": 16,
"bpack.knn.hnsw.ef_construction": 300
"bpack.knn.cache.item.expiry.time": "1h",
"bpack.knn.memory.cache.limit": "15g",
"bpack.knn.memory.cache.expiry.time":"1h",
"bpack.knn.memory.circuit_breaker.limit" : "70%"
99.97% 12.96ms 134.96ms
1000万32维向量
1shards
"bpack.knn.hnsw.space": "cosine",
"bpack.knn.hnsw.m": 16,
"bpack.knn.hnsw.ef_construction": 600
"bpack.knn.cache.item.expiry.time": "1h",
"bpack.knn.memory.cache.limit": "15g",
"bpack.knn.memory.cache.expiry.time":"1h",
"bpack.knn.memory.circuit_breaker.limit" : "70%"
99.97% 24.69ms 1209.13ms
1000万32维向量
16shards
"bpack.knn.hnsw.space": "cosine",
"bpack.knn.hnsw.m": 48,
"bpack.knn.hnsw.ef_construction": 600
"bpack.knn.cache.item.expiry.time": "1h",
"bpack.knn.memory.cache.limit": "15g",
"bpack.knn.memory.cache.expiry.time":"1h",
"bpack.knn.memory.circuit_breaker.limit" : "70%"
99.99% 20.26ms 609.56ms

算法总结

  • linear算法适用场景:

    • 数据量小(通常单分片在100w以下);
    • 先执行正常的搜索过滤条件,然后在过滤后的结果集上进行向量检索计算;
    • 召回率100%,查询性能相比hnsw较慢
  • hnsw算法适用场景:

    • 数据量相对大(集群数据量在千万级);
    • 向量检索计算和其他过滤同时进行,建议适当的增大hnsw的查询参数k,以保证尽可能多的满足过滤条件的数据参与计算;
    • 查询性能要求高,召回率在90%以上

最佳实践

  • 建议写入结束后,在业务低峰期进行定期forceMerge,有助于降低查询延迟。
  • 使用linear算法查询时,要根据数据量大小,定义好"bpack.knn.memory.cache.limit"参数。比如节点数据量为10G,如果使用"bpack.knn.memory.cache.limit"的默认值(计算2型默认值为30G*10%=3G),则会无法缓存。当大量查询时,有可能触发Elasticsearch的熔断操作,报错circuitBreakingException。
  • 当构建较大数据量的向量索引时,可能会出现build较慢的情况,可以根据分片数和节点cpu核数,在写入数据前适当调整"bpack.knn.hnsw.index_thread_qty" 。例如,1kw数据量,1节点2分片,节点为16核cpu,我们可以把"bpack.knn.hnsw.index_thread_qty" 设置为4-6(如果设置为8,会使cpu满载,生产环境可能有风险),可以提高构建效率。

    需要注意的是,"bpack.knn.hnsw.index_thread_qty" 参数设置偏大,会导致构建时启动线程过多。在负载比较高的集群,不建议调整这个参数,以免集群满载。如果写入和构建向量索引偏慢,可以通过临时减少集群负载(减少其他写入和查询),并调大"bpack.knn.hnsw.index_thread_qty"的方式来加快构建 ,等到构建结束,再将"bpack.knn.hnsw.index_thread_qty" 调整回1。

  • 当写入数据量为1kw条(比如约为10G)、1节点1分片、计算2型节点(16核cpu、64G内存)时,推荐参数设置为:

    PUT /_cluster/settings
    {
        "persistent" : {
            "bpack.knn.hnsw.index_thread_qty" : 1,
            "bpack.knn.cache.item.expiry.time": "1h",
            "bpack.knn.memory.cache.limit": "12g",
            "bpack.knn.memory.cache.expiry.time":"1h",
            "bpack.knn.memory.circuit_breaker.limit" : "70%"
        }
    }

​ 分析:

  1. "bpack.knn.hnsw.index_thread_qty" : 1:通常情况下,建议设置为1;当索引构建过慢,可以参考上一条建议适当调整这个参数。
  2. "bpack.knn.cache.item.expiry.time": "1h":可以根据自己需求的业务设定超时时间。
  3. "bpack.knn.memory.cache.limit": "12g":数据约10G,缓存需要可以容纳所有数据。
  4. "bpack.knn.memory.cache.expiry.time":"1h":可以根据自己需求的业务设定超时时间。
  5. "bpack.knn.memory.circuit_breaker.limit" : "70%":计算2型Elasticsearch的默认jvm内存为30G。"bpack.knn.memory.circuit_breaker.limit"为 70%*(64-30)=23.8G,可以容纳数据所占据的堆外内存。

常见问题

  • Q:召回率是怎么定义的?

    A:用同样的向量查询两种查询方式,对比召回的文档,取二者相同的文档与召回文档总数的比值,即为待测向量的召回率。我们用召回率来表征查询的准确率。

  • Q:为什么写入已经成功完成,索引的文档数并没有增加或完全达到写入量,且此时可能会查询失败?

    A:向量索引的构建发生在refresh或flush期间,虽然写入已经完成,但后台的向量索引构建任务可能仍然在继续。

  • Q:如何安装向量检索插件?

    A:新申请的7.4.2集群,自带向量检索插件;如果您需要在集群安装向量检索插件,可以联系客服协助安装。

上一篇
百度NLP中文分词词典动态更新
下一篇
基于BOS的冷热数据分离