Elasticsearch分頁解決方案

1、命令的方式作分頁java

一、常見的分頁方式:from+sizejson

elasticsearch默認採用的分頁方式是from+size的形式,可是在深度分頁的狀況下,這種使用方式的效率是很是低的,好比from=5000,size=10,es須要在各個分片上匹配排序並獲得5000*10條有效數據,而後在結果集中取最後10條數據返回。除了會遇到效率上的問題,還有一個沒法解決的問題是es目前支持最大的skip值是max_result_window默認爲10000,也就是說當from+size > max_result_window時,es將返回錯誤。api

解決方案:緩存

        問題描述:好比當客戶線上的es數據出現問題,當分頁到幾百頁的時候,es沒法返回數據,此時爲了恢復正常使用,咱們能夠採用緊急規避的方式,就是將max_result_window的值調至50000。app

curl -XPUT "127.0.0.1:9200/custm/_settings" -d 
'{ 
    "index" : { 
        "max_result_window" : 50000 
    }
}'

對於上面這種解決方案只是暫時解決問題,當es的使用愈來愈多時,數據量愈來愈大,深度分頁的場景愈來愈複雜時,可使用另外一種分頁方式scroll。curl

二、scroll方式elasticsearch

爲了知足深度分頁的場景,es提供了scroll的方式進行分頁讀取。原理上是對某次查詢生成一個遊標scroll_id,後續的查詢只須要根據這個遊標去取數據,知道結果集中返回的hits字段爲空,就表示遍歷結束。Scroll的做用不是用於實時查詢數據,由於它會對es作屢次請求,不願能作到實時查詢。它的主要做用是用來查詢大量數據或所有數據。工具

使用scroll,每次只能獲取一頁的內容,而後會返回一個scroll_id。根據返回的這個scroll_id能夠不斷地獲取下一頁的內容,因此scroll並不適用於有跳頁的情景ui

使用curl進行深度分頁讀取過程以下:url

一、 先獲取第一個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"]}'

二、在遍歷時候,拿到上一次遍歷中的_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" 
}'

三、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"]}'

三、 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"
        ]
      }
    ]
  }
}

2、java api作elasticsearch分頁

按照通常的查詢流程,好比我想查找前10條數據:

一、 客戶端請求發給某個節點

二、 節點轉發給各個分片,查詢每一個分片上的前10條數據

三、 結果返回給節點,整合數據,提取前10條

四、 返回給請求客戶端

然而當我想查詢第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());// 獲取字符串格式打印
        }
    }
}

相關文章
相關標籤/搜索