ElasticSearch 深度分页示例解析
深巷的猫 人气:01 前言
ElasticSearch 是一个实时的分布式搜索与分析引擎,常用于大量非结构化数据的存储和快速检索场景,具有很强的扩展性。纵使其有诸多优点,在搜索领域远超关系型数据库,但依然存在与关系型数据库同样的深度分页问题,本文就此问题做一个实践性分析探讨
2 from + size 分页方式
from + size 分页方式是 ES 最基本的分页方式,类似于关系型数据库中的 limit 方式。from 参数表示:分页起始位置;size 参数表示:每页获取数据条数。例如:
GET /wms_order_sku/_search { "query": { "match_all": {} }, "from": 10, "size": 20 }
该条 DSL 语句表示从搜索结果中第 10 条数据位置开始,取之后的 20 条数据作为结果返回。这种分页方式在 ES 集群内部是如何执行的呢?在 ES 中,搜索一般包括 2 个阶段,Query 阶段和 Fetch 阶段,Query 阶段主要确定要获取哪些 doc,也就是返回所要获取 doc 的 id 集合,Fetch 阶段主要通过 id 获取具体的 doc。
2.1 Query 阶段
如上图所示,Query 阶段大致分为 3 步:
- 第一步:Client 发送查询请求到 Server 端,Node1 接收到请求然后创建一个大小为 from + size 的优先级队列用来存放结果,此时 Node1 被称为 coordinating node(协调节点);
- 第二步:Node1 将请求广播到涉及的 shard 上,每个 shard 内部执行搜索请求,然后将执行结果存到自己内部的大小同样为 from+size 的优先级队列里;
- 第三步:每个 shard 将暂存的自身优先级队列里的结果返给 Node1,Node1 拿到所有 shard 返回的结果后,对结果进行一次合并,产生一个全局的优先级队列,存在 Node1 的优先级队列中。(如上图中,Node1 会拿到 (from + size) * 6 条数据,这些数据只包含 doc 的唯一标识_id 和用于排序的_score,然后 Node1 会对这些数据合并排序,选择前 from + size 条数据存到优先级队列);
2.2 Fetch 阶段
如上图所示,当 Query 阶段结束后立马进入 Fetch 阶段,Fetch 阶段也分为 3 步:
- 第一步:Node1 根据刚才合并后保存在优先级队列中的 from+size 条数据的 id 集合,发送请求到对应的 shard 上查询 doc 数据详情;
- 第二步:各 shard 接收到查询请求后,查询到对应的数据详情并返回为 Node1;(Node1 中的优先级队列中保存了 from + size 条数据的_id,但是在 Fetch 阶段并不需要取回所有数据,只需要取回从 from 到 from + size 之间的 size 条数据详情即可,这 size 条数据可能在同一个 shard 也可能在不同的 shard,因此 Node1 使用 multi-get 来提高性能)
- 第三步:Node1 获取到对应的分页数据后,返回给 Client;
2.3 ES 示例
依据上述我们对 from + size 分页方式两阶段的分析会发现,假如起始位置 from 或者页条数 size 特别大时,对于数据查询和 coordinating node 结果合并都是巨大的性能损耗。例如:索引 wms_order_sku 有 1 亿数据,分 10 个 shard 存储,当一个请求的 from = 1000000, size = 10。在 Query 阶段,每个 shard 就需要返回 1000010 条数据的_id 和_score 信息,而 coordinating node 就需要接收 10 * 1000010 条数据,拿到这些数据后需要进行全局排序取到前 1000010 条数据的_id 集合保存到 coordinating node 的优先级队列中,后续在 Fetch 阶段再去获取那 10 条数据的详情返回给客户端。分析:这个例子的执行过程中,在 Query 阶段会在每个 shard 上均有巨大的查询量,返回给 coordinating node 时需要执行大量数据的排序操作,并且保存到优先级队列的数据量也很大,占用大量节点机器内存资源。
2.4 实现示例
private SearchHits getSearchHits(BoolQueryBuilder queryParam, int from, int size, String orderField) { SearchRequestBuilder searchRequestBuilder = this.prepareSearch(); searchRequestBuilder.setQuery(queryParam).setFrom(from).setSize(size).setExplain(false); if (StringUtils.isNotBlank(orderField)) { searchRequestBuilder.addSort(orderField, SortOrder.DESC); } log.info("getSearchHits searchBuilder:{}", searchRequestBuilder.toString()); SearchResponse searchResponse = searchRequestBuilder.execute().actionGet(); log.info("getSearchHits searchResponse:{}", searchResponse.toString()); return searchResponse.getHits(); }
2.5 小结
其实 ES 对结果窗口的返回数据有默认 10000 条的限制(参数:index.max_result_window = 10000),当 from + size 的条数大于 10000 条时 ES 提示可以通过 scroll 方式进行分页,非常不建议调大结果窗口参数值。
3 Scroll 分页方式
scroll 分页方式类似关系型数据库中的 cursor(游标),首次查询时会生成并缓存快照,返回给客户端快照读取的位置参数(scroll_id),后续每次请求都会通过 scroll_id 访问快照实现快速查询需要的数据,有效降低查询和存储的性能损耗。
3.1 执行过程
scroll 分页方式在 Query 阶段同样也是 coordinating node 广播查询请求,获取、合并、排序其他 shard 返回的数据_id 集合,不同的是 scroll 分页方式会将返回数据_id 的集合生成快照保存到 coordinating node 上。Fetch 阶段以游标的方式从生成的快照中获取 size 条数据的_id,并去其他 shard 获取数据详情返回给客户端,同时将下一次游标开始的位置标识_scroll_id 也返回。这样下次客户端发送获取下一页请求时带上 scroll_id 标识,coordinating node 会从 scroll_id 标记的位置获取接下来 size 条数据,同时再次返回新的游标位置标识 scroll_id,这样依次类推直到取完所有数据。
加载全部内容