ES-KNN搜索

根据向量查询相近的目标。

常见的使用场景:

  1. 基于自然语言算法的相关性排名

  1. 相似推荐

  1. 相似的图片或视频搜索

提前需要做的准备

使用knn需要对数据进行预处理,你需要把需要匹配的数据转换为有意义的向量值,然后写入目标索引的dense_vector字段中。

KNN方法

对于KNN搜索,ES支持两种方法:

  1. 使用script_score暴力查询

  1. 近似KNN搜索

大多数情况下,我们推荐使用近似的KNN搜索,它会有较低的延迟,但是会牺牲索引的速度和结果的准确度。

如果使用暴力的script_score,那么需要使用query限定匹配的结果集,避免造成慢查询,匹配的结果越小,效果就越好。

暴力script_score

先创建对应的索引,需要有一个或者多个dense_vector字段,如果不需要使用近似KNN搜索,可以忽略字段映射或者将index设置为false,这样便于提高索引速度。

PUT product-index
{
  "mappings": {
    "properties": {
      "product-vector": {
        "type": "dense_vector",
        "dims": 5,
        "index": false
      },
      "price": {
        "type": "long"
      }
    }
  }
}

写入数据

POST product-index/_bulk?refresh=true
{ "index": { "_id": "1" } }
{ "product-vector": [230.0, 300.33, -34.8988, 15.555, -200.0], "price": 1599 }
{ "index": { "_id": "2" } }
{ "product-vector": [-0.5, 100.0, -13.0, 14.8, -156.0], "price": 799 }
{ "index": { "_id": "3" } }
{ "product-vector": [0.5, 111.3, -13.0, 14.8, -156.0], "price": 1099 }

查询

POST product-index/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "range" : {
              "price" : {
                "gte": 1000
              }
            }
          }
        }
      },
      "script": {
        "source": "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
        "params": {
          "queryVector": [-0.5, 90.0, -10, 14.8, -156.0]
        }
      }
    }
  }
}

提示:如果要限制匹配的文档数量,推荐在script_score里面指定一个filter查询,就像上面这样。

近似KNN

注意:相较于其他查询,近似KNN需要指定特殊的资源,因为需要把所有向量数据放在节点的页缓存中。相关的资源配置参考近似knn搜索调整。

创建索引,下面两个操作是必须的:

  1. dense_vector字段需要开启索引

  1. 指定similarity值,用于对匹配的相似度打分,参数的设置similarity参数

PUT image-index
{
  "mappings": {
    "properties": {
      "image-vector": {
        "type": "dense_vector",
        "dims": 3,
        "index": true,
        "similarity": "l2_norm"
      },
      "title": {
        "type": "text"
      },
      "file-type": {
        "type": "keyword"
      }
    }
  }
}

写入数据

POST image-index/_bulk?refresh=true
{ "index": { "_id": "1" } }
{ "image-vector": [1, 5, -20], "title": "moose family", "file-type": "jpg" }
{ "index": { "_id": "2" } }
{ "image-vector": [42, 8, -15], "title": "alpine lake", "file-type": "png" }
{ "index": { "_id": "3" } }
{ "image-vector": [15, 11, 23], "title": "full moon", "file-type": "jpg" }

查询

POST image-index/_search
{
  "knn": {
    "field": "image-vector",
    "query_vector": [-5, 9, -12],
    "k": 10,
    "num_candidates": 100
  },
  "fields": [ "title", "file-type" ]
}

文档的分数由查询和文档的向量决定,具体怎么计算,参考similarity参数。

knn api 会先去每个分片找num_candidates个最近邻候选者,然后每个分片计算最优的k个。最后把每个分片的结果合并,在计算出k个全局最优。

num_candidates的值可以控制结果的精确度,但是更好的结果会带来更多的消耗。

过滤后的KNN搜索

在KNN搜索里面可以指定的一个filter查询,限定匹配的结果集:

POST image-index/_search
{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "filter": {
      "term": {
        "file-type": "png"
      }
    }
  },
  "fields": ["title"],
  "_source": false
}

