ElasticSearch 深度分頁(yè)詳解
來源 | OSCHINA 社區(qū)
作者 | 京東云開發(fā)者-何守優(yōu)
原文鏈接:https://my.oschina.net/u/4090830/blog/5593128
1 前言
2 from + size 分頁(yè)方式
GET /wms_order_sku/_search
{
"query": {
"match_all": {}
},
"from": 10,
"size": 20
}2.1 Query 階段

第一步:Client 發(fā)送查詢請(qǐng)求到 Server 端,Node1 接收到請(qǐng)求然后創(chuàng)建一個(gè)大小為 from + size 的優(yōu)先級(jí)隊(duì)列用來存放結(jié)果,此時(shí) Node1 被稱為 coordinating node(協(xié)調(diào)節(jié)點(diǎn));
第二步:Node1 將請(qǐng)求廣播到涉及的 shard 上,每個(gè) shard 內(nèi)部執(zhí)行搜索請(qǐng)求,然后將執(zhí)行結(jié)果存到自己內(nèi)部的大小同樣為 from+size 的優(yōu)先級(jí)隊(duì)列里;
第三步:每個(gè) shard 將暫存的自身優(yōu)先級(jí)隊(duì)列里的結(jié)果返給 Node1,Node1 拿到所有 shard 返回的結(jié)果后,對(duì)結(jié)果進(jìn)行一次合并,產(chǎn)生一個(gè)全局的優(yōu)先級(jí)隊(duì)列,存在 Node1 的優(yōu)先級(jí)隊(duì)列中。(如上圖中,Node1 會(huì)拿到 (from + size) * 6 條數(shù)據(jù),這些數(shù)據(jù)只包含 doc 的唯一標(biāo)識(shí)_id 和用于排序的_score,然后 Node1 會(huì)對(duì)這些數(shù)據(jù)合并排序,選擇前 from + size 條數(shù)據(jù)存到優(yōu)先級(jí)隊(duì)列);
2.2 Fetch 階段

第一步:Node1 根據(jù)剛才合并后保存在優(yōu)先級(jí)隊(duì)列中的 from+size 條數(shù)據(jù)的 id 集合,發(fā)送請(qǐng)求到對(duì)應(yīng)的 shard 上查詢 doc 數(shù)據(jù)詳情;
第二步:各 shard 接收到查詢請(qǐng)求后,查詢到對(duì)應(yīng)的數(shù)據(jù)詳情并返回為 Node1;(Node1 中的優(yōu)先級(jí)隊(duì)列中保存了 from + size 條數(shù)據(jù)的_id,但是在 Fetch 階段并不需要取回所有數(shù)據(jù),只需要取回從 from 到 from + size 之間的 size 條數(shù)據(jù)詳情即可,這 size 條數(shù)據(jù)可能在同一個(gè) shard 也可能在不同的 shard,因此 Node1 使用 multi-get 來提高性能)
第三步:Node1 獲取到對(duì)應(yīng)的分頁(yè)數(shù)據(jù)后,返回給 Client;
2.3 ES 示例
2.4 實(shí)現(xiàn)示例

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 小結(jié)

3 Scroll 分頁(yè)方式
3.1 執(zhí)行過程
3.2 ES 示例
GET /wms_order_sku2021_10/_search?scroll=1m
{
"query": {
"bool": {
"must": [
{
"range": {
"shipmentOrderCreateTime": {
"gte": "2021-10-04 00:00:00",
"lt": "2021-10-15 00:00:00"
}
}
}
]
}
},
"size": 20
}
GET /_search/scroll
{
"scroll":"1m",
"scroll_id" : "DnF1ZXJ5VGhlbkZldGNoIAAAAAJFQdUKFllGc2E4Y2tEUjR5VkpKbkNtdDFMNFEAAAACJj74YxZmSWhNM2tVbFRiaU9VcVpDUWpKSGlnAAAAAiY--F4WZkloTTNrVWxUYmlPVXFaQ1FqSkhpZwAAAAJMQKhIFmw2c1hwVFk1UXppbDhZcW1za2ZzdlEAAAACRUHVCxZZRnNhOGNrRFI0eVZKSm5DbXQxTDRRAAAAAkxAqEcWbDZzWHBUWTVRemlsOFlxbXNrZnN2UQAAAAImPvhdFmZJaE0za1VsVGJpT1VxWkNRakpIaWcAAAACJ-MhBhZOMmYzWVVMbFIzNkdnN1FwVXVHaEd3AAAAAifjIQgWTjJmM1lVTGxSMzZHZzdRcFV1R2hHdwAAAAIn4yEHFk4yZjNZVUxsUjM2R2c3UXBVdUdoR3cAAAACJ5db8xZxeW5NRXpHOFR0eVNBOHlOcXBGbWdRAAAAAifjIQkWTjJmM1lVTGxSMzZHZzdRcFV1R2hHdwAAAAJFQdUMFllGc2E4Y2tEUjR5VkpKbkNtdDFMNFEAAAACJj74YhZmSWhNM2tVbFRiaU9VcVpDUWpKSGlnAAAAAieXW_YWcXluTUV6RzhUdHlTQTh5TnFwRm1nUQAAAAInl1v0FnF5bk1Fekc4VHR5U0E4eU5xcEZtZ1EAAAACJ5db9RZxeW5NRXpHOFR0eVNBOHlOcXBGbWdRAAAAAkVB1Q0WWUZzYThja0RSNHlWSkpuQ210MUw0UQAAAAImPvhfFmZJaE0za1VsVGJpT1VxWkNRakpIaWcAAAACJ-MhChZOMmYzWVVMbFIzNkdnN1FwVXVHaEd3AAAAAkVB1REWWUZzYThja0RSNHlWSkpuQ210MUw0UQAAAAImPvhgFmZJaE0za1VsVGJpT1VxWkNRakpIaWcAAAACTECoShZsNnNYcFRZNVF6aWw4WXFtc2tmc3ZRAAAAAiY--GEWZkloTTNrVWxUYmlPVXFaQ1FqSkhpZwAAAAJFQdUOFllGc2E4Y2tEUjR5VkpKbkNtdDFMNFEAAAACRUHVEBZZRnNhOGNrRFI0eVZKSm5DbXQxTDRRAAAAAiY--GQWZkloTTNrVWxUYmlPVXFaQ1FqSkhpZwAAAAJFQdUPFllGc2E4Y2tEUjR5VkpKbkNtdDFMNFEAAAACJj74ZRZmSWhNM2tVbFRiaU9VcVpDUWpKSGlnAAAAAkxAqEkWbDZzWHBUWTVRemlsOFlxbXNrZnN2UQAAAAInl1v3FnF5bk1Fekc4VHR5U0E4eU5xcEZtZ1EAAAACTECoRhZsNnNYcFRZNVF6aWw4WXFtc2tmc3ZR"
}
3.3 實(shí)現(xiàn)示例

protected <T> Page<T> searchPageByConditionWithScrollId(BoolQueryBuilder queryParam, Class<T> targetClass, Page<T> page) throws IllegalAccessException, InstantiationException, InvocationTargetException {
SearchResponse scrollResp = null;
String scrollId = ContextParameterHolder.get("scrollId");
if (scrollId != null) {
scrollResp = getTransportClient().prepareSearchScroll(scrollId).setScroll(new TimeValue(60000)).execute()
.actionGet();
} else {
logger.info("基于scroll的分頁(yè)查詢,scrollId為空");
scrollResp = this.prepareSearch()
.setSearchType(SearchType.QUERY_AND_FETCH)
.setScroll(new TimeValue(60000))
.setQuery(queryParam)
.setSize(page.getPageSize()).execute().actionGet();
ContextParameterHolder.set("scrollId", scrollResp.getScrollId());
}
SearchHit[] hits = scrollResp.getHits().getHits();
List<T> list = new ArrayList<T>(hits.length);
for (SearchHit hit : hits) {
T instance = targetClass.newInstance();
this.convertToBean(instance, hit);
list.add(instance);
}
page.setTotalRow((int) scrollResp.getHits().getTotalHits());
page.setResult(list);
return page;
}3.4 小結(jié)
4 Search After 分頁(yè)方式
4.1 執(zhí)行過程
后續(xù)分頁(yè)查詢以此類推…
4.2 ES 示例
GET /wms_order_sku2021_10/_search
{
"query": {
"bool": {
"must": [
{
"range": {
"shipmentOrderCreateTime": {
"gte": "2021-10-12 00:00:00",
"lt": "2021-10-15 00:00:00"
}
}
}
]
}
},
"size": 20,
"sort": [
{
"_id": {
"order": "desc"
}
},{
"shipmentOrderCreateTime":{
"order": "desc"
}
}
]
}
GET /wms_order_sku2021_10/_search
{
"query": {
"bool": {
"must": [
{
"range": {
"shipmentOrderCreateTime": {
"gte": "2021-10-12 00:00:00",
"lt": "2021-10-15 00:00:00"
}
}
}
]
}
},
"size": 20,
"sort": [
{
"_id": {
"order": "desc"
}
},{
"shipmentOrderCreateTime":{
"order": "desc"
}
}
],
"search_after": ["SO-460_152-1447931043809128448-100017918838",1634077436000]
}
4.3 實(shí)現(xiàn)示例


public <T> ScrollDto<T> queryScrollDtoByParamWithSearchAfter(
BoolQueryBuilder queryParam, Class<T> targetClass, int pageSize, String afterId,
List<FieldSortBuilder> fieldSortBuilders) {
SearchResponse scrollResp;
long now = System.currentTimeMillis();
SearchRequestBuilder builder = this.prepareSearch();
if (CollectionUtils.isNotEmpty(fieldSortBuilders)) {
fieldSortBuilders.forEach(builder::addSort);
}
builder.addSort("_id", SortOrder.DESC);
if (StringUtils.isBlank(afterId)) {
log.info("queryScrollDtoByParamWithSearchAfter基于afterId的分頁(yè)查詢,afterId為空");
SearchRequestBuilder searchRequestBuilder = builder.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
.setQuery(queryParam).setSize(pageSize);
scrollResp = searchRequestBuilder.execute()
.actionGet();
log.info("queryScrollDtoByParamWithSearchAfter基于afterId的分頁(yè)查詢,afterId 為空,searchRequestBuilder:{}", searchRequestBuilder);
} else {
log.info("queryScrollDtoByParamWithSearchAfter基于afterId的分頁(yè)查詢,afterId=" + afterId);
Object[] afterIds = JSON.parseObject(afterId, Object[].class);
SearchRequestBuilder searchRequestBuilder = builder.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
.setQuery(queryParam).searchAfter(afterIds).setSize(pageSize);
log.info("queryScrollDtoByParamWithSearchAfter基于afterId的分頁(yè)查詢,searchRequestBuilder:{}", searchRequestBuilder);
scrollResp = searchRequestBuilder.execute()
.actionGet();
}
SearchHit[] hits = scrollResp.getHits().getHits();
log.info("queryScrollDtoByParamWithSearchAfter基于afterId的分頁(yè)查詢,totalRow={}, size={}, use time:{}", scrollResp.getHits().getTotalHits(), hits.length, System.currentTimeMillis() - now);
now = System.currentTimeMillis();
List<T> list = new ArrayList<>();
if (ArrayUtils.getLength(hits) > 0) {
list = Arrays.stream(hits)
.filter(Objects::nonNull)
.map(SearchHit::getSourceAsMap)
.filter(Objects::nonNull)
.map(JSON::toJSONString)
.map(e -> JSON.parseObject(e, targetClass))
.collect(Collectors.toList());
afterId = JSON.toJSONString(hits[hits.length - 1].getSortValues());
}
log.info("es數(shù)據(jù)轉(zhuǎn)換bean,totalRow={}, size={}, use time:{}", scrollResp.getHits().getTotalHits(), hits.length, System.currentTimeMillis() - now);
return ScrollDto.<T>builder().scrollId(afterId).result(list).totalRow((int) scrollResp.getHits().getTotalHits()).build();
}4.4 小結(jié)
5 總結(jié)思考
5.1 ES 三種分頁(yè)方式對(duì)比總結(jié)

如果數(shù)據(jù)量?。╢rom+size 在 10000 條內(nèi)),或者只關(guān)注結(jié)果集的 TopN 數(shù)據(jù),可以使用 from/size 分頁(yè),簡(jiǎn)單粗暴
數(shù)據(jù)量大,深度翻頁(yè),后臺(tái)批處理任務(wù)(數(shù)據(jù)遷移)之類的任務(wù),使用 scroll 方式
數(shù)據(jù)量大,深度翻頁(yè),用戶實(shí)時(shí)、高并發(fā)查詢需求,使用 search after 方式
5.2 個(gè)人思考
在一般業(yè)務(wù)查詢頁(yè)面中,大多情況都是 10-20 條數(shù)據(jù)為一頁(yè),10000 條數(shù)據(jù)也就是 500-1000 頁(yè)。正常情況下,對(duì)于用戶來說,有極少需求翻到比較靠后的頁(yè)碼來查看數(shù)據(jù),更多的是通過查詢條件框定一部分?jǐn)?shù)據(jù)查看其詳情。因此在業(yè)務(wù)需求敲定初期,可以同業(yè)務(wù)人員商定 1w 條數(shù)據(jù)的限定,超過 1w 條的情況可以借助導(dǎo)出數(shù)據(jù)到 Excel 表,在 Excel 表中做具體的操作。
如果給導(dǎo)出中心返回大量數(shù)據(jù)的場(chǎng)景可以使用 Scroll 或 Search After 分頁(yè)方式,相比之下最好使用 Search After 方式,既可以保證數(shù)據(jù)的實(shí)時(shí)性,也具有很高的搜索性能。
總之,在使用 ES 時(shí)一定要避免深度分頁(yè)問題,要在跳頁(yè)功能實(shí)現(xiàn)和 ES 性能、資源之間做一個(gè)取舍。必要時(shí)也可以調(diào)大 max_result_window 參數(shù),原則上不建議這么做,因?yàn)?1w 條以內(nèi) ES 基本能保持很不錯(cuò)的性能,超過這個(gè)范圍深度分頁(yè)相當(dāng)耗時(shí)、耗資源,因此謹(jǐn)慎選擇此方式。
推薦閱讀
Twitter技術(shù)主管回懟馬斯克:不懂技術(shù)亂評(píng)價(jià)!馬斯克:He’s fired 業(yè)務(wù)開發(fā)時(shí),接口不能對(duì)外暴露該如何實(shí)現(xiàn)? 你應(yīng)該停止相信的關(guān)于 Linux 的 7 個(gè)神話
你好,我是程序猿DD,10年開發(fā)老司機(jī)、阿里云MVP、騰訊云TVP、出過書創(chuàng)過業(yè)、國(guó)企4年互聯(lián)網(wǎng)6年。從普通開發(fā)到架構(gòu)師、再到合伙人。一路過來,給我最深的感受就是一定要不斷學(xué)習(xí)并關(guān)注前沿。只要你能堅(jiān)持下來,多思考、少抱怨、勤動(dòng)手,就很容易實(shí)現(xiàn)彎道超車!所以,不要問我現(xiàn)在干什么是否來得及。如果你看好一個(gè)事情,一定是堅(jiān)持了才能看到希望,而不是看到希望才去堅(jiān)持。相信我,只要堅(jiān)持下來,你一定比現(xiàn)在更好!如果你還沒什么方向,可以先關(guān)注我,這里會(huì)經(jīng)常分享一些前沿資訊,幫你積累彎道超車的資本。
