Elasticsearch之深度分页问题

深度分页是什么

分页问题是Elasticsearch中最常见的查询场景,一般查询语句如下:

GET /es_db_batch/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0,
  "size": 20
}

结果如下:

但是当我们的(from + size) 值特别大的时候就会出现异常:

Elasticsearch的(from + size) 大小默认限制为10000,此限制可通过更改[index.max_result_window]索引级别设置来设置。

Paginate search results | Elasticsearch Guide [7.17] | Elastic

深度分页问题

Elasticsearch分页查询流程大致如下:

  • 数据存储在各个分片中,协调节点将查询请求转发给各个节点,当各个节点执行搜索后,将排序后的前N条数据返回给协调节点。
  • 协调节点汇总各个分片返回的数据,再次排序,最终返回前N条数据给客户端。
  • 这个流程会导致一个深度分页的问题,也就是翻页越多,性能越差,甚至导致ES出现OOM。

在分布式系统中,对结果排序的成本随分页的深度成指数上升。

比如从12w人员中查询10001-100100的人员信息:

        从上面案例中可以看出,每次有序的查询都会在每个分片中执行单独的查询,然后进行数据的再次排序,而这个二次排序的过程是发生在内存中的,当单次查询的数量越大,那么堆内存中汇总的数据也就越多,对内存的压力也就越大。这里的单次查询的数据量取决于你查询的是第几条数据而不是查询了几条数据,比如你希望查询的是第10001-10100这一百条数据,但是ES必须将每个分片中的前10100全部取出进行二次查询。因此,如果查询的数据排序越靠后,就越容易导致OOM的发生,频繁的深度分页查询会导致频繁的FullGC

        ES为了避免用户在不了解其内部原理的情况下而做出错误的操作,设置了一个阈值,即max_result_window,其默认值为10000,其作用是为了保护堆内存不被错误操作导致溢出。

深度分页常见解决方案

避免使用深度分页

解决深度分页问题最好的方法就是避免使用深度分页。比如百度、Bing、谷歌在分页条中删除了跳页的功能,目的就是为了避免使用深度分页查询。

滚动搜索:Scroll Search

        scroll 滚动搜索是先搜索一批数据,然后下次再搜索下一批数据,以此类推,直到搜索出全部的数据。

        scroll 搜索会在第一次搜索的时候,保存一个当时的视图快照,之后只会基于该视图快照搜索数据,如果在搜索期间数据发生了变更,用户是看不到变更的数据的。因此,滚动查询不适合实时性要求高的搜索场景。

        注:官方已不推荐使用滚动查询进行深度分页查询,因为无法保存索引状态。

Paginate search results | Elasticsearch Guide [7.17] | Elastic

适用场景:

单个滚动搜索请求中检索大量结果,即非C端业务场景

使用:
第一次进行Scroll 查询

查询命令中新增scroll=1m,说明采用游标查询,并保持游标查询窗口1分钟,也就是本次快照的结果缓存的有效时间是1分钟。


GET /es_db_batch/_search?scroll=1m 
{
  "query": {
    "match_all": {}
  },
  "size": 2
}

查询结果中返回了前2条数据和一个游标值:_scroll_id

2)从第二次查询开始之后,每次查询都要指定_scroll_id参数:

# scroll_id 的值就是上个请求中返回的 _scroll_id 值
GET /_search/scroll
{
    "scroll": "1m", 
    "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFjZkanNHclJyU0h1dnpHQXVUZk5aSUEAAAAAAAAUehZGQmx1NzFCTVNYMm5raUR4NVF5aTJn"
}

多次根据 scroll_id 游标查询,直到没有数据返回则结束查询。采用游标查询索引全量数据,更安全高效,限制了单次对内存的消耗。

删除scroll

scroll 超时后,搜索上下文会自动删除。然而,保持scroll打开是有代价的,因此一旦不再使用,就应明确清除scroll 上下文

DELETE /_search/scroll
{
    "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFjZkanNHclJyU0h1dnpHQXVUZk5aSUEAAAAAAAAUehZGQmx1NzFCTVNYMm5raUR4NVF5aTJn"
}

DELETE /_search/scroll/_all

注意事项

  • scroll滚动查询不适合实时性要求高的查询场景,比较适合数据迁移的场景。
  • scroll查询完毕后,需要手动清理掉 scroll_id 。虽然ES有自动清理机制,但是 srcoll_id 存在会耗费大量的资源来保存一份当前查询结果集映像,并且会占用文件描述符。

官方建议:ES7之后,不再建议使用scroll API进行深度分页。如果要分页检索超过 Top 10,000+ 结果时,推荐使用:PIT + search_after。

