二、分布式系统中的深度分页问题
为什么分布式存储系统中对深度分页支持都不怎么友好呢?
首先我们看一下分布式存储系统中分页查询的过程。
假设在一个有 4 个主分片的索引中搜索,每页返回10条记录。
当我们请求结果的第1页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 40 个结果排序得到全部结果的前 10 个。
当我们请求第 99 页(结果从 990 到 1000),需要从每个分片中获取满足查询条件的前1000个结果,返回给协调节点, 然后协调节点对全部 4000 个结果排序,获取前10个记录。
当请求第10000页,每页10条记录,则需要先从每个分片中获取满足查询条件的前100010个结果,返回给协调节点。然后协调节点需要对全部(100010 * 分片数4)的结果进行排序,然后返回前10个记录。
可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。
这就是 web 搜索引擎对任何查询都不要返回超过 10000 个结果的原因。
三、From + Size 查询
1、准备数据
PUT user_index
{
"mappings": {
"properties": {
"id": {"type": "integer"},
"name": {"type": "keyword"}
}
}
}
POST user_index/_bulk
{ "create": { "\_id": "1" }}
{ "id":1,"name":"老万"}
{ "create": { "\_id": "2" }}
{ "id":2,"name":"老王"}
{ "create": { "\_id": "3" }}
{ "id":3,"name":"老刘"}
{ "create": { "\_id": "4" }}
{ "id":4,"name":"小明"}
{ "create": { "\_id": "5" }}
{ "id":5,"name":"小红"}
2、查询演示
无条件查询
POST user_index/_search
默认返回前10个匹配的匹配项。其中:
from:未指定,默认值是 0,注意不是1,代表当前页返回数据的起始值。
size:未指定,默认值是 10,代表当前页返回数据的条数。
指定from+size查询
POST user_index/_search
{
"from": 0,
"size": 10,
"query": {
"match\_all": {}
},
"sort": [
{"id": "asc"}
]
}
3、max_result_window
es 默认采用的分页方式是 from+ size 的形式,在深度分页的情况下,这种使用方式效率是非常低的。
比如from = 5000, size=10, es 需要在各个分片上匹配排序并得到5000*10条有效数据,然后在结果集中取最后10条
数据返回,这种方式类似于mongo的 skip + size。
除了效率上的问题,还有一个无法解决的问题是,es 目前支持最大的 skip 值是 max_result_window ,默认为 10000 。
也就是当 from + size > max_result_window 时,es 将返回错误。
POST user_index/_search
{
"from": 10000,
"size": 10,
"query": {
"match\_all": {}
},
"sort": [
{"id": "asc"}
]
}
这是ElasticSearch最简单的分页查询,但以上命令是会报错的。
报错信息,指window默认是10000。
"root\_cause": [
{
"type": "illegal\_argument\_exception",
"reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max\_result\_window] index level setting."
}
],
"type": "search\_phase\_execution\_exception",
怎么解决这个问题,首先能想到的就是调大这个window。
PUT user_index/_settings
{
"index" : {
"max\_result\_window" : 20000
}
}
然后这种方式只能暂时解决问题,当es 的使用越来越多,数据量越来越大,深度分页的场景越来越复杂时,如何解决这种问题呢?
官方建议:
避免过度使用 from 和 size 来分页或一次请求太多结果。
不推荐使用 from + size 做深度分页查询的核心原因:
- 搜索请求通常跨越多个分片,每个分片必须将其请求的命中内容以及任何先前页面的命中内容加载到内存中。
- 对于翻页较深的页面或大量结果,这些操作会显著增加内存和 CPU 使用率,从而导致性能下降或节点故障。
四、Search After 查询
search_after 参数使用上一页中的一组排序值来检索下一页的数据。
使用 search_after 需要具有相同查询和排序值的多个搜索请求。 如果在这些请求之间发生刷新,结果的顺序可能会发生变化,从而导致跨页面的结果不一致。 为防止出现这种情况,您可以创建一个时间点 (PIT) 以保留搜索中的当前索引状态。
时间点 Point In Time(PIT)保障搜索过程中保留特定事件点的索引状态。
注意⚠️:
es 给出了 search_after 的方式,这是在 >= 5.0 版本才提供的功能。
Point In Time(PIT)是 Elasticsearch 7.10 版本之后才有的新特性。
PIT的本质:存储索引数据状态的轻量级视图。
如下示例能很好的解读 PIT 视图的内涵。
#1、给索引user_index创建pit
POST /user_index/_pit?keep_alive=5m
#2、统计当前记录数 5
POST /user_index/_count
#3、根据pit统计当前记录数 5
GET /_search
{
"query": {
"match\_all": {}
},
"pit": {
"id": "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==",
"keep\_alive": "5m"
},
"sort": [
{"id": "asc"}
]
}
#4、插入一条数据
POST user_index/_bulk
{ "create": { "\_id": "6" }}
{ "id":6,"name":"老李"}
#5、数据总量 6
POST /user_index/_count
#6、根据pit统计数据总量还是 5 ,说明是根据时间点的视图进行统计。
GET /_search
{
"query": {
"match\_all": {}
},
"pit": {
"id": "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==",
"keep\_alive": "5m"
},
"sort": [
{"id": "asc"}
]
}
有了 PIT,search_after 的后续查询都是基于 PIT 视图进行,能有效保障数据的一致性。
search_after 分页查询可以简单概括为如下几个步骤。
1、获取索引的pit
POST /user_index/_pit?keep_alive=5m
2、根据pit首次查询
说明:根据pit查询的时候,不用指定索引名称。
GET /_search
{
"query": {
"match\_all": {}
},
"pit": {
"id": "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==",
"keep\_alive": "1m"
},
"sort": [
{"id": "asc"}
]
}
查询结果:返回的sort值为2.
hits" : [
{
"\_index" : "user\_index",
"\_type" : "\_doc",
"\_id" : "2",
"\_score" : null,
"\_source" : {
"id" : 2,
"name" : "老王"
},
"sort" : [
2
]
}
]
3、根据search_after和pit进行翻页查询
说明:
search_after指定为上一次查询返回的sort值。
要获得下一页结果,请使用最后一次命中的排序值(包括 tiebreaker)作为 search_after 参数重新运行先前的搜索。 如果使用 PIT,请在 pit.id 参数中使用最新的 PIT ID。 搜索的查询和排序参数必须保持不变。 如果提供,则 from 参数必须为 0(默认值)或 -1。
GET /_search
{
"size": 1,
"query": {
"match\_all": {}
},
"pit": {
"id": "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIOJ7FmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==",
"keep\_alive": "5m"
},
"sort": [
{"id": "asc"}
![img](https://img-blog.csdnimg.cn/img_convert/e92cb6cfe3b40867f1cb88f25c994f6e.png)
![img](https://img-blog.csdnimg.cn/img_convert/06e95a825691f50083882ae71be4ecda.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
RuNFRxaWI4cXFTVERhOHR3AAAAAAAAIOJ7FmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==",
"keep\_alive": "5m"
},
"sort": [
{"id": "asc"}
[外链图片转存中...(img-kr9UPRMW-1726008001463)]
[外链图片转存中...(img-IW941msW-1726008001464)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**