掘金 人工智能 05月21日 08:43
​ES查询优化随记1: 多路向量查询 & KNN IO排查 & 高效Filter使用
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文作者分享了在使用Elasticsearch(ES)构建知识库时,针对向量检索进行优化的一些实战经验。主要涉及多Query向量查询的各种方案,包括Pooling Script查询、Script循环取值以及KNN查询。重点讨论了KNN查询中遇到的IOUtil过高问题,并提出了通过优化图大小和预加载数据到内存的解决方案。此外,还对比了pre filter、post filter以及must语句三种不同的Filter使用方式对查询效率的影响,总结了在KNN场景下pre filter效率最高的结论。

🧮针对多Query向量查询,作者对比了Pooling Script查询、向量script循环取cosine值以及多向量KNN查询三种方案。Pooling Script查询虽然效率高,但效果差;ES8推荐使用KNN查询,并建议使用msearch组合查询以提升效率。

📈在使用KNN查询时,作者遇到了IOUtil被打满的问题。通过优化HNSW图的大小(例如,将Float32存储改为INT8存储)和预加载向量数据到内存(使用`index.store.preload`参数),可以有效降低IO压力,避免影响线上查询。

⏱️针对向量搜索中的时间Filter,作者对比了pre filter、post filter以及must语句三种不同的使用方式。实验表明,pre filter(在knn的filter语句中)的查询效率最高,因为它可以有效缩减knn查询范围;post filter效率较低,而must语句由于所有文档都会参与评分,效率最低。

哈哈最近感觉自己不像算法倒像是DB,整天围着ES打转,今天查IO,明天查内存,一会优化查询,一会优化吞吐。毕竟RAG离不开知识库,我们的选型是ES,于是这一年都是和ES的各种纠葛。所以顺手把近期获得的一些小tips记下来,万一有人和我踩进了一样的坑,也能早日爬出来。当前使用的ES版本是8.13,和7版本有较大的差异,用7.X的朋友这一章可能有不适配。本章主要覆盖以下

多Query向量查询的各种方法

大模型的知识库都离不开向量查询,并且当前的RAG往往会对用户query进行多角度的改写和发散,因此会涉及多query同时进行向量查询。如果用ES实现的话,常用的有以下几种形式

query_body = {    "size":10    "query": {            "bool": {                "must": [{                    "script_score": {                        "query": {"match_all": {}},                          "script": {                            "source": f"cosineSimilarity(params.query_vector, 'vectors')",                            "params": {"query_vector": avg_embedding}                        }                    }                }]            }        }}
query_body = {    "query": {        "function_score": {            "script_score": {                "script": {                    "source": """                        double max_score = -1.0;                        if (doc[params.field].size() == 0) return 0; // 空值保护                        for (int i=0; i<params.length; i++) {                            double similarity = cosineSimilarity(                                params.query_vectors[i],                                 params.field                            );                            max_score = Math.max(max_score, similarity);                        }                        return max_score;                    """,                    "params": {                        "length": len(embedding_list),                        "query_vectors": embedding_list,                        "field": "vectors"                    }                }            },            "boost_mode": "replace"        }    }}response = es.search(index=index, body=query_body)
query_body = {    "query": {                "bool": {                    "must": [{                        "knn": {                            "field": 'vectors',                            "query_vector": embedding_item,                            "num_candidates": 10                        }                    }]                }            }}

KNN查询IO打满的问题排查

在使用以上KNN搜索时,我们遇到一个问题,就是一使用KNN查询,IOUitl指标就会打满,进而影响其他所有查询任务,导致线上查询会Hang住,如下图所示。

经过阿里ES大佬的帮助,我们定位了问题,核心是HNSW图向量没有加载到内存里,因此在查询时触发了向量加载,因此大量的IO都是在进行向量搬运操作,搬多久IO就打满多久,解决的方案主要分2步

第一步优化图大小,原始我们的图使用Float32存储的,ES8.15提供了int4,int8存储,8.17好像还提供压缩比例更高的存储方式,因此我们reindex了索引修改成了INT8存储。但是发现优化后IO指标并未下降。

于是就有了第二步预加载,大佬给出的解释是HNSW算法存在大量的随机读,除了HNSW图,还会读取原始或量化后的向量数据,这些数据也需要加载到内存中进行查询(从磁盘加载)。而解决这个问题的办法就是预加载。以int8_hnsw为例,预加载的方式如下,需要先关闭索引,完成预加载,再开启索引。因此必须在非业务使用期进行操作,百万级的索引大小大概几分钟就能完成以下操作,不过我们在预加载时有时会引起集群的不稳定,因此建议在业务低峰期操作。

# 关闭索引POST your_index/_close# 调整preload参数PUT your_index/_settings{  "index.store.preload": ["vex", "veq"]}# 开启索引POST your_index/_open

只不过以上预加载存在一个问题,就是预加载的内容会持续存在缓存中,也算是用空间换时间的方案,如果有太多索引需要做预加载,应该会存在竞争关系,不过在我们的配置下还未出现这个问题,所以大家在预加载时需要关注下内存等指标变化

Filter的三种不同使用方式

依旧围绕向量搜索,在使用向量时在我们的场景中一般会有时间Filter,根据不同的时效性分层和时效性抽取结果选取不同的查询时间段,和整个index大小相比,时效性filter往往能大幅缩减查询的索引范围,但是使用filter的方式不同,对查询效率有较大影响。

    pre filter:过滤条件在knn的filter语句中
{"knn": {"field": "vectors","query_vector": [-0.0574951171875, 0.0222320556640625, ...],"num_candidates": 3,  "filter": [{      "range": {          "publishDate": {              "lte": "2025-04-22",              "gte": "2023-04-22",              "format": "yyyy-MM-dd"          }      }  }]}}
    post filter:过滤条件在bool的filter语句中
{      "bool": {          "must": [{              "knn": {                 "field": "vectors",                  "query_vector": [-0.0574951171875, 0.0222320556640625, ...],                  "num_candidates": 3,              }          }],          "filter": [{              "range": {                  "publishDate": {                      "lte": "2025-04-22",                      "gte": "2023-04-22",                      "format": "yyyy-MM-dd"                  }              }          }]      }  }
    过滤条件在must语句中
{      "bool": {          "must": [{              "range": {                  "publishDate": {                      "lte": "2025-04-22",                      "gte": "2023-04-22",                      "format": "yyyy-MM-dd"                  }              }          },        {              "knn": {                 "field": "vectors",                  "query_vector": [-0.0574951171875, 0.0222320556640625, ...],                  "num_candidates": 3,              }          }]      }  }

以上三种查询方式的对比如下

Filter方式查询效率
Pre Filter查询效率最高,前置过滤会缩减knn查询范围提高查询效率
Post Filter查询效率较低,在非KNN的其他查询场景这是查询效率最高的写法。但在KNN场景中会比只用KNN更慢,因为是在KNN搜索结果拿到后进行后置过滤,同时会导致最终返回的数量可能少于查询数量
过滤条件在must语句中查询效率最低,在filter语句中ES会直接忽略不满足条件的文档,而在must语句中所有文档都会参与评分。并且Filter语句ES会缓存过滤条件使得后续查询更快而must不会进行缓存

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Elasticsearch 向量检索 KNN查询 性能优化 知识库
相关文章