天天看點

分頁搜尋—Elastic Stack 實戰手冊

分頁搜尋—Elastic Stack 實戰手冊
https://developer.aliyun.com/topic/download?id=1295 · 更多精彩内容,請下載下傳閱讀全本《Elastic Stack實戰手冊》 https://developer.aliyun.com/topic/download?id=1295 https://developer.aliyun.com/topic/es100 · 加入創作人行列,一起交流碰撞,參與技術圈年度盛事吧 https://developer.aliyun.com/topic/es100

創作人:張超

在查詢場景中,從 Elasticsearch 中取得結果,根據不同場景,有多種不同的方式。

  • 通過 from 、size 進行分頁
  • 通過 scroll 拉取大量資料
  • 通過 search_after 拉取大量資料

每種方式有其各自的使用場景,或者說他們是為了解決某種場景而設計的。

from + size

搜尋引擎的場景,類似 google 搜尋,翻頁操作一般是人為觸發的,并且人的行為一般不會翻頁太多,from+size 這種最經典的翻頁模式是為了解決使用者對于 TopN 的需求,使用者希望找到 TopN 個最比對的文檔。其使用方式類似 SQL 中的 LIMIT關鍵字,Elasticserach 使用 from 和 size 兩個參數來控制翻頁:

  • size: 要傳回的結果數量,預設為 10
  • from: 要跳過的結果數量,預設為 0

如果每頁顯示 5 條結果,下面的指令可以得到 1-3 頁的結果:

GET /_search?size=5
GET /_search?size=5&from=5
GET /_search?size=5&from=10           

分頁搜尋實作方式是:

  1. 每個分片各自查詢的時候先建構 from+size 的優先隊列,然後将所有的文檔 ID 和排序值傳回給協調節點。
  2. 協調節點建立 size 為 number_of_shards * (from + size) 的優先隊列,對資料節點的傳回結果進行合并,取全局的 from+size 傳回給用戶端

這種工作模式的主要代價在于,協調節點需要等待所有分片傳回結果,然後再全局排序。是以會建立非常大的優先隊列,需要控制分頁的深度,Elasticsearch 預設最多傳回 10000 個文檔。但是有些時候,使用者需要周遊取回所有文檔,甚至可以不關心排序。在資料庫中取回全部結果可以使用遊标查詢的方式,類似的概念在 Elasticsearch 中叫做 scroll。

scroll

scroll 可以用于拉取全量資料,他的工作模式不需要像 from + size 一樣全局排序,是以沒有深分頁的代價,例如 reindex 本質上就是 scroll+bulk。使用 scroll 可以簡單的在查詢語句中添加

scroll

參數:

POST /my-index-000001/_search?scroll=1m           

上面的查詢語句會傳回一個 ID,後面可以根據這個 ID 順序拉取結果:

POST /my-index-000001/_search/scroll                                                               
{
  "scroll" : "1m",                                                                 
  "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==" 
}            

Elasticsearch 将查詢後的上下文儲存在服務端,是以用戶端可以依據這個 scroll_id 依次擷取後續的資料。

這個上下文也必然有一個生命周期,因為他會一直占用服務端資源。在這個例子中,我們設定 scroll 視窗儲存 1 分鐘的時間。1 分鐘之内,你都可以使用 scroll_id 來拉取資料。當然,你也可以在拉取完成之後根據 scroll_id 手工清理上下文。

search_after

如果說 scroll 是把上下文儲存在服務端,而 search_after 要求資料中存在一個無重複,可以用于排序的字段,需要用戶端每次傳入上次查到的最後結果,然後擷取其随後的資料。

由于随後的請求每次都是查詢出來的,如果資料發生變化,就可能出現跨頁面結果不一緻的情況,為了防止這種情況,需要在請求中加一個參數來設定目前的索引狀态保留時間。

POST /my-index-000001/_pit?keep_alive=1m           

PIT 是 point in time 的簡寫,他是一個輕量級的視圖。上述請求傳回一個 ID:

{
  "id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}           

随後的請求中你需要帶上他。可以通過下面的方式,擷取第一頁的結果,其中的特别之處在于,要指定進行排序的字段:

GET /_search
{
  "size": 10000,
  "query": {
    "match" : {
      "user.id" : "elkbee"
    }
  },
  "pit": {
        "id":  "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==", 
        "keep_alive": "1m"
  },
  "sort": [ 
    {"@timestamp": "asc"}
  ]
}
           

在傳回結果中,會攜帶 sort 字段的值,

{
  "hits" : {
    "hits" : [
      {
        "sort" : [
          4294967298                              
        ]
      }
    ]
  }
}
           

你需要在下次請求的時候帶上他:

GET /_search
{
  "size": 10000,
  "query": ...
  "pit": {
        "id":  ...
        "keep_alive": ...
  },
  "sort": [
    {"@timestamp": "asc"}
  ],
  "search_after": [                                
    4294967298
  ]                    
}
           

類似 scroll,pit 請求傳回的 ID 也可以手工清理。

最後我們總結一下每種分頁方式的特點:

  • from+size 支援跳頁,不适合深分頁;
  • scroll 不支援跳頁,适合拉取大量資料,不适合大量并發
  • search_after 不支援跳頁,适合拉取大量資料

Scroll 和 search_after 都可以用于深分頁,search_after 需要提供一個主鍵字段進行排序,預設為

_shard_doc

,它是 shard index 與 Lucene 内部 ID 的組合值。在服務端儲存的上下文要比 scroll 小,目前官方推薦使用 search_after 代替 scroll。