代價較小的批量操作
與 mget
可以使我們一次取回多個文檔同樣的方式, bulk
API 允許在單個步驟中進行多次 create
、 index
、 update
或 delete
請求。 如果你需要索引一個數據流比如日誌事件,它可以排隊和索引數百或數千批次。
bulk
與其他的請求體格式稍有不同,如下所示:
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
...
這種格式類似一個有效的單行 JSON 文檔 流 ,它通過換行符(\n
)連接到一起。注意兩個要點:
- 每行一定要以換行符(
\n
)結尾, 包括最後一行 。這些換行符被用作一個標記,可以有效分隔行。 - 這些行不能包含未轉義的換行符,因爲他們將會對解析造成干擾。這意味着這個 JSON 不 能使用 pretty 參數打印。
在 爲什麼是有趣的格式? 中, 我們解釋爲什麼 bulk
API 使用這種格式。
action/metadata
行指定 哪一個文檔 做 什麼操作 。
action
必須是以下選項之一:
create
如果文檔不存在,那麼就創建它。
index
創建一個新文檔或者替換一個現有的文檔。
update
部分更新一個文檔。
delete
刪除一個文檔。
metadata
應該 指定被索引、創建、更新或者刪除的文檔的 _index
、 _type
和 _id
。
例如,一個 delete
請求看起來是這樣的:
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
request body
行由文檔的 _source
本身組成--文檔包含的字段和值。它是 index
和 create
操作所必需的,這是有道理的:你必須提供文檔以索引。
它也是 update
操作所必需的,並且應該包含你傳遞給 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" }}
{ "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"} }
請注意 |
|
謹記最後一個換行符不要落下。 |
這個 Elasticsearch 響應包含 items
數組, 這個數組的內容是以請求的順序列出來的每個請求的結果。
{
"took": 4,
"errors": false,
"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
失敗,因爲它已經存在。但是隨後的 index
請求,也是對文檔 123
操作,就成功了:
{
"took": 3,
"errors": true,
"items": [
{ "create": {
"_index": "website",
"_type": "blog",
"_id": "123",
"status": 409,
"error": "DocumentAlreadyExistsException
[[website][4] [blog][123]:
document already exists]"
}},
{ "index": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 5,
"status": 200
}}
]
}
一個或者多個請求失敗。 |
|
這個請求的HTTP狀態碼報告爲 |
|
解釋爲什麼請求失敗的錯誤信息。 |
|
第二個請求成功,返回 HTTP 狀態碼 |
這也意味着 bulk
請求不是原子的: 不能用它來實現事務控制。每個請求是單獨處理的,因此一個請求的成功或失敗不會影響其他的請求。
不要重複指定Index和Type
也許你正在批量索引日誌數據到相同的 index
和 type
中。 但爲每一個文檔指定相同的元數據是一種浪費。相反,可以像 mget
API 一樣,在 bulk
請求的 URL 中接收默認的 /_index
或者 /_index/_type
:
POST /website/_bulk
{ "index": { "_type": "log" }}
{ "event": "User logged in" }
你仍然可以覆蓋元數據行中的 _index
和 _type
, 但是它將使用 URL 中的這些元數據值作爲默認值:
POST /website/log/_bulk
{ "index": {}}
{ "event": "User logged in" }
{ "index": { "_type": "blog" }}
{ "title": "Overriding the default type" }
多大是太大了?
整個批量請求都需要由接收到請求的節點加載到內存中,因此該請求越大,其他請求所能獲得的內存就越少。 批量請求的大小有一個最佳值,大於這個值,性能將不再提升,甚至會下降。 但是最佳值不是一個固定的值。它完全取決於硬件、文檔的大小和複雜度、索引和搜索的負載的整體情況。
幸運的是,很容易找到這個 最佳點 :通過批量索引典型文檔,並不斷增加批量大小進行嘗試。 當性能開始下降,那麼你的批量大小就太大了。一個好的辦法是開始時將 1,000 到 5,000 個文檔作爲一個批次, 如果你的文檔非常大,那麼就減少批量的文檔個數。
密切關注你的批量請求的物理大小往往非常有用,一千個 1KB 的文檔是完全不同於一千個 1MB 文檔所佔的物理大小。 一個好的批量大小在開始處理後所佔用的物理大小約爲 5-15 MB。