search_after

        scroll 适用于高效的深度滚动,但滚动上下文成本高昂,不建议将其用于实时用户请求。而search_after参数通过提供一个活动光标来规避这个问题。这样可以使用上一页的结果来帮助检索下一页。

        search_after 分页查询可以简单概括为如下几个步骤:

获取索引的pit

        使用 search_after 需要具有相同查询和排序值的多个搜索请求。 如果在这些请求之间发生刷新,结果的顺序可能会发生变化,从而导致跨页面的结果不一致。 为防止出现这种情况,可以创建一个时间点 (PIT) 以保留搜索中的当前索引状态。Point In Time(PIT)是 Elasticsearch 7.10 版本之后才有的新特性。

# 创建一个时间点(PIT)来保存搜索期间的当前索引状态
POST /es_db/_pit?keep_alive=1m

根据pit首次查询

根据pit查询的时候,不用指定索引的名词

GET /_search
{
  "query": {
        "match_all": {}
    },
  "pit": {
        "id":  "5-C1AwELZXNfZGJfYmF0Y2gWTGtjZTRlWkRUOUtsQ3prOHlGVzc0ZwAWRkJsdTcxQk1TWDJua2lEeDVReWkyZwAAAAAAAAAWohY2ZGpzR3JSclNIdXZ6R0F1VGZOWklBAAEWTGtjZTRlWkRUOUtsQ3prOHlGVzc0ZwAA", 
        "keep_alive": "1m"
  },
  "size": 2, 
  "sort": [
        {"_id": "asc"}    
    ]
}

根据search_after和pit进行翻页查询

        要获得下一页结果,请使用最后一次命中的排序值(包括 tiebreaker)作为 search_after 参数重新运行先前的搜索。 如果使用 PIT,请在 pit.id 参数中使用最新的 PIT ID。 搜索的查询和排序参数必须保持不变。

#search_after指定为上一次查询返回的sort值。
GET /_search
{
  "query": {
        "match_all": {}
    },
  "pit": {
        "id":  "5-C1AwELZXNfZGJfYmF0Y2gWTGtjZTRlWkRUOUtsQ3prOHlGVzc0ZwAWRkJsdTcxQk1TWDJua2lEeDVReWkyZwAAAAAAAAAWohY2ZGpzR3JSclNIdXZ6R0F1VGZOWklBAAEWTGtjZTRlWkRUOUtsQ3prOHlGVzc0ZwAA", 
        "keep_alive": "1m"
  },
  "size": 2, 
  "sort": [
        {"_id": "asc"}    
    ],
  "search_after": [                                
    5
  ]
}

总结 

分页方式

性能

优点

缺点

适用场景

(from + size)

灵活性好,实现简单,支持随机翻页

受制于max_result_window设置,不能无限制翻页;

存在深度分页问题,越往后分页越慢。

数据量比较小,

能容忍深度分页问题

scroll

解决了深度分页问题

scroll 查询的相应数据是非实时的,如果遍历过程中插入新的数据,查询不到;

保留上下文需要足够的堆内存空间。

海量数据的导出,需要查询海量结果集的数据

search_after

性能最好,不存在深度分页问题,能够反映数据的实时变更

实现复杂,需要有一个全局唯一的字段连续分页的实现会比较复杂,因为每一次查询都需要上次查询的结果,不适用于大幅度跳页查询

海量数据的分页

  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Elasticsearch 中使用深度分页功能需要注意以下几点: 1. 尽量避免使用深度分页功能,因为它会增加网络和计算开销,可能导致性能问题。 2. 深度分页功能是通过设置 from 和 size 参数来实现的。from 参数表示从哪个位置开始查询,size 参数表示每页返回的文档数量。 3. Elasticsearch 默认最多只能返回 10000 条记录,如果需要查询更多的记录,需要设置 index.max_result_window 参数。但是设置太大会占用过多的内存,影响性能。 下面是一个 Java 实现 Elasticsearch 分页查询的示例代码: ``` import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchType; import org.elasticsearch.client.Client; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; public class ESQuery { private Client client; public ESQuery(Client client) { this.client = client; } public void search(String index, String type, int from, int size) { SearchResponse response = client.prepareSearch(index) .setTypes(type) .setQuery(QueryBuilders.matchAllQuery()) .addSort(SortBuilders.fieldSort("_id").order(SortOrder.DESC)) .setSearchType(SearchType.DFS_QUERY_THEN_FETCH) .setFrom(from) .setSize(size) .execute() .actionGet(); SearchHits hits = response.getHits(); for (SearchHit hit : hits) { System.out.println(hit.getSourceAsString()); } } } ``` 调用示例: ``` ESQuery esQuery = new ESQuery(client); esQuery.search("my_index", "my_type", 0, 10); // 查询第一页,每页10条记录 esQuery.search("my_index", "my_type", 10, 10); // 查询第二页,每页10条记录,从第11条记录开始 ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值