Elasticsearch 之分页查询
引言
在搜索引擎应用中,分页查询是一种常见需求。Elasticsearch 提供了多种分页方式以应对不同场景。本文将结合实际应用场景,介绍三种常用的分页查询方法
From + Size 分页查询
json
GET /content_item_profile/_search
{
"from": 0,
"size":20,
"query": {
"match": {
"title": "字节跳动"
}
},
"sort": [
{
"born_time": {
"order": "desc"
}
}
]
}
- 上述 ES 查询语句共返回20条结果
- From + Size 查询的优缺点
- 优点: 支持随机翻页
- 缺点 :
- 受制于 max_result_window 设置,不能无限制翻页
- 存在深度翻页问题,越往后翻页越慢
- 不推荐使用 from + size 做深度分页查询的核心原因
- 搜索请求通常跨越多个分片,每个分片必须将其请求的命中内容以及任何先前页面的命中内容加载到内存中
- CPU: 每个分片和协调节点都需要进行大规模排序,CPU压力巨大
- 内存:协调节点需要创建一个容量为 from + size的优先级队列来存储和排序所有分片的结果,大量消耗堆内存
- 带宽:分片和协调节点之间需要传输大量数据,占用网络带宽
- 响应时间:整个过程非常缓慢,延迟可能从毫秒级变为秒级甚至分钟级
- From + Size 分页查询适用场景
- 小型数据集
- 搜索引擎随机跳转分页的业务场景
search_after 查询
search_after 避免使用昂贵的 from,它使用上一页最后一个结果的排序值作为起点来获取下一页。查询时需要指定上一页最后一个文档的排序值。ES 创建一个时间点 Point In Time(PIT)保障搜索过程中保留特定事件点的索引状态。search_after 分页查询可以简单概括为如下几个步骤
- 步骤 1:创建 PIT 视图
json
POST /job_item_profile_prod20240112181802/_pit?keep_alive=1m
返回结果:
json
{
"id" : "64O1AwMjam9iX2l0ZW1fcHJvZmlsZV9wcm9kMjAyNDAxMTIxODE4MDIWMHpKY3VEanJTYnU4dXBpTGhLZjd2ZwAWTDhJS1lfME1SRUdfNldWMUIwZlNFQQAAAAAAFd1wmxZEMFZYb1RudlJIaS1seW9PWTdyZktnACNqb2JfaXRlbV9wcm9maWxlX3Byb2QyMDI0MDExMjE4MTgwMhYwekpjdURqclNidTh1cGlMaEtmN3ZnARZuSHhjd1hQRlRlV1ZPVzFlVDZmSXJBAAAAAAAXrPHvFlFpelRRcEJsUXlXZXg4VTVEZGJ6VFEAI2pvYl9pdGVtX3Byb2ZpbGVfcHJvZDIwMjQwMTEyMTgxODAyFjB6SmN1RGpyU2J1OHVwaUxoS2Y3dmcCFm02TVZRNGl4U2NxY0hpeTlQaFF3NHcAAAAAABbwjhcWUllXVWJWLXNUMDIxZnJCbGJhb3dqdwABFjB6SmN1RGpyU2J1OHVwaUxoS2Y3dmcAAA=="
}
获取数据量
json
POST /job_item_profile_prod20240112181802/_count
返回结果:
json
{
"count" : 337562,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
}
}
keep_alive=1m,代表视图保留时间是1分钟,超过1分钟执行会报错如下
json
{
"error" : {
"root_cause" : [
{
"type" : "search_context_missing_exception",
"reason" : "No search context found for id [384877637]"
}
]
},
"status" : 404
- 步骤 2:创建基础查询语句,这里要设置翻页的条件
json
GET /_search
{
"size":10,
"query": {
"match" : {
"job_name" : "java"
}
},
"pit": {
"id": "64O1AwMjam9iX2l0ZW1fcHJvZmlsZV9wcm9kMjAyNDAxMTIxODE4MDIWMHpKY3VEanJTYnU4dXBpTGhLZjd2ZwAWbTZNVlE0aXhTY3FjSGl5OVBoUXc0dwAAAAAAFvDERRZSWVdVYlYtc1QwMjFmckJsYmFvd2p3ACNqb2JfaXRlbV9wcm9maWxlX3Byb2QyMDI0MDExMjE4MTgwMhYwekpjdURqclNidTh1cGlMaEtmN3ZnARZMOElLWV8wTVJFR182V1YxQjBmU0VBAAAAAAAV3Z7zFkQwVlhvVG52UkhpLWx5b09ZN3JmS2cAI2pvYl9pdGVtX3Byb2ZpbGVfcHJvZDIwMjQwMTEyMTgxODAyFjB6SmN1RGpyU2J1OHVwaUxoS2Y3dmcCFm02TVZRNGl4U2NxY0hpeTlQaFF3NHcAAAAAABbwxEYWUllXVWJWLXNUMDIxZnJCbGJhb3dqdwABFjB6SmN1RGpyU2J1OHVwaUxoS2Y3dmcAAA==",
"keep_alive": "1m"
},
"sort": [
{"edu_level": "asc"}
]
}
PIT 和索引不能同时使用,设置了PIT,检索时候就不需要再指定索引,两者一起使用会报下面的错误
json
{
"error" : {
"root_cause" : [
{
"type" : "action_request_validation_exception",
"reason" : "Validation Failed: 1: [indices] cannot be used with point in time. Do not specify any index with point in time.;"
}
],
"type" : "action_request_validation_exception",
"reason" : "Validation Failed: 1: [indices] cannot be used with point in time. Do not specify any index with point in time.;"
},
"status" : 400
}
在每个返回文档的最后,sort
会有两个结果值,如下所示:
json
{
"pit_id" : "64O1AwMjam9iX2l0ZW1fcHJvZmlsZV9wcm9kMjAyNDAxMTIxODE4MDIWMHpKY3VEanJTYnU4dXBpTGhLZjd2ZwAWTDhJS1lfME1SRUdfNldWMUIwZlNFQQAAAAAAFd20khZEMFZYb1RudlJIaS1seW9PWTdyZktnACNqb2JfaXRlbV9wcm9maWxlX3Byb2QyMDI0MDExMjE4MTgwMhYwekpjdURqclNidTh1cGlMaEtmN3ZnARZuSHhjd1hQRlRlV1ZPVzFlVDZmSXJBAAAAAAAXrUI0FlFpelRRcEJsUXlXZXg4VTVEZGJ6VFEAI2pvYl9pdGVtX3Byb2ZpbGVfcHJvZDIwMjQwMTEyMTgxODAyFjB6SmN1RGpyU2J1OHVwaUxoS2Y3dmcCFm02TVZRNGl4U2NxY0hpeTlQaFF3NHcAAAAAABbw3WUWUllXVWJWLXNUMDIxZnJCbGJhb3dqdwABFjB6SmN1RGpyU2J1OHVwaUxoS2Y3dmcAAA==",
"took" : 15,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 10000,
"relation" : "gte"
},
"max_score" : null,
"hits" : [
{
"_index" : "job_item_profile_prod20240112181802",
"_type" : "_doc",
"_id" : "658a3a55aec19df69534f217",
"_score" : null,
"_source" : {
"id" : 30100000262389,
...
},
"sort" : [
0,
94
]
},
...
- 返回的 sort数组 [0, 94]分别代表
- 0: 这是当前文档 _source.edu_level字段的排序值。从 _source里可以看到 "edu_level": 0,所以这里就是 0
- 94 : 这是当前文档 _id字段 (658a3a55aec19df69534f217) 的内部排序表示。Elasticsearch 不会直接返回字符串形式的 _id用于 search_after,而是会将其转换成一个更高效的、可用于比较的数字或哈希值。94就是这个内部值
- Note: Elasticsearch 有一个内置的保障机制:为了确保排序结果的唯一性和分页的准确性,如果提供的排序条件不足以唯一确定所有文档的顺序,它会自动添加一个隐式的、基于文档 _id的排序条件作为最终决胜局(Tiebreaker)
步骤3:实现后续翻页
json
GET /_search
{
"size": 10,
"query": {
"match": {
"job_name": "java"
}
},
"pit": {
"id": "64O1AwMjam9iX2l0ZW1fcHJvZmlsZV9wcm9kMjAyNDAxMTIxODE4MDIWMHpKY3VEanJTYnU4dXBpTGhLZjd2ZwAWTDhJS1lfME1SRUdfNldWMUIwZlNFQQAAAAAAFd20khZEMFZYb1RudlJIaS1seW9PWTdyZktnACNqb2JfaXRlbV9wcm9maWxlX3Byb2QyMDI0MDExMjE4MTgwMhYwekpjdURqclNidTh1cGlMaEtmN3ZnARZuSHhjd1hQRlRlV1ZPVzFlVDZmSXJBAAAAAAAXrUI0FlFpelRRcEJsUXlXZXg4VTVEZGJ6VFEAI2pvYl9pdGVtX3Byb2ZpbGVfcHJvZDIwMjQwMTEyMTgxODAyFjB6SmN1RGpyU2J1OHVwaUxoS2Y3dmcCFm02TVZRNGl4U2NxY0hpeTlQaFF3NHcAAAAAABbw3WUWUllXVWJWLXNUMDIxZnJCbGJhb3dqdwABFjB6SmN1RGpyU2J1OHVwaUxoS2Y3dmcAAA==",
"keep_alive": "1m"
},
"sort": [
{"edu_level": "asc"}
],
"search_after": [0, 94]
}
后续翻页都可以借助 search_after 指定前一页的最后一个文档的 sort 字段值
- search_after 查询的优缺点
- 优点:不严格受制于 max_result_window,可以无限制往后翻页
- 缺点:只支持向后翻页,不支持随机翻页
- search_after 分页查询适用场景
- 适合手机端搜索,下拉刷新式的翻页
Scroll 遍历查询
scroll 支持全量遍历,详细操作可以参阅 Elasticsearch scroll 之滚动查询~
- Scroll 查询的优缺点
- 优点:支持全量遍历
- 缺点 :
- 单次遍历的 size 值也不能超过 max_result_window 大小
- 保留上下文需要足够的堆内存空间
- 响应时间非实时
- Note: Scroll 为旧版全量遍历方式。ES 官方推荐的现代方式是 search_after + PIT 的方式来进行全量遍历
参考文献
1\] Elasticsearch最佳生产实践,推荐收藏!