根据向量查询相近的目标。
常见的使用场景:
基于自然语言算法的相关性排名
相似推荐
相似的图片或视频搜索
提前需要做的准备
使用knn需要对数据进行预处理,你需要把需要匹配的数据转换为有意义的向量值,然后写入目标索引的dense_vector字段中。
KNN方法
对于KNN搜索,ES支持两种方法:
使用script_score暴力查询
近似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搜索调整。
创建索引,下面两个操作是必须的:
dense_vector字段需要开启索引
指定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
}
要查询的目标字段,必须是一个dense_vector类型。
查询的向量,和目标字段的维度要一样。
返回最相邻的k个结果,这个值必须小于num_candidates。
每个分片要考虑的最近邻候选者的数量。 不能超过 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)
}
}
}
}
}
knn使用的算法,当前只支持hnsw。
HNSW 图中每个节点将连接到的邻居数量。 默认为 16。
在为每个新节点组装最近邻居列表时要跟踪的候选者数量。 默认为 100。
近似KNN搜索限制
不能再一个nested 映射里面使用dense_vector。
使用用knn搜索做跨集群搜索时,ccs_minimize_roundtrips 操作不支持。
因为用了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创建一个大的段。
首先在初始化的时候要确保没有查询服务,并且要禁止索引刷新。
给 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,因为这种情况下没有定义余旋。文章来源:https://www.toymoban.com/news/detail-821510.html
注意:向量的相似度和文本的相似度是不一样的。文章来源地址https://www.toymoban.com/news/detail-821510.html
到了这里,关于ES-KNN搜索的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!