批量更高效
與mget能同時允許幫助我們獲取多個文檔相同,bulk API可以幫助我們同時完成執行多個請求,比如:create,index, update以及delete。當你在處理類似于log等海量數據的時候,你就可以一下處理成百上千的請求,這個操作將會極大提高效率。
bulk的請求主體的格式稍微有些不同:
~~~
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
...
~~~
這種格式就類似于一個用"\n"字符來連接的單行json一樣。下面是兩點注意事項:
每一行都結尾處都必須有換行字符"\n",最后一行也要有。這些標記可以有效地分隔每行。
這些行里不能包含非轉義字符,以免干擾數據的分析 — — 這也意味著JSON不能是pretty-printed樣式。
TIP
在《bulk格式》一章中,我們將解釋為何bulk API要使用這種格式。
action/metadata 行指定了將要在哪個文檔中執行什么操作。
其中action必須是index, create, update或者delete。metadata 需要指明需要被操作文檔的_index, _type以及_id,例如刪除命令就可以這樣填寫:
`{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}`
在你進行index以及create操作時,request body 行必須要包含文檔的_source數據——也就是文檔的所有內容。
同樣,在執行update API: doc, upsert,script的時候,也需要包含相關數據。而在刪除的時候就不需要request body行。
~~~
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }
~~~
如果沒有指定_id,那么系統就會自動生成一個ID:
~~~
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }
~~~
完成以上所有請求的bulk如下:
POST /_bulk
~~~
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }} <1>
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict" : 3} }
{ "doc" : {"title" : "My updated blog post"} } <2>
~~~
注意delete操作是如何處理request body的,你可以在它之后直接執行新的操作。
請記住最后有換行符
Elasticsearch會返回含有items的列表、它的順序和我們請求的順序是相同的:
~~~
{
"took": 4,
"errors": false, <1>
"items": [
{ "delete": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 2,
"status": 200,
"found": true
}},
{ "create": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 3,
"status": 201
}},
{ "create": {
"_index": "website",
"_type": "blog",
"_id": "EiwfApScQiiy7TIKFxRCTw",
"_version": 1,
"status": 201
}},
{ "update": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 4,
"status": 200
}}
]
}}
~~~
所有的請求都被成功執行。
每一個子請求都會被單獨執行,所以一旦有一個子請求失敗了,并不會影響到其他請求的成功執行。如果一旦出現失敗的請求,error就會變為true,詳細的錯誤信息也會出現在返回內容的下方:
POST /_bulk
~~~
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "Cannot create - it already exists" }
{ "index": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "But we can update it" }
~~~
請求中的create操作失敗,因為123已經存在,但是之后針對文檔123的index操作依舊被成功執行:
~~~
{
"took": 3,
"errors": true, <1>
"items": [
{ "create": {
"_index": "website",
"_type": "blog",
"_id": "123",
"status": 409, <2>
"error": "DocumentAlreadyExistsException <3>
[[website][4] [blog][123]:
document already exists]"
}},
{ "index": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 5,
"status": 200 <4>
}}
]
}
~~~
至少有一個請求錯誤發生。
這條請求的狀態碼為409 CONFLICT。
錯誤信息解釋了導致錯誤的原因。
第二條請求的狀態碼為200 OK。
這也更好地解釋了bulk請求是獨立的,每一條的失敗與否 都不會影響到其他的請求。
能省就省
或許你在批量導入大量的數據到相同的index以及type中。每次都去指定每個文檔的metadata是完全沒有必要的。在mget API中,bulk請求可以在URL中聲明/_index 或者/_index/_type:
POST /website/_bulk
~~~
{ "index": { "_type": "log" }}
{ "event": "User logged in" }
~~~
你依舊可以在metadata行中使用_index以及_type來重寫數據,未聲明的將會使用URL中的配置作為默認值:
POST /website/log/_bulk
~~~
{ "index": {}}
{ "event": "User logged in" }
{ "index": { "_type": "blog" }}
{ "title": "Overriding the default type" }
~~~
最大有多大?
整個數據將會被處理它的節點載入內存中,所以如果請求量很大的話,留給其他請求的內存空間將會很少。bulk應該有一個最佳的限度。超過這個限制后,性能不但不會提升反而可能會造成宕機。
最佳的容量并不是一個確定的數值,它取決于你的硬件,你的文檔大小以及復雜性,你的索引以及搜索的負載。幸運的是,這個平衡點 很容易確定:
試著去批量索引越來越多的文檔。當性能開始下降的時候,就說明你的數據量太大了。一般比較好初始數量級是1000到5000個文檔,或者你的文檔很大,你就可以試著減小隊列。 有的時候看看批量請求的物理大小是很有幫助的。1000個1KB的文檔和1000個1MB的文檔的差距將會是天差地別的。比較好的初始批量容量是5-15MB。