elasticsearch集群查詢超10000的解決方案
前言
默認情況下,Elasticsearch集群中每個分片的搜索結(jié)果數(shù)量限制為10000。這是為了避免潛在的性能問題。
但是我們 在實際工作過程中時常會遇到 需要深度分頁,以及查詢批量數(shù)據(jù)更新的情況
問題:當請求form + size >10000 時,請求直接報錯

1:修改max_result_window 參數(shù)(不推薦)
在此方案中,我們建議僅限于測試用,生產(chǎn)禁用,畢竟當數(shù)據(jù)量大的時候,過大的數(shù)據(jù)量可能導致es的內(nèi)存溢出,直接崩掉,一年績效白干。
PUT wkl_test/_settings
{
"index":{
"max_result_window":2147483647
}
}
查看索引的 settings

重新查數(shù)據(jù):

2:使用游標 scroll API
使用scroll API:scroll API可以幫助我們在不加載所有數(shù)據(jù)的情況下獲取所有結(jié)果。它會在后臺執(zhí)行查詢以獲取滾動ID,并將其用于進行后續(xù)查詢。這樣就可以一次性獲取所有結(jié)果,而不必擔心限制
ES語句查詢
在游標方案中,我們只需要在第一次拿到游標id,之后通過游標就能唯一確定查詢,在這個查詢中通過我們指定的 size 移動游標,具體操作看看下面實操。
- 游標查詢,設(shè)置游標有效時間,有效時間內(nèi),游標都可以使用,過期就不行了
GET wkl_test/_search?scroll=5m
{
"query": {
"match_all": {}
},
"sort": [
{
"seq": {
"order": "asc"
}
}
],
"size": 200
}
- 上面操作中通過游標的結(jié)果返回

- 之后將_scroll_id 復制到窗口,就可以不端通過這個_scroll_id 進行之前設(shè)置的頁數(shù)不斷翻頁
以此類推,后面每次滾屏都把前一個的scroll_id復制過來。注意到,后續(xù)請求時沒有了index信息,size信息等,這些都在初始請求中,只需要使用scroll_id和scroll兩個參數(shù)即可。
注意,此時游標移動了,所以我們可以通過游標的方式不斷后移,直到移動到我們想要的 from+size 范圍內(nèi)。再次點擊

java實現(xiàn)
@Test
public void testScroll(){
RestHighLevelClient restHighLevelClient ;
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.mustNot(QueryBuilders.existsQuery("seq"));
try {
//滾動查詢的Scroll,設(shè)置請求滾動時間窗口時間
Scroll scroll = new Scroll(TimeValue.timeValueMillis(180000));
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//加入query語句
sourceBuilder.query(boolQueryBuilder);
//每次滾動的長度
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);
//存儲scroll的list
List<String> scrollIdList = new ArrayList<>();
//執(zhí)行首次檢索
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//首次檢索返回scrollId,用于下一次的滾動查詢
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 {
//滾動查詢將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ā)送請求
//實時更新
bulkRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
System.out.println(bulk.getTook()+"-------"+bulk.getItems().length);
//說明滾動完了,返回結(jié)果即可
if (resultSize > 20000) {
break;
}
//繼續(xù)滾動,根據(jù)上一個游標,得到這次開始查詢位置
SearchScrollRequest searchScrollRequest = new SearchScrollRequest(scrollId);
searchScrollRequest.scroll(scroll);
//得到結(jié)果
SearchResponse searchScrollResponse = restHighLevelClient.scroll(searchScrollRequest, RequestOptions.DEFAULT);
//定位游標
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)缺點和總結(jié)
優(yōu)缺點:
- scroll查詢的相應(yīng)數(shù)據(jù)是非實時的,如果遍歷過程中插入新的數(shù)據(jù),是查詢不到的。并且保留上下文需要足夠的堆內(nèi)存空間。
- 相比于 from/size 和 search_after 返回一頁數(shù)據(jù),Scroll API 可用于從單個搜索請求中檢索大量結(jié)果。但是 scroll 滾動遍歷查詢是非實時的,數(shù)據(jù)量大的時候,響應(yīng)時間可能會比較長
適用場景
- 全量或數(shù)據(jù)量很大時遍歷結(jié)果數(shù)據(jù),而非分頁查詢。
- scroll方案基于快照,不能用在高實時性的場景下,建議用在類似數(shù)據(jù)導出場景下使用
3: search_after + PIT 深度查詢
- Search_after是 ES 5 新引入的一種分頁查詢機制,其原理幾乎就是和scroll一樣,因此代碼也幾乎是一樣的。
- 官方文檔說明不再建議使用scroll滾動分頁和from size分頁,建議使用search_after
- search_after 分頁的方式和 scroll 搜索有一些顯著的區(qū)別,首先它是根據(jù)上一頁的最后一條數(shù)據(jù)來確定下一頁的位置,同時在分頁請求的過程中,如果有索引數(shù)據(jù)的增刪改查,這些變更也會實時的反映到游標上。
不帶PIT
ES語句實現(xiàn)
檢索第一頁的查詢?nèi)缦滤荆?/p>
GET wkl_test/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"seq": {
"order": "asc"
}
}
],
"size": 200
}
上述請求的結(jié)果包括每個文檔的 sort 值數(shù)組。

這些 sort 值可以與 search_after 參數(shù)一起使用,以開始返回在這個結(jié)果列表之后的任何文檔。例如,我們可以使用上一個文檔的 sort 值并將其傳遞給 search_after 以檢索下一頁結(jié)果:

Java 實現(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);
}
}
//往后的每次請求都攜帶上一次的sort_id進行訪問。
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é)束了-------");
}
}
問題
「優(yōu)點:」
無狀態(tài)查詢,可以防止在查詢過程中,數(shù)據(jù)的變更無法及時反映到查詢中。
不需要維護scroll_id,不需要維護快照,因此可以避免消耗大量的資源。
「缺點:」
由于無狀態(tài)查詢,因此在查詢期間的變更可能會導致跨頁面的不一值。
排序順序可能會在執(zhí)行期間發(fā)生變化,具體取決于索引的更新和刪除。
至少需要制定一個唯一的不重復字段來排序。
它不適用于大幅度跳頁查詢,或者全量導出,對第N頁的跳轉(zhuǎn)查詢相當于對es不斷重復的執(zhí)行N次search after,而全量導出則是在短時間內(nèi)執(zhí)行大量的重復查詢。
帶PIT
關(guān)于PIT
在7.*版本中,ES官方不再推薦使用Scroll方法來進行深分頁,而是推薦使用帶PIT的search_after來進行查詢;
從7.*版本開始,您可以使用SEARCH_AFTER參數(shù)通過上一頁中的一組排序值檢索下一頁命中。
使用SEARCH_AFTER需要多個具有相同查詢和排序值的搜索請求。
如果這些請求之間發(fā)生刷新,則結(jié)果的順序可能會更改,從而導致頁面之間的結(jié)果不一致。
為防止出現(xiàn)這種情況,您可以創(chuàng)建一個時間點(PIT)來在搜索過程中保留當前索引狀態(tài)。
ES語句實現(xiàn)
1:生成pit
#keep_alive必須要加上,它表示這個pit能存在多久,這里設(shè)置的是1分鐘 POST wkl_test/_pit?keep_alive=1m

2:在搜索請求中指定PIT:
在每個搜索請求中添加 keep_alive 參數(shù)來延長 PIT 的保留期,相當于是重置了一下時間
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ù)量?。╢rom+size在10000條內(nèi)),或者只關(guān)注結(jié)果集的TopN數(shù)據(jù),可以使用from/size 分頁,簡單粗暴
數(shù)據(jù)量大,深度翻頁,后臺批處理任務(wù)(數(shù)據(jù)遷移)之類的任務(wù),使用 scroll 方式
數(shù)據(jù)量大,深度翻頁,用戶實時、高并發(fā)查詢需求,使用 search after 方式
到此這篇關(guān)于elasticsearch集群查詢超10000解決方案的文章就介紹到這了,更多相關(guān)elasticsearch查詢超10000內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot?調(diào)用外部接口的三種實現(xiàn)方法
Spring Boot調(diào)用外部接口的方式有多種,常見的有以下三種方式:RestTemplate、Feign 和 WebClient,本文就詳細介紹一下,感興趣的可以了解一下2023-08-08
java中的Io(input與output)操作總結(jié)(三)
這一節(jié)我們來講Scanner類和PrintWriter類的用法,感興趣的朋友可以了解下2013-01-01
SpringBoot整合EasyExcel進行大數(shù)據(jù)處理的方法詳解
EasyExcel是一個基于Java的簡單、省內(nèi)存的讀寫Excel的開源項目。在盡可能節(jié)約內(nèi)存的情況下支持讀寫百M的Excel。本文將在SpringBoot中整合EasyExcel進行大數(shù)據(jù)處理,感興趣的可以了解一下2022-05-05