注意:这个查询会在KNN搜索期间过滤结果,能够确保返回的结果是k个。如果是post-filtering,那么是在knn搜索完之后再过滤的,返回的结果可能是会少于k个的。

结合近似KNN搜索和其他功能

可以使用knn搜索和不同的query混合搭配:

POST image-index/_search
{
  "query": {
    "match": {
      "title": {
        "query": "mountain lake",
        "boost": 0.9
      }
    }
  },
  "knn": {
    "field": "image-vector",                                         (1)
    "query_vector": [54, 10, -2],                                    (2)    
    "k": 5,                                                                                (3)
    "num_candidates": 50,                                             (4)
    "boost": 0.1
  },
  "size": 10
}
  1. 要查询的目标字段,必须是一个dense_vector类型。

  1. 查询的向量,和目标字段的维度要一样。

  1. 返回最相邻的k个结果,这个值必须小于num_candidates。

  1. 每个分片要考虑的最近邻候选者的数量。 不能超过 10000。 ES 从每个分片收集 num_candidates 个结果,然后合并它们以找到前 k 个结果。 增加 num_candidates 往往会提高最终 k 个结果的准确性。

提示:这里knn里面还可以使用filter,它可以过滤需要匹配的文档,返回的k个文档都会符合匹配的条件。

这个查询是先得到全局5个最相邻结果,然后将他们与匹配查询匹配的结果组合,选出得分最高的前10个返回。分数的计算是这个样子的:

score = 0.9 * match_score + 0.1 * knn_score

近似knn还可以搭配聚合,它聚合的是top k个邻近文档的结果。如果还有query,那么聚合的是混合查询的结果。

索引注意事项

在索引是ES的每个段需要把dense_vector值存储为HNSW graph,构建这些图花费是巨大的。所以写入的时候做好响应时间的调整,调优参考近似KNN搜索调整。

另外,HNSW算法也有一些参数用来在构图开销,搜索速度和准确度之间进行权衡,可以使用index_options来调整这些参数:

PUT image-index
{
  "mappings": {
    "properties": {
      "image-vector": {
        "type": "dense_vector",
        "dims": 3,
        "index": true,
        "similarity": "l2_norm",
        "index_options": {
          "type": "hnsw",                                   (1)
          "m": 32,                                          (2)
          "ef_construction": 100                            (3)    
        }
      }
    }
  }
}
  1. knn使用的算法,当前只支持hnsw。

  1. HNSW 图中每个节点将连接到的邻居数量。 默认为 16。

  1. 在为每个新节点组装最近邻居列表时要跟踪的候选者数量。 默认为 100。

近似KNN搜索限制

  1. 不能再一个nested 映射里面使用dense_vector。

  1. 使用用knn搜索做跨集群搜索时,ccs_minimize_roundtrips 操作不支持。

  1. 因为用了HNSW算法,保证了搜索的速度,但是牺牲掉了准确度。

注意:为了跨多个分片收集全局的top k个结果,近似KNN搜索使用dfs_query_then_fetch搜索类型。这个没法更改。

近似KNN搜索调整

多用dot_product

cosine 相似性计算可以接受任何的浮点向量,对于测试来说它是非常方便的,但是它 得不到最优的效果。相反的,推荐使用dot_product计算相似性。要使用dot_product,需要把所有向量归一化长度为1。这样就能够获得更快的速度,因为它避免了额外的向量长度计算。

确保数据节点有足够的内存

ES的knn搜索用的是HNSW算法,HNSW算法需要大部分的向量数据在内存中才有效果,因此要确保数据节点有足够的RAM保留向量数据和索引结构。如果要检查向量数据的大小,可以使用 Analyze index disk usage API。

HNSW内存占用的预估方法可以参考这个公式:

num_vectors * 4 * (num_dimensions + 32)+ small buffer

特别注意的的是,需要的RAM是要与jvm的堆内存分开的。预留的一小部分缓冲,主要是考虑到其他可能需要用到缓存的地方,比如文本字段或者数值字段,也有可能需要使用文件缓存。

预热文件系统缓存

当es重启的时候,文件缓存是空的,如果等到热数据都加载到内存中,是需要一定的时间的,这个时候可以通过设置index.store.preload提前加载需要的数据到文件缓存中。

