Elasticsearch分頁解決方案


一、命令的方式做分頁

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());// 獲取字符串格式打印
        }
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM