elasticsearch集群查詢超10000的解決方案
前言
默認(rèn)情況下,Elasticsearch集群中每個(gè)分片的搜索結(jié)果數(shù)量限制為10000。這是為了避免潛在的性能問(wèn)題。
但是我們 在實(shí)際工作過(guò)程中時(shí)常會(huì)遇到 需要深度分頁(yè),以及查詢批量數(shù)據(jù)更新的情況
問(wèn)題:當(dāng)請(qǐng)求form + size >10000 時(shí),請(qǐng)求直接報(bào)錯(cuò)
1:修改max_result_window 參數(shù)(不推薦)
在此方案中,我們建議僅限于測(cè)試用,生產(chǎn)禁用,畢竟當(dāng)數(shù)據(jù)量大的時(shí)候,過(guò)大的數(shù)據(jù)量可能導(dǎo)致es的內(nèi)存溢出,直接崩掉,一年績(jī)效白干。
PUT wkl_test/_settings { "index":{ "max_result_window":2147483647 } }
查看索引的 settings
重新查數(shù)據(jù):
2:使用游標(biāo) scroll API
使用scroll API:scroll API可以幫助我們?cè)诓患虞d所有數(shù)據(jù)的情況下獲取所有結(jié)果。它會(huì)在后臺(tái)執(zhí)行查詢以獲取滾動(dòng)ID,并將其用于進(jìn)行后續(xù)查詢。這樣就可以一次性獲取所有結(jié)果,而不必?fù)?dān)心限制
ES語(yǔ)句查詢
在游標(biāo)方案中,我們只需要在第一次拿到游標(biāo)id,之后通過(guò)游標(biāo)就能唯一確定查詢,在這個(gè)查詢中通過(guò)我們指定的 size 移動(dòng)游標(biāo),具體操作看看下面實(shí)操。
- 游標(biāo)查詢,設(shè)置游標(biāo)有效時(shí)間,有效時(shí)間內(nèi),游標(biāo)都可以使用,過(guò)期就不行了
GET wkl_test/_search?scroll=5m { "query": { "match_all": {} }, "sort": [ { "seq": { "order": "asc" } } ], "size": 200 }
- 上面操作中通過(guò)游標(biāo)的結(jié)果返回
- 之后將_scroll_id 復(fù)制到窗口,就可以不端通過(guò)這個(gè)_scroll_id 進(jìn)行之前設(shè)置的頁(yè)數(shù)不斷翻頁(yè)
以此類推,后面每次滾屏都把前一個(gè)的scroll_id復(fù)制過(guò)來(lái)。注意到,后續(xù)請(qǐng)求時(shí)沒(méi)有了index信息,size信息等,這些都在初始請(qǐng)求中,只需要使用scroll_id和scroll兩個(gè)參數(shù)即可。
注意,此時(shí)游標(biāo)移動(dòng)了,所以我們可以通過(guò)游標(biāo)的方式不斷后移,直到移動(dòng)到我們想要的 from+size 范圍內(nèi)。再次點(diǎn)擊
java實(shí)現(xiàn)
@Test public void testScroll(){ RestHighLevelClient restHighLevelClient ; BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); boolQueryBuilder.mustNot(QueryBuilders.existsQuery("seq")); try { //滾動(dòng)查詢的Scroll,設(shè)置請(qǐng)求滾動(dòng)時(shí)間窗口時(shí)間 Scroll scroll = new Scroll(TimeValue.timeValueMillis(180000)); SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); //加入query語(yǔ)句 sourceBuilder.query(boolQueryBuilder); //每次滾動(dòng)的長(zhǎng)度 sourceBuilder.size(SIZE); //加入排序字段 sourceBuilder.sort("id", SortOrder.DESC); //構(gòu)建searchRequest //加入scroll和構(gòu)造器 SearchRequest searchRequest = new SearchRequest() .indices("wkl_test") .source(sourceBuilder) .scroll(scroll); //存儲(chǔ)scroll的list List<String> scrollIdList = new ArrayList<>(); //執(zhí)行首次檢索 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); //首次檢索返回scrollId,用于下一次的滾動(dòng)查詢 String scrollId = searchResponse.getScrollId(); //拿到hits結(jié)果 SearchHit[] hits = searchResponse.getHits().getHits(); long value = searchResponse.getHits().getTotalHits().value; //保存返回結(jié)果List大小 Long resultSize = 0L; scrollIdList.add(scrollId); try { //滾動(dòng)查詢將SearchHit封裝到result中 while (ArrayUtils.isNotEmpty(hits) && hits.length > 0) { BulkRequest bulkRequest = new BulkRequest(); JSONArray esArray = new JSONArray(); for (SearchHit hit : hits) { String sourceAsString = hit.getSourceAsString(); String index = hit.getIndex(); JSONObject jsonObject = JSONObject.parseObject(sourceAsString); String seq = jsonObject.getString("seq"); if(StringUtils.isBlank(seq) ){ esArray.add(jsonObject); String uuid = jsonObject.getString("id"); jsonObject.put("is_del",1); bulkRequest.add(new UpdateRequest(index, uuid).doc(jsonObject)); } } resultSize = resultSize+hits.length; //發(fā)送請(qǐng)求 //實(shí)時(shí)更新 bulkRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT); System.out.println(bulk.getTook()+"-------"+bulk.getItems().length); //說(shuō)明滾動(dòng)完了,返回結(jié)果即可 if (resultSize > 20000) { break; } //繼續(xù)滾動(dòng),根據(jù)上一個(gè)游標(biāo),得到這次開始查詢位置 SearchScrollRequest searchScrollRequest = new SearchScrollRequest(scrollId); searchScrollRequest.scroll(scroll); //得到結(jié)果 SearchResponse searchScrollResponse = restHighLevelClient.scroll(searchScrollRequest, RequestOptions.DEFAULT); //定位游標(biāo) scrollId = searchScrollResponse.getScrollId(); hits = searchScrollResponse.getHits().getHits(); scrollIdList.add(scrollId); } System.out.println("----徹底結(jié)束了-----"); } finally { //清理scroll,釋放資源 ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); clearScrollRequest.setScrollIds(scrollIdList); restHighLevelClient.clearScroll(clearScrollRequest, RequestOptions.DEFAULT); } } catch (Exception e) { throw new RuntimeException(e); } }
scroll API 的優(yōu)缺點(diǎn)和總結(jié)
優(yōu)缺點(diǎn):
- scroll查詢的相應(yīng)數(shù)據(jù)是非實(shí)時(shí)的,如果遍歷過(guò)程中插入新的數(shù)據(jù),是查詢不到的。并且保留上下文需要足夠的堆內(nèi)存空間。
- 相比于 from/size 和 search_after 返回一頁(yè)數(shù)據(jù),Scroll API 可用于從單個(gè)搜索請(qǐng)求中檢索大量結(jié)果。但是 scroll 滾動(dòng)遍歷查詢是非實(shí)時(shí)的,數(shù)據(jù)量大的時(shí)候,響應(yīng)時(shí)間可能會(huì)比較長(zhǎng)
適用場(chǎng)景
- 全量或數(shù)據(jù)量很大時(shí)遍歷結(jié)果數(shù)據(jù),而非分頁(yè)查詢。
- scroll方案基于快照,不能用在高實(shí)時(shí)性的場(chǎng)景下,建議用在類似數(shù)據(jù)導(dǎo)出場(chǎng)景下使用
3: search_after + PIT 深度查詢
- Search_after是 ES 5 新引入的一種分頁(yè)查詢機(jī)制,其原理幾乎就是和scroll一樣,因此代碼也幾乎是一樣的。
- 官方文檔說(shuō)明不再建議使用scroll滾動(dòng)分頁(yè)和from size分頁(yè),建議使用search_after
- search_after 分頁(yè)的方式和 scroll 搜索有一些顯著的區(qū)別,首先它是根據(jù)上一頁(yè)的最后一條數(shù)據(jù)來(lái)確定下一頁(yè)的位置,同時(shí)在分頁(yè)請(qǐng)求的過(guò)程中,如果有索引數(shù)據(jù)的增刪改查,這些變更也會(huì)實(shí)時(shí)的反映到游標(biāo)上。
不帶PIT
ES語(yǔ)句實(shí)現(xiàn)
檢索第一頁(yè)的查詢?nèi)缦滤荆?/p>
GET wkl_test/_search { "query": { "match_all": {} }, "sort": [ { "seq": { "order": "asc" } } ], "size": 200 }
上述請(qǐng)求的結(jié)果包括每個(gè)文檔的 sort 值數(shù)組。
這些 sort 值可以與 search_after 參數(shù)一起使用,以開始返回在這個(gè)結(jié)果列表之后的任何文檔。例如,我們可以使用上一個(gè)文檔的 sort 值并將其傳遞給 search_after 以檢索下一頁(yè)結(jié)果:
Java 實(shí)現(xiàn)
@Test public void testSearchAfter() throws IOException { RestHighLevelClient restHighLevelClient = es7UtilApi.getRestHighLevelClient(); MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(matchAllQueryBuilder); searchSourceBuilder.from(0); searchSourceBuilder.size(200); searchSourceBuilder.sort("seq", SortOrder.ASC); searchSourceBuilder.trackTotalHits(true); SearchRequest searchRequest = new SearchRequest() .indices("wkl_test") .source(searchSourceBuilder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); SearchHits hits = searchResponse.getHits(); long value = hits.getTotalHits().value; System.out.println("查詢到記錄數(shù)=" + value); List<JSONObject> list = new ArrayList<>(); SearchHit[] searchHists = hits.getHits(); Object[] sortValues = searchHists[searchHists.length - 1].getSortValues(); if (searchHists.length > 0) { for (SearchHit hit : searchHists) { String sourceAsString = hit.getSourceAsString(); JSONObject jsonObject = JSON.parseObject(sourceAsString); jsonObject.put("_id", hit.getId()); list.add(jsonObject); } } //往后的每次請(qǐng)求都攜帶上一次的sort_id進(jìn)行訪問(wèn)。 while (ArrayUtils.isNotEmpty(searchHists) && searchHists.length > 0){ searchSourceBuilder.searchAfter(sortValues); searchRequest.source(searchSourceBuilder); SearchResponse searchResponseAfter = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); hits = searchResponseAfter.getHits(); searchHists = hits.getHits(); sortValues = searchHists[searchHists.length - 1].getSortValues(); if (searchHists.length > 0) { for (SearchHit hit : searchHists) { String sourceAsString = hit.getSourceAsString(); JSONObject jsonObject = JSON.parseObject(sourceAsString); jsonObject.put("_id", hit.getId()); list.add(jsonObject); } } if(list.size()>20000){ break; } System.out.println("-----徹底結(jié)束了-------"); } }
問(wèn)題
「優(yōu)點(diǎn):」
無(wú)狀態(tài)查詢,可以防止在查詢過(guò)程中,數(shù)據(jù)的變更無(wú)法及時(shí)反映到查詢中。
不需要維護(hù)scroll_id,不需要維護(hù)快照,因此可以避免消耗大量的資源。
「缺點(diǎn):」
由于無(wú)狀態(tài)查詢,因此在查詢期間的變更可能會(huì)導(dǎo)致跨頁(yè)面的不一值。
排序順序可能會(huì)在執(zhí)行期間發(fā)生變化,具體取決于索引的更新和刪除。
至少需要制定一個(gè)唯一的不重復(fù)字段來(lái)排序。
它不適用于大幅度跳頁(yè)查詢,或者全量導(dǎo)出,對(duì)第N頁(yè)的跳轉(zhuǎn)查詢相當(dāng)于對(duì)es不斷重復(fù)的執(zhí)行N次search after,而全量導(dǎo)出則是在短時(shí)間內(nèi)執(zhí)行大量的重復(fù)查詢。
帶PIT
關(guān)于PIT
在7.*版本中,ES官方不再推薦使用Scroll方法來(lái)進(jìn)行深分頁(yè),而是推薦使用帶PIT的search_after來(lái)進(jìn)行查詢;
從7.*版本開始,您可以使用SEARCH_AFTER參數(shù)通過(guò)上一頁(yè)中的一組排序值檢索下一頁(yè)命中。
使用SEARCH_AFTER需要多個(gè)具有相同查詢和排序值的搜索請(qǐng)求。
如果這些請(qǐng)求之間發(fā)生刷新,則結(jié)果的順序可能會(huì)更改,從而導(dǎo)致頁(yè)面之間的結(jié)果不一致。
為防止出現(xiàn)這種情況,您可以創(chuàng)建一個(gè)時(shí)間點(diǎn)(PIT)來(lái)在搜索過(guò)程中保留當(dāng)前索引狀態(tài)。
ES語(yǔ)句實(shí)現(xiàn)
1:生成pit
#keep_alive必須要加上,它表示這個(gè)pit能存在多久,這里設(shè)置的是1分鐘 POST wkl_test/_pit?keep_alive=1m
2:在搜索請(qǐng)求中指定PIT:
在每個(gè)搜索請(qǐng)求中添加 keep_alive 參數(shù)來(lái)延長(zhǎng) PIT 的保留期,相當(dāng)于是重置了一下時(shí)間
GET _search { "query": { "match_all": {} }, "pit":{ "id":"t_yxAwEId2tsX3Rlc3QWU0hzbEJkYWNTVEd0ZGRoN0xsQVVNdwAWUGQtaXJpT0xTa2VUN0RGLXZfTlBvZwAAAAAACHG1fxY1UWNKX1RHOFMybXBaV20zbWx3enp3ARZTSHNsQmRhY1NUR3RkZGg3TGxBVU13AAA=", "keep_alive":"5m" }, "sort": [ { "seq": { "order": "asc" } } ], "size": 200 }
3:刪除PIT
DELETE _pit { "id":"t_yxAwEId2tsX3Rlc3QWU0hzbEJkYWNTVEd0ZGRoN0xsQVVNdwAWUGQtaXJpT0xTa2VUN0RGLXZfTlBvZwAAAAAACHG1fxY1UWNKX1RHOFMybXBaV20zbWx3enp3ARZTSHNsQmRhY1NUR3RkZGg3TGxBVU13AAA=" }
總結(jié)
如果數(shù)據(jù)量小(from+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 方式
到此這篇關(guān)于elasticsearch集群查詢超10000解決方案的文章就介紹到這了,更多相關(guān)elasticsearch查詢超10000內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot?調(diào)用外部接口的三種實(shí)現(xiàn)方法
Spring Boot調(diào)用外部接口的方式有多種,常見的有以下三種方式:RestTemplate、Feign 和 WebClient,本文就詳細(xì)介紹一下,感興趣的可以了解一下2023-08-08Java多線程模擬銀行系統(tǒng)存錢問(wèn)題詳解
本文將利用Java多線程模擬一個(gè)簡(jiǎn)單的銀行系統(tǒng),使用兩個(gè)不同的線程向同一個(gè)賬戶存錢。文中的示例代碼講解詳細(xì),感興趣的可以了解一下2022-09-09java中的Io(input與output)操作總結(jié)(三)
這一節(jié)我們來(lái)講Scanner類和PrintWriter類的用法,感興趣的朋友可以了解下2013-01-01SpringBoot如何實(shí)現(xiàn)緩存預(yù)熱
緩存預(yù)熱是指在 Spring Boot 項(xiàng)目啟動(dòng)時(shí),預(yù)先將數(shù)據(jù)加載到緩存系統(tǒng)(如 Redis)中的一種機(jī)制,本文主要介紹了SpringBoot如何實(shí)現(xiàn)緩存預(yù)熱,感興趣的可以了解下2024-12-12Java線上CPU內(nèi)存沖高問(wèn)題排查解決步驟
這篇文章主要介紹了Java線上CPU內(nèi)存沖高問(wèn)題排查解決步驟的相關(guān)資料,Java程序在實(shí)際生產(chǎn)過(guò)程中經(jīng)常遇到CPU或內(nèi)存使用率高的問(wèn)題,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-07-07Java中反射動(dòng)態(tài)代理接口的詳解及實(shí)例
這篇文章主要介紹了Java中反射動(dòng)態(tài)代理接口的詳解及實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-04-04SpringBoot整合EasyExcel進(jìn)行大數(shù)據(jù)處理的方法詳解
EasyExcel是一個(gè)基于Java的簡(jiǎn)單、省內(nèi)存的讀寫Excel的開源項(xiàng)目。在盡可能節(jié)約內(nèi)存的情況下支持讀寫百M(fèi)的Excel。本文將在SpringBoot中整合EasyExcel進(jìn)行大數(shù)據(jù)處理,感興趣的可以了解一下2022-05-05Java實(shí)現(xiàn)定時(shí)器的4種方法超全總結(jié)
對(duì)于一些特殊的代碼是需要定時(shí)執(zhí)行的,下面來(lái)看看定時(shí)器該如何編寫吧,下面這篇文章主要給大家介紹了關(guān)于Java實(shí)現(xiàn)定時(shí)器的4種方法,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05Java中不用第三個(gè)變量來(lái)互換兩個(gè)變量的值
在程序運(yùn)行期間,隨時(shí)可能產(chǎn)生一些臨時(shí)數(shù)據(jù),應(yīng)用程序會(huì)將這些數(shù)據(jù)保存在一些內(nèi)存單元中,每個(gè)內(nèi)存單元都用一個(gè)標(biāo)識(shí)符來(lái)標(biāo)識(shí)。這些內(nèi)存單元被稱為變量,定義的標(biāo)識(shí)符就是變量名,內(nèi)存單元中存儲(chǔ)的數(shù)據(jù)就是變量的值2021-10-10