一、命令的方式做分頁
1、常見的分頁方式:from+size
elasticsearch默認采用的分頁方式是from+size的形式,但是在深度分頁的情況下,這種使用方式的效率是非常低的,比如from=5000,size=10,es需要在各個分片上匹配排序並得到5000*10條有效數據,然后在結果集中取最后10條數據返回。除了會遇到效率上的問題,還有一個無法解決的問題是es目前支持最大的skip值是max_result_window默認為10000,也就是說當from+size > max_result_window時,es將返回錯誤。
解決方案:
問題描述:比如當客戶線上的es數據出現問題,當分頁到幾百頁的時候,es無法返回數據,此時為了恢復正常使用,我們可以采用緊急規避的方式,就是將max_result_window的值調至50000。
curl -XPUT "127.0.0.1:9200/custm/_settings" -d '{ "index" : { "max_result_window" : 50000 } }'
對於上面這種解決方案只是暫時解決問題,當es的使用越來越多時,數據量越來越大,深度分頁的場景越來越復雜時,可以使用另一種分頁方式scroll。
2、scroll方式
為了滿足深度分頁的場景,es提供了scroll的方式進行分頁讀取。原理上是對某次查詢生成一個游標scroll_id,后續的查詢只需要根據這個游標去取數據,知道結果集中返回的hits字段為空,就表示遍歷結束。Scroll的作用不是用於實時查詢數據,因為它會對es做多次請求,不肯能做到實時查詢。它的主要作用是用來查詢大量數據或全部數據。
使用scroll,每次只能獲取一頁的內容,然后會返回一個scroll_id。根據返回的這個scroll_id可以不斷地獲取下一頁的內容,所以scroll並不適用於有跳頁的情景
使用curl進行深度分頁讀取過程如下:
1、 先獲取第一個scroll_id,url參數包括/index/type和scroll,scroll字段指定了scroll_id的有效生存時間,過期后會被es自動清理。
[root@master ~]# curl -H "Content-Type: application/json" -XGET '192.168.200.100:9200/chuyun/_search?pretty&scroll=2m' -d' {"query":{"match_all":{}}, "sort": ["_doc"]}'
2、在遍歷時候,拿到上一次遍歷中的_scroll_id,然后帶scroll參數,重復上一次的遍歷步驟,直到返回的數據為空,表示遍歷完成。
每次都要傳參數scroll,刷新搜索結果的緩存時間,另外不需要指定index和type(不要把緩存的時時間設置太長,占用內存)后續查詢:
curl -H "Content-Type: application/json" -XGET '192.168.200.100:9200/_search/scroll?pretty' -d' { "scroll" : "2m", "scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAABWFm43cDd3eERJVHNHMHJzSlNkajdPUHcAAAAAAAAAVxZuN3A3d3hESVRzRzByc0pTZGo3T1B3AAAAAAAAAFsWazlvUFptQnNTdXlmNmZRTl80cVdCdwAAAAAAAABVFm43cDd3eERJVHNHMHJzSlNkajdPUHcAAAAAAAAAWhZrOW9QWm1Cc1N1eWY2ZlFOXzRxV0J3" }'
3、scroll的刪除
刪除所有scroll_id
curl -XDELETE 192.168.200.100:9200/_search/scroll/_all
指定scroll_id刪除:
curl -XDELETE 192.168.200.100:9200/_search/scroll -d '{"scroll_id" : ["cXVlcnlBbmRGZXRjaDsxOzg3OTA4NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7"]}'
3、 search_after 的方式
使用search_after必須要設置from=0。 這里我使用_id作為唯一值排序。 我們在返回的最后一條數據里拿到sort屬性的值傳入到search_after。
數據:
scroll的方式,官方不建議用於實時的請求(一般用於數據導出),因為每一個scroll_id不僅會占用大量的資源,而且會生成歷史快照,對於數據的變更不會反映到快照上。而search_after分頁的方式是根據上一頁的最后一條數據來確定下一頁的位置,同時再分頁請求的過程中,如果有索引數據的增刪改查,這些變更也會實時的反映到游標上。但是需要注意,因為每一頁的數據依賴於上一頁的最后一條數據,所以沒法跳頁請求。
為了找到每一頁最后一條數據,每個文檔那個必須有一個全局唯一值,官方推薦使用_uuid作為全局唯一值,當然在業務上的id也可以。
例如:在下面實例中我先根據id做倒序排列:
curl -H "Content-Type: application/json" -XGET '192.168.200.100:9200/chuyun/_search?pretty' -d' { "size": 2, "from": 0, "sort": [ { "_id": { "order": "desc" } } ] }'
結果:
[root@master ~]# curl -H "Content-Type: application/json" -XGET '192.168.200.100:9200/chuyun/_search?pretty' -d' > { > "size": 2, > "from": 0, > "sort": [ > { > "_id": { > "order": "desc" > } > } > ] > }' { "took" : 7, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 3, "max_score" : null, "hits" : [ { "_index" : "chuyun", "_type" : "article", "_id" : "3", "_score" : null, "_source" : { "id" : 3, "title" : "《青玉案·元夕》", "content" : "東風夜放花千樹,更吹落,星如雨。寶馬雕車香滿路。鳳簫聲動,玉壺光轉,一夜魚龍舞。蛾兒雪柳黃金縷,笑語盈盈暗香去。眾里尋他千百度,驀然回首,那人卻在,燈火闌珊處。", "viewCount" : 786, "createTime" : 1557471088252, "updateTime" : 1557471088252 }, "sort" : [ "3" ] }, { "_index" : "chuyun", "_type" : "article", "_id" : "2", "_score" : null, "_source" : { "id" : 2, "title" : "《蝶戀花》", "content" : "佇倚危樓風細細,望極春愁,黯黯生天際。草色煙光殘照里,無言誰會憑闌意。擬把疏狂圖一醉,對酒當歌,強樂還無味。衣帶漸寬終不悔,為伊消得人憔悴。", "viewCount" : null, "createTime" : 1557471087998, "updateTime" : 1557471087998 }, "sort" : [ "2" ] } ] } }
使用sort返回的值搜索下一頁:
curl -H "Content-Type: application/json" -XGET '192.168.200.100:9200/chuyun/_search?pretty' -d' { "size": 2, "from": 0, "search_after": [ 2 ], "sort": [ { "_id": { "order": "desc" } } ] }'
結果:
[root@master ~]# curl -H "Content-Type: application/json" -XGET '192.168.200.100:9200/chuyun/_search?pretty' -d' > { > "size": 2, > "from": 0, > "search_after": [ > 2 > ], > "sort": [ > { > "_id": { > "order": "desc" > } > } > ] > }' { "took" : 12, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 3, "max_score" : null, "hits" : [ { "_index" : "chuyun", "_type" : "article", "_id" : "1", "_score" : null, "_source" : { "id" : 1, "title" : "《蝶戀花》", "content" : "檻菊愁煙蘭泣露,羅幕輕寒,燕子雙飛去。明月不諳離恨苦,斜光到曉穿朱戶。昨夜西風凋碧樹,獨上高樓,望盡天涯路。欲寄彩箋兼尺素,山長水闊知何處?", "viewCount" : 678, "createTime" : 1557471087754, "updateTime" : 1557471087754 }, "sort" : [ "1" ] } ] } }
二、java api做elasticsearch分頁
按照一般的查詢流程,比如我想查找前10條數據:
1、 客戶端請求發給某個節點
2、 節點轉發給各個分片,查詢每個分片上的前10條數據
3、 結果返回給節點,整合數據,提取前10條
4、 返回給請求客戶端
然而當我想查詢第10條到20條的時候,就需要用到分頁查詢。
工具類:
** * 構建elasticsrarch client */ public class LowClientUtil { private static TransportClient client; public TransportClient CreateClient() throws Exception { // 先構建client System.out.println("11111111111"); Settings settings=Settings.builder() .put("cluster.name","elasticsearch1") .put("client.transport.ignore_cluster_name", true) //如果集群名不對,也能連接 .build(); //創建Client TransportClient client = new PreBuiltTransportClient(settings) .addTransportAddress( new TransportAddress( InetAddress.getByName( "192.168.200.100"), 9300)); return client; } }
准備數據:
/** * 准備數據 * @throws Exception */ public static void createDocument100() throws Exception { for (int i = 1; i <= 100; i++) { try { HashMap<String, Object> map = new HashMap<>(); map.put("title", "第" + i + "本書"); map.put("author", "作者" + i); map.put("id", i); map.put("message", i + "是英國物理學家斯蒂芬·霍金創作的科學著作,首次出版於1988年。全書"); IndexResponse response = client.prepareIndex("blog2", "article") .setSource(map) .get(); // 索引名稱 String _index = response.getIndex(); // 類型 String _type = response.getType(); // 文檔ID String _id = response.getId(); // 版本 long _version = response.getVersion(); // 返回的操作狀態 RestStatus status = response.status(); System.out.println("索引名稱:" + _index + " " + "類型 :" + _type + " 文檔ID:" + _id + " 版本 :" + _version + " 返回的操作狀態:" + status ); } catch (Exception e) { e.printStackTrace(); } } }
淺分頁:from_size
原理:就比如查詢前20條數據,然后截斷前10條,只返回10-20條。
/** * from-size searchRequestBuilder 的 setFrom【從0開始】 和 setSize【查詢多少條記錄】方法實現 * */ public static void sortPages(){ // 搜索數據 SearchRequestBuilder searchRequestBuilder = client.prepareSearch("blog2").setTypes("article") .setQuery(QueryBuilders.matchAllQuery());//默認每頁10條記錄 final long totalHits = searchRequestBuilder.get().getHits().getTotalHits();//總條數 final int pageDocument = 10 ;//每頁顯示多少條 final long totalPage = totalHits / pageDocument;//總共分多少頁 for(int i=1;i<=totalPage;i++){ System.out.println("=====================當前打印的是第 :"+i+" 頁=============="); //setFrom():從第幾條開始檢索,默認是0。 //setSize():查詢多少條文檔。 searchRequestBuilder.setFrom(i*pageDocument).setSize(pageDocument); SearchResponse searchResponse = searchRequestBuilder.get(); SearchHits hits = searchResponse.getHits(); Iterator<SearchHit> iterator = hits.iterator(); while (iterator.hasNext()) { SearchHit searchHit = iterator.next(); // 每個查詢對象 System.out.println(searchHit.getSourceAsString()); // 獲取字符串格式打印 } } }
使用scroll深分頁:
對於上面介紹的淺分頁(from-size),當Elasticsearch響應請求時,它必須確定docs的順序,排列響應結果。
如果請求的頁數較少(假設每頁20個docs), Elasticsearch不會有什么問題,但是如果頁數較大時,比如請求第20頁,Elasticsearch不得不取出第1頁到第20頁的所有docs,再去除第1頁到第19頁的docs,得到第20頁的docs。
解決的方式就是使用scroll,scroll就是維護了當前索引段的一份快照信息--緩存(這個快照信息是你執行這個scroll查詢時的快照)在這個查詢后的任何新索引進來的數據,都不會在這個快照中查詢到。但是它相對於from和size,不是查詢所有數據然后剔除不要的部分,而是記錄一個讀取的位置,保證下一次快速繼續讀取。
可以把 scroll 分為初始化和遍歷兩步:
1、初始化時將所有符合搜索條件的搜索結果緩存起來,可以想象成快照;
2、遍歷時,從這個快照里取數據,也就是說,在初始化后對索引插入、刪除、更新數據都不會影響遍歷結果
public static void scrollPages(){ //獲取Client對象,設置索引名稱,搜索類型(SearchType.SCAN)[5.4移除,對於java代碼,直接返回index順序,不對結果排序],搜索數量,發送請求 SearchResponse searchResponse = client .prepareSearch("blog2") .setSearchType(SearchType.DEFAULT)//執行檢索的類別 .setSize(10).setScroll(new TimeValue(1000)).execute() .actionGet();//注意:首次搜索並不包含數據 //獲取總數量 long totalCount=searchResponse.getHits().getTotalHits(); int page=(int)totalCount/(10);//計算總頁數 System.out.println("總頁數: ================="+page+"============="); for (int i = 1; i <= page; i++) { System.out.println("=========================頁數:"+i+"=================="); searchResponse = client .prepareSearchScroll(searchResponse.getScrollId())//再次發送請求,並使用上次搜索結果的ScrollId .setScroll(new TimeValue(1000)).execute() .actionGet(); SearchHits hits = searchResponse.getHits(); for(SearchHit searchHit : hits){ System.out.println(searchHit.getSourceAsString());// 獲取字符串格式打印 } } }