注意:当缓存不足够大时,加载过多的索引或者数据到缓存中会使搜索变慢,使用的时候要小心。

减少向量维度

knn搜索向量的维度和搜索速度呈线性关系,在可接受的效果下,应该尽量想办法减少向量的维度。可以尝试使用一些像PCA这样的数据对向量做降维处理。

查询时排除召回向量字段

向量字段一般都比较大,如果返回到结果里面,会有很大的加载开销。通常这个字段在结果里面也是不怎么需要的。在召回的时候应该尽量按需取数。

减少索引的段数量

ES的索引数据是放在segment上的,而向量数据在segment上是存成HNSW图的。当搜索的时候需要扫描多个segment,一个接一个的搜索HNSW图。如果segment过多,必然带来更多的性能开销。默认情况下,ES会有固定的策略把小的端合并成大的端。你也可以手动合并。

强制合并段

使用force merge api将数据合并到一个段里面。这样在搜索的时候KNN搜索就只需要检查一个HNSW图。强制合并段是一个非常消耗资源的操作,要特别注意集群的压力。

注意:段合并推荐在只读索引上操作,当文档更新时,ES只是标记文档已被删除,在常规的合并流程中这种文件会在段合并期间被清理掉。但是强制合并会产生非常大的段,这些段不符合常规合并的条件,因此会有大量的标记删除文档出现,从而带来更高的磁盘使用和更差的搜索性能。

在批量索引的时候创建一个大的段

在索引初始化的时候,可以通过索引的设置,让ES创建一个大的段。

  1. 首先在初始化的时候要确保没有查询服务,并且要禁止索引刷新。

  1. 给 Elasticsearch 一个大的索引缓冲区,这样它可以在刷新之前接受更多的文档。 默认情况下,indices.memory.index_buffer_size 设置为堆大小的 10%。 对于像 32GB 这样大的堆大小,这通常就足够了。 要允许使用完整的索引缓冲区,您还应该增加限制 index.translog.flush_threshold_size。

避免在索引期间做很重的索引

主动索引文档会给KNN搜索带来不好的效果,因为它会占用计算资源。当搜索和索引同时发生的时候,ES也会刷新得比较频繁,从而创建过多的小的segment,而过多的segment又会影响查询的性能。

最好在knn搜索期间大量的索引文档。如果是需要重新构建索引向量这种情况,应该新建一个索引做替换策略,而不是就地更新。

设置合适的预读值

搜索会导致大量随机读取 I/O。 当底层块设备具有高预读值时,可能会做大量不必要的读取 I/O,尤其是在使用内存映射访问文件时。

大多数 Linux 发行版对单个普通设备使用 128KiB 的敏感预读值,但是,当使用软件 raid、LVM 或 dm-crypt 时,生成的块设备(支持 Elasticsearch path.data)可能最终具有非常大的预读值(在几个 MiB 的范围)。 这通常会导致严重的页面(文件系统)缓存抖动,从而对搜索(或更新)性能产生不利影响。

可以使用 lsblk -o NAME,RA,MOUNTPOINT,TYPE,SIZE 检查 KiB 中的当前值。建议预读值为 128KiB。

注意:blockdev 期望 512 字节扇区中的值,而 lsblk 报告 KiB 中的值。 例如,要临时将 /dev/nvme0n1 的预读设置为 128KiB,请指定 blockdev --setra 256 /dev/nvme0n1(todo)

similarity参数

knn计算相似度的算法,支持的值如下:

12_norm

基于向量的L^2(欧几里德距离)计算,_score = 1 / (1 + l2_norm(query, vector)^2)

dot_product

计算两个向量的点积,能够优化cosine算法,是向量要做好归一化。包括文档向量和查询向量。

_score = (1 + dot_product(query, vector)) / 2

cosine

计算余旋相似度,计算余旋相似度最有效的办法是将向量归一化为固定长度,而不是使用dot product。如果没有办法做归一化,那没只能使用余旋。_score = (1 + cosine(query, vector)) / 2。余旋算法不允许向量幅度为0,因为这种情况下没有定义余旋。

注意:向量的相似度和文本的相似度是不一样的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值