再谈elasticsearch下的深度分页

Elasticsearch 在业务系统中使用也越来越广,一些开发规范也需要慢慢重视起来。 我们知道在关系型数据库中,我们被告知要注意甚至被明确禁止使用深度分页,在es中也应该尽量避免使用深度分页。

es提供的分页查询是通过fromsize参数来完成,from默认是0,size默认为10,比如:

{ "from" : 100000, "size" : 50, "query" : { "term" : { "user" : "alex" } } }

注: from+size不能大于 index.max_result_window 的默认设置10000

回归mysql

当我们使用深度分页,此时 select * from base_product_shop_sap limit 100000,50 时就会出现慢查询,它相当于先遍历了前100000个,然后取了第100000到100050个,舍弃了前100000个。 通常我们的优化方式是依赖覆盖索引( 只遍历索引本身-- 查询的列均是索引字段 ) ,而表现形式也不外乎以下几种:

  1. 子查询,比如 : SELECT * FROM base_product_shop_sap WHERE ID > =(select id from product limit 100000, 1) limit 50
  2. join,比如: SELECT * FROM base_product_shop_sap a JOIN (select id from base_product_shop_sap limit 100000, 50) b ON a.ID = b.id

当然,这种方法的好处性能较好,可以实现快速查询,但局限性也很明显:依赖于主键的自增长特性,不适合复杂查询条件的分页逻辑。

es分页查询原理

在es中, 搜索一般包括query 和 fetch 阶段 两个阶段,关于es默认的搜索类型为 QUERY_THEN_FETCH

之前也有分享过

query 过程:首先 Client 发送一次搜索请求,node1 接收到请求,然后,node1 创建一个大小为 from + size 的优先级队列用来存结果,我们管 node1 叫 coordinating node ;然后 coordinating node将请求广播到涉及到的 shards,每个 shard 在内部执行搜索请求,然后,将结果存到内部的大小同样为 from + size 的优先级队列里,可以把优先级队列理解为一个包含 top N 结果的列表; 每个 shard 把暂存在自身优先级队列里的数据返回给 coordinating node,coordinating node 拿到各个 shards 返回的结果后对结果进行一次合并,产生一个全局的优先级队列,存到自身的优先级队列里。

fetch 过程:首先 coordinating node 发送 GET 请求到相关shards;然后 shard 根据 doc 的 _id 取到数据详情,然后返回给 coordinating node 。其中 coordinating node 优先级队列里有 from + size 个 _docId

那上面的分页在es中执行, CPU、内存、IO和网络带宽消耗非常明显, 在 query 阶段即使是每条数据只返回_docId和 _score ,这数据量也很大了 ,而且这个数据量是很多 shards 中获取的。

既然 深度分页的请求并不合理 ( 很少人为的看很后面的请求 ),因此很多公司坚持二八原则(20%的才需要深度分页) 直接限制分页,不允许深度分页。

但是, 深度分页确实存在 ,在很多情况下无法回避,因此es官方也给出了两种解决方案:scroll 和 search_after

注:也有人提到过 search_type=scan,其实在 2.1.0 系列已经被官方废弃,可参阅

scroll

scroll 其实不难理解,有点类似关系型数据库的游标,so, scroll 并不适合用来做实时搜索,而更适用于后台批处理任务(接受明显的延迟)。

scroll 会一次性给你生成所有数据的一个快照,然后每次滑动向后翻页就是通过游标 scroll_id 移动,获取下一页下一页这样子,性能会比上面说的那种分页性能要高很多很多,基本上都是毫秒级的。

用法这里就不展开讲,具体可参阅官方文档

缺点也和明显:

  • 不能随机地跳跃分页
  • 时效性,初始化时必须指定 scroll 参数 用于指定保存搜索结果的时间,会存在超时而失败 。通常可以通过,每次请求都要传参数 scroll来刷新搜索结果的缓存时间
  • 要关注内存空间的消耗,毕竟空间有限,可通过 nodes stats API进行监控

注: scroll 分为初始化和遍历两步,初始化时将搜索结果缓存起来,可以理解为快照,在遍历时直接从这个快照里取数据,但是一旦初始化后再对索引插入、删除、更新数据都不会影响遍历结果 

searchAfter

大概从 es 5.0版本开始,es提供了新的参数 search_after 来解决分页的性能及时效性问题,search_after 提供了一个活的游标来拉取从上次返回的最后一个请求开始拉取下一页的数据。search_after有点mysql的依赖主键id的味道,它是无状态的, 可以并行的拉取大量数据, 能用于用户的实时搜索。

用法上官方文档已经足够的详细,这里再逻辑一遍:

  • 不能随机地跳跃分页
  • 依赖排序, sort参数里必须至少使用一个唯一的字段来进行排序,推荐的做法是使用 _id字段
  • 首次查询,search_after参数可以为空字符串不能为null,下次的search_after参数是上次查询结果返回的SearchAfterResult.searchAfter

这里有个小技巧,如果搜索结果有几万条,可以通过search_after来分页完成多次查询的功能,据说排序后的查询比默认查询的方式速度更快。

form&size / scroll / search_after 性能比较

曾有es专家对该话题进行了性能比较,无非是为了证明search_after 更值得推荐使用。

【1 - 10】【49000 - 49010】【 99000 - 99010】范围各10条数据(前提10w条)

 1~1049000~4901099000~99010
form/size8ms30ms117ms
scroll7ms66ms36ms
search_after5ms8ms7ms

尽管性能是非功能性需求,但它带来的挑战值得每个人去探索。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值