百度ElasticsearchBES

    向量检索插件使用指南

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

    背景

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

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

    集群准备

    建议 说明
    套餐选择 至少16G内存以上 向量检索对集群内存要求较高,如果数据量超过10G,建议选择16核64G以上的套餐,如计算2型、计算3型、存储3型。
    单机数据量 建议不超过节点总内存的三分之一 向量检索对集群内存要求较高,如果数据量过大,可能造成内存溢出。
    写入限流 以计算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集群,自带向量检索插件;如果您已经安装向量检索插件,可以联系客服协助安装。

    上一篇
    Es-各个版本之间的升级
    下一篇
    常见问题