1. 程式人生 > 其它 >ElasticSearch的深度分頁

ElasticSearch的深度分頁

ElasticSearch的深度分頁

coordinate node節點

搜尋和bulk等請求可能會涉及到多個節點上的不同shard裡的資料,比如一個search請求,就需要兩個階段執行,首先第一個階段就是一個coordinating node接收到這個客戶端的search request。接著,coordinating node會將這個請求轉發給儲存相關資料的node,每個data node都會在自己本地執行這個請求操作,同時返回結果給coordinating node,接著coordinating node會將返回過來的所有的請求結果進行縮減和合並,合併為一個global結果。

每個node都是一個coordinating node。這就意味著如果一個node,將node.master,node.data,node.ingest全部設定為false,那麼它就是一個純粹的coordinating node,僅僅用於接收客戶端的請求,同時進行請求的轉發和合並。

分頁查詢的流程

以前專案中主要用的solr,當分頁到幾十萬頁的時候,就會要等2秒左右,有一定的延遲,ElasticSearch也有這樣的問題。

常見深度分頁方式 from+size
es 預設採用的分頁方式是 from+ size 的形式,在深度分頁的情況下,這種使用方式效率是非常低的,比如from = 10000, size=10,首先請求可能會請求到不包含這個index的shard的node上去,這個node就是一個coordinate node,那麼這個coordinate node就會將搜尋請求轉發到index的三個shard所在node上。 es 需要在各個分片上匹配排序並得到10010條有效資料,如果是3個shard的話,那麼協調節點就會拿到30030節點,然後對這些資料進行排序,相關度分數 ,在結果集中取最後10條資料返回,這種方式類似於mongo的 skip + size。

除了效率上的問題,還有一個無法解決的問題是,es 目前支援最大的 skip 值是 max_result_window ,預設為 10000 。也就是當 from + size > max_result_window 時,es 將返回錯誤,我們專案中是將它設定10000000。

但是這種資料量大的話,還是治標不治本,其實還可以通過scroll來實現

分頁方式 scroll

如果一次性要查出來比如10萬條資料,那麼效能會很差,此時一般會採取用scoll滾動查詢,一批一批的查,直到所有資料都查詢完處理完。

使用scoll滾動搜尋,可以先搜尋一批資料,然後下次再搜尋一批資料,以此類推,直到搜尋出全部的資料來scoll搜尋會在第一次搜尋的時候,儲存一個當時的檢視快照,之後只會基於該舊的檢視快照提供資料搜尋,如果這個期間資料變更,是不會讓使用者看到的。
採用基於_doc進行排序的方式,效能較高每次傳送scroll請求,我們還需要指定一個scoll引數,指定一個時間視窗,每次搜尋請求只要在這個時間視窗內能完成就可以了。

原理上是對某次查詢生成一個遊標 scroll_id , 後續的查詢只需要根據這個遊標去取資料,直到結果集中返回的 hits 欄位為空,就表示遍歷結束。scroll_id 的生成可以理解為建立了一個臨時的歷史快照,在此之後的增刪改查等操作不會影響到這個快照的結果。

使用 curl 進行分頁讀取過程如下:

先獲取第一個 scroll_id,url 引數包括 /index/_type/ 和 scroll,scroll 欄位指定了scroll_id 的有效生存期,以分鐘為單位,過期之後會被es 自動清理。如果文件不需要特定排序,可以指定按照文件建立的時間返回會使迭代更高效。

{
  "query": {
    "match_all": {}
  },
  "sort": [ "_doc" ],
  "size": 3
}

返回的結果如下:

後續的文件讀取上一次查詢返回的scroll_id 來不斷的取下一頁,如果srcoll_id 的生存期很長,那麼每次返回的 scroll_id 都是一樣的,直到該 scroll_id 過期,才會返回一個新的 scroll_id。請求指定的 scroll_id 時就不需要 /index/_type 等資訊了。每讀取一頁都會重新設定 scroll_id 的生存時間,所以這個時間只需要滿足讀取當前頁就可以,不需要滿足讀取所有的資料的時間,1 分鐘足以。

{
    "scroll": "1m", 
    "scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAABEDCFi1PUVZzclI1VGgybjRrZlpQRU1uMkEAAAAAAARAxBYtT1FWc3JSNVRoMm40a2ZaUEVNbjJBAAAAAAAEQMEWLU9RVnNyUjVUaDJuNGtmWlBFTW4yQQAAAAAABEDFFi1PUVZzclI1VGgybjRrZlpQRU1uMkEAAAAAAARAwxYtT1FWc3JSNVRoMm40a2ZaUEVNbjJB"
    
}
    public List<T> getListByScroll(String scrollId) {
        Scroll scroll = new Scroll(TimeValue.timeValueMinutes(3L));
        SearchResponse searchResponse = null;
        JSONArray rs = new JSONArray();
        try {
            SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
            scrollRequest.scroll(scroll);
            searchResponse = getClient().scroll(scrollRequest, RequestOptions.DEFAULT);
            SearchHit[] searchHits = searchResponse.getHits().getHits();
            for (SearchHit hit : searchHits) {
                String res = hit.getSourceAsString();
                JSONObject result = JSON.parseObject(res);
                rs.add(result);
            }
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
        return rs.toJavaList(clazz);
    }

返回結果


所有文件獲取完畢之後,需要手動清理掉 scroll_id 。雖然es 會有自動清理機制,但是 srcoll_id 的存在會耗費大量的資源來儲存一份當前查詢結果集映像,並且會佔用檔案描述符。所以用完之後要及時清理。使用 es 提供的 CLEAR_API 來刪除指定的 scroll_id

刪掉指定的srcoll_id

刪除掉所有索引上的 scroll_id

    public Integer clearScroll(String scrollId) {
        Integer flag = 1;
        ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
        clearScrollRequest.addScrollId(scrollId);
        try {
            getClient().clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            flag = 0;
        }
        return flag;
    }

查詢當前所有的scroll 狀態