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"}    
    ],
  "search\_after": [                                
    2
  ]
}

优缺点分析

search_after 查询仅支持向后翻页。
不严格受制于 max_result_window,可以无限制往后翻页。
单次请求值不能超过 max_result_window;但总翻页结果集可以超过。

思考🤔???

1、为什么采用search_after 查询能解决深度分页的问题?
2、search_after + pit分页查询过程中,PIT视图过期怎么办?
3、search_after 查询,如果需要回到前几页怎么办?

五、Scroll 遍历查询

ES官方不再推荐使用Scroll API 进行深度分页。 如果您需要在分页超过 10,000 个点击时保留索引状态,请使用带有时间点 (PIT) 的 search_after 参数。
相比于 From + size 和 search_after 返回一页数据,Scroll API 可用于从单个搜索请求中检索大量结果(甚至所有结果),其方式与传统数据库中游标(cursor)类似。

Scroll API 原理上是对某次查询生成一个游标 scroll_id , 后续的查询只需要根据这个游标去取数据,直到结果集中返回的 hits 字段为空,就表示遍历结束。scroll_id 的生成可以理解为建立了一个临时的历史快照,在此之后的增删改查等操作不会影响到这个快照的结果。

所有文档获取完毕之后,需要手动清理掉 scroll_id 。虽然es 会有自动清理机制,但是 srcoll_id 的存在会耗费大量的资源来保存一份当前查询结果集映像,并且会占用文件描述符。所以用完之后要及时清理。使用 es 提供的 CLEAR_API 来删除指定的 scroll_id

1、首次查询,并获取_scroll_id

POST /user_index/_search?scroll=1m
{
  "size": 1,
  "query": {
        "match\_all": {}
    }
}

返回结果:

{
  "\_scroll\_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EAAAAAACDlQBZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3",
  "took" : 0,
  "timed\_out" : false,
  "\_shards" : {