文檔
什么是文檔
在大多數應用中,多數實體或對象可以被序列化為包含鍵值對的 JSON 對象。 一個 鍵 可以是一個字段或字段的名稱,一個 值 可以是一個字符串,一個數字,一個布爾值, 另一個對象,一些數組值,或一些其它特殊類型諸如表示日期的字符串,或代表一個地理位置的對象:
{
"name": "John Smith",
"age": 42,
"confirmed": true,
"join_date": "2019-06-01",
"home": {
"lat": 51.5,
"lon": 0.1
},
"accounts": [
{
"type": "facebook",
"id": "johnsmith"
},
{
"type": "twitter",
"id": "johnsmith"
}
]
}
通常情況下,我們使用的術語對象
和文檔
是可以互相替換的。不過,有一個區別: 一個對象僅僅是類似於hash、hashmap 、字典或者關聯數組的JSON對象,對象中也可以嵌套其他的對象。 對象可能包含了另外一些對象。在Elasticsearch中,術語文檔
有着特定的含義。它是指最頂層或者根對象, 這個根對象被序列化成JSON並存儲到Elasticsearch中,指定了唯一ID。
需要注意的是字段的名字可以是任何合法的字符串,但不可以包含英文句號(.)。
文檔元數據
一個文檔不僅僅包含它的數據 ,也包含元數據——有關文檔的信息。 三個必須的元數據元素如下:
- _index
文檔在哪存放
- _type
文檔表示的對象類別
- _id
文檔唯一標識
還有其他的元數據,后面會陸續說到。
索引文檔
通過使用 index
API ,文檔可以被索引 —— 存儲和使文檔可被搜索 。 但是首先,我們要確定文檔的位置。一個文檔的 _index
、 _type
和 _id
唯一標識一個文檔。 我們可以提供自定義的_id
值,或者讓index
API自動生成。
使用自定義的ID
如果你的文檔有一個自然的唯一的標識符,應該使用如下方式的index
API並提供你自己的_id
PUT /{index}/{type}/{id}
{
"field": "value",
...
}
例如:
curl -X PUT "localhost:9200/website/blog/123?pretty" -H 'Content-Type: application/json' -d'
{
"title": "My first blog entry",
"text": "Just trying this out...",
"date": "2019/01/01"
}
'
Elasticsearch 響應體如下:
{
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 1,
"created": true
}
_version:
在 Elasticsearch中每個文檔都有一個版本號。當每次對文檔進行修改時(包括刪除),
_version
的值會遞增。_version
確保你的應用程序中的一部分修改不會覆蓋另一部分所做的修改。
使用Elasticsearch自動生成ID
請求的結構調整為:不再使用PUT
謂詞(“使用這個URL存儲這個文檔”),而是使用POST
謂詞(“存儲文檔在這個URL命名空間下”)。
現在該URL只需包含_index
和_type
:
POST /website/blog/
{
"title": "My second blog entry",
"text": "Still trying this out...",
"date": "2019/01/01"
}
除了_id
是Elasticsearch自動生成的,響應的其他部分和前面的類似:
{
"_index": "website",
"_type": "blog",
"_id": "AVFgSgVHUP18jI2wRx0w",
"_version": 1,
"created": true
}
自動生成的 ID 是 URL-safe、 基於Base64編碼且長度為20個字符的GUID字符串。 這些GUID字符串由可修改的FlakeID模式生成,這種模式允許多個節點並行生成唯一ID,且互相之間的沖突概率幾乎為零。
取回一個文檔
為了從Elasticsearch中檢索出文檔 ,我們仍然使用相同的_index
、_type
和_id
,但是HTTP謂詞更改為GET:
curl -X GET "localhost:9200/website/blog/123?pretty"
響應體增加了_source
字段,這個字段包含我們索引數據時發送給Elasticsearch的原始JSON文檔:
{
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 1,
"found" : true,
"_source" : {
"title": "My first blog entry",
"text": "Just trying this out...",
"date": "2019/01/01"
}
}
URL中增加pretty參數,將會調用Elasticsearch的
pretty-print
功能,該功能使JSON響應體更加可讀。但是,_source
字段不能被格式化打印出來。相反,我們得到的_source
字段中的JSON串,剛好是和我們傳給它的一樣。
GET請求的響應體包括{"found": true}
,這證實了文檔已經被找到。 如果我們請求一個不存在的文檔,我們仍舊會得到一個JSON響應體,但是found將會是false。 此外,HTTP 響應碼將會是404 Not Found,而不是 200 OK。
curl -i -XGET http://localhost:9200/website/blog/124?pretty
響應頭類似這樣:
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=UTF-8
Content-Length: 83
{
"_index" : "website",
"_type" : "blog",
"_id" : "124",
"found" : false
}
返回文檔的一部分
默認情況下,GET請求會返回整個文檔,這個文檔正如存儲在_source
字段中的一樣。但是也許你只對其中的title
字段感興趣。單個字段能用_source
參數請求得到,多個字段也能使用逗號分隔的列表來指定。
curl -X GET "localhost:9200/website/blog/123?_source=title,text&pretty"
該_source
字段現在包含的只是我們請求的那些字段,並且已經將date
字段過濾掉了。
{
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 1,
"found" : true,
"_source" : {
"title": "My first blog entry" ,
"text": "Just trying this out..."
}
}
或者,如果你只想得到_source
字段,不需要任何元數據,你能使用_source
端點:
curl -X GET "localhost:9200/website/blog/123/_source?pretty"
返回內容:
{
"title": "My first blog entry",
"text": "Just trying this out...",
"date": "2014/01/01"
}
檢查文檔是否存在
如果只想檢查一個文檔是否存在,根本不想關心內容,那么用HEAD
方法來代替GET
方法。HEAD
請求沒有返回體,只返回一個HTTP請求報頭:
curl -i -XHEAD http://localhost:9200/website/blog/123
文檔存在:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
文檔不存在:
curl -i -XHEAD http://localhost:9200/website/blog/124
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
更新整個文檔
在Elasticsearch中文檔是不可改變的,不能修改它們。相反,如果想要更新現有的文檔,需要重建索引或者進行替換, 我們可以使用相同的index
API進行實現:
curl -X PUT "localhost:9200/website/blog/123?pretty" -H 'Content-Type: application/json' -d'
{
"title": "My first blog entry",
"text": "I am starting to get the hang of this...",
"date": "2014/01/02"
}
'
在響應體中,我們能看到Elasticsearch已經增加 _version
字段值:
{
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 2,
"created": false
}
created:
created
標志設置成false,是因為相同的索引、類型和ID的文檔已經存在。
在內部,Elasticsearch已將舊文檔標記為已刪除,並增加一個全新的文檔。盡管不能再對舊版本的文檔進行訪問,但它並不會立即消失。當繼續索引更多的數據,Elasticsearch會在后台清理這些已刪除文檔。
與update API的區別
update
API雖然它似乎對文檔直接進行了修改,但實際上Elasticsearch按前述完全相同方式執行以下過程:
- 從舊文檔構建JSON
- 更改該JSON
- 刪除舊文檔
- 索引一個新文檔
唯一的區別在於,update
API僅僅通過一個客戶端請求來實現這些步驟,而不需要單獨的get
和index
請求。
創建新文檔
當我們索引一個文檔,怎么確認我們正在創建一個完全新的文檔,而不是覆蓋現有的呢?
請記住,_index
、_type
和_id
的組合可以唯一標識一個文檔。所以,確保創建一個新文檔的最簡單辦法是,使用索引請求的POST
形式讓Elasticsearch
自動生成唯一_id
:
POST /website/blog/
{ ... }
如果已經有自己的_id
,那么我們必須告訴Elasticsearch,只有在相同的_index
、_type
和_id
不存在時才接受我們的索引請求。這里有兩種方式,他們做的實際是相同的事情。使用哪種,取決於哪種使用起來更方便。
第一種方法使用op_type
:
PUT /website/blog/123?op_type=create
{ ... }
第二種方法是在URL末端使用/_create
:
PUT /website/blog/123/_create
{ ... }
如果創建新文檔的請求成功執行,Elasticsearch會返回元數據和一個201 Created
的HTTP
響應碼。
另一方面,如果具有相同的_index
、_type
和_id
的文檔已經存在,Elasticsearch將會返回 409 Conflict
響應碼,以及如下的錯誤信息:
{
"error": {
"root_cause": [
{
"type": "document_already_exists_exception",
"reason": "[blog][123]: document already exists",
"shard": "0",
"index": "website"
}
],
"type": "document_already_exists_exception",
"reason": "[blog][123]: document already exists",
"shard": "0",
"index": "website"
},
"status": 409
}
刪除文檔
刪除文檔的語法和我們所知道的規則相同,只是使用 DELETE
方法:
curl -X DELETE "localhost:9200/website/blog/123?pretty"
如果找到該文檔,Elasticsearch 將要返回一個 200 ok
的 HTTP 響應碼,和一個類似以下結構的響應體。注意,字段 _version
值已經增加:
{
"found" : true,
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 3
}
如果文檔沒有 找到,我們將得到404 Not Found
的響應碼和類似這樣的響應體:
{
"found" : false,
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 4
}
即使文檔不存在( Found
是 false ), _version
值仍然會增加。這是 Elasticsearch 內部記錄本的一部分,用來確保這些改變在跨多節點時以正確的順序執行。
同更新一樣,刪除文檔不會立即將文檔從磁盤中刪除,只是將文檔標記為已刪除狀態。隨着你不斷的索引更多的數據,Elasticsearch 將會在后台清理標記為已刪除的文檔。
處理沖突
當我們使用 index
API 更新文檔 ,可以一次性讀取原始文檔,做我們的修改,然后重新索引 整個文檔 。 最近的索引請求將獲勝:無論最后哪一個文檔被索引,都將被唯一存儲在 Elasticsearch 中。如果其他人同時更改這個文檔,他們的更改將丟失。
變更越頻繁,讀數據和更新數據的間隙越長,也就越可能丟失變更。
在數據庫領域中,有兩種方法通常被用來確保並發更新時變更不會丟失:
悲觀並發控制
這種方法被關系型數據庫廣泛使用,它假定有變更沖突可能發生,因此阻塞訪問資源以防止沖突。 一個典型的例子是讀取一行數據之前先將其鎖住,確保只有放置鎖的線程能夠對這行數據進行修改。
樂觀並發控制
Elasticsearch中使用的這種方法假定沖突是不可能發生的,並且不會阻塞正在嘗試的操作。 然而,如果源數據在讀寫當中被修改,更新將會失敗。應用程序接下來將決定該如何解決沖突。 例如,可以重試更新、使用新的數據、或者將相關情況報告給用戶。
樂觀並發控制
Elasticsearch 是分布式的。當文檔創建、更新或刪除時, 新版本的文檔必須復制到集群中的其他節點。Elasticsearch 也是異步和並發的,這意味着這些復制請求被並行發送,並且到達目的地時也許順序是亂的。 Elasticsearch需要一種方法確保文檔的舊版本不會覆蓋新的版本。
我們之前說到的 index
, GET
和 delete
請求時,我們指出每個文檔都有一個 _version
(版本)號,當文檔被修改時版本號遞增。 Elasticsearch 使用這個 _version
號來確保變更以正確順序得到執行。如果舊版本的文檔在新版本之后到達,它可以被簡單的忽略。
我們可以利用 _version
號來確保 應用中相互沖突的變更不會導致數據丟失。我們通過指定想要修改文檔的 version 號來達到這個目的。 如果該版本不是當前版本號,我們的請求將會失敗。
讓我們創建一個新的博客文章:
curl -X PUT "localhost:9200/website/blog/1/_create?pretty" -H 'Content-Type: application/json' -d'
{
"title": "My first blog entry",
"text": "Just trying this out..."
}
'
響應體告訴我們,這個新創建的文檔 _version
版本號是 1 。現在假設我們想編輯這個文檔:我們加載其數據到 web 表單中, 做一些修改,然后保存新的版本。
首先我們檢索文檔:
curl -X GET "localhost:9200/website/blog/1?pretty"
響應體包含相同的 _version
版本號 1 :
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"title": "My first blog entry",
"text": "Just trying this out..."
}
}
現在,當我們嘗試通過重建文檔的索引來保存修改,我們指定 version 為我們的修改會被應用的版本:
curl -X PUT "localhost:9200/website/blog/1?version=1&pretty" -H 'Content-Type: application/json' -d'
{
"title": "My first blog entry",
"text": "Starting to get the hang of this..."
}
'
我們想這個在我們索引中的文檔只有現在的 _version
為 1 時,本次更新才能成功。
此請求成功,並且響應體告訴我們 _version
已經遞增到 2 :
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 2
"created": false
}
然而,如果我們重新運行相同的索引請求,仍然指定 version=1 , Elasticsearch 返回 409 Conflict
HTTP 響應碼,和一個如下所示的響應體:
{
"error": {
"root_cause": [
{
"type": "version_conflict_engine_exception",
"reason": "[blog][1]: version conflict, current [2], provided [1]",
"index": "website",
"shard": "3"
}
],
"type": "version_conflict_engine_exception",
"reason": "[blog][1]: version conflict, current [2], provided [1]",
"index": "website",
"shard": "3"
},
"status": 409
}
這告訴我們在 Elasticsearch 中這個文檔的當前 _version
號是 2 ,但我們指定的更新版本號為 1 。
我們現在怎么做取決於我們的應用需求。我們可以告訴用戶說其他人已經修改了文檔,並且在再次保存之前檢查這些修改內容。
所有文檔的更新或刪除 API,都可以接受 version
參數,這允許你在代碼中使用樂觀的並發控制,這是一種明智的做法。
通過外部系統使用版本控制
一個常見的設置是使用其它數據庫作為主要的數據存儲,使用 Elasticsearch 做數據檢索, 這意味着主數據庫的所有更改發生時都需要被復制到 Elasticsearch ,如果多個進程負責這一數據同步,你可能遇到類似於之前描述的並發問題。
如果你的主數據庫已經有了版本號,或一個能作為版本號的字段值比如 timestamp,那么你就可以在 Elasticsearch 中通過增加 version_type=external
到查詢字符串的方式重用這些相同的版本號, 版本號必須是大於零的整數, 且小於 9.2E+18(一個 Java 中 long 類型的正值)。
外部版本號的處理方式和我們之前討論的內部版本號的處理方式有些不同, Elasticsearch 不是檢查當前 _version
和請求中指定的版本號是否相同, 而是檢查當前 _version
是否 小於 指定的版本號。 如果請求成功,外部的版本號作為文檔的新 _version
進行存儲。
外部版本號不僅在索引和刪除請求是可以指定,而且在 創建 新文檔時也可以指定。
例如,要創建一個新的具有外部版本號 5 的博客文章,我們可以按以下方法進行:
curl -X PUT "localhost:9200/website/blog/2?version=5&version_type=external&pretty" -H 'Content-Type: application/json' -d'
{
"title": "My first external blog entry",
"text": "Starting to get the hang of this..."
}
'
在響應中,我們能看到當前的_version
版本號是 5 :
{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 5,
"created": true
}
現在我們更新這個文檔,指定一個新的 version 號是 10 :
curl -X PUT "localhost:9200/website/blog/2?version=10&version_type=external&pretty" -H 'Content-Type: application/json' -d'
{
"title": "My first external blog entry",
"text": "This is a piece of cake..."
}
'
請求成功並將當前 _version
設為 10 :
{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 10,
"created": false
}
如果你要重新運行此請求時,它將會失敗,並返回像我們之前看到的同樣的沖突錯誤, 因為指定的外部版本號不大於 Elasticsearch 的當前版本號。
文檔的部分更新
使用 update
API 我們可以部分更新文檔。
前邊我們說過文檔是不可變的:他們不能被修改,只能被替換。 update
API必須遵循同樣的規則。 從外部來看,我們在一個文檔的某個位置進行部分更新。然而在內部, update
API 簡單使用與之前描述相同的 檢索-修改-重建索引 的處理過程。 區別在於這個過程發生在分片內部,這樣就避免了多次請求的網絡開銷。通過減少檢索和重建索引步驟之間的時間,我們也減少了其他進程的變更帶來沖突的可能性。
update
請求最簡單的一種形式是接收文檔的一部分作為 doc
的參數, 它只是與現有的文檔進行合並。對象被合並到一起,覆蓋現有的字段,增加新的字段。 例如,我們增加字段 tags
和 views
到我們的博客文章,如下所示:
curl -X POST "localhost:9200/website/blog/1/_update?pretty" -H 'Content-Type: application/json' -d'
{
"doc" : {
"tags" : [ "testing" ],
"views": 0
}
}
'
如果請求成功,我們看到類似於 index
請求的響應:
{
"_index" : "website",
"_id" : "1",
"_type" : "blog",
"_version" : 3
}
檢索文檔顯示了更新后的 _source
字段:
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 3,
"found": true,
"_source": {
"title": "My first blog entry",
"text": "Starting to get the hang of this...",
"tags": [ "testing" ],
"views": 0
}
}
新的字段已被添加到 _source
中。
使用腳本部分更新文檔
腳本可以在 update
API中用來改變_source
的字段內容, 它在更新腳本中稱為 ctx._source
。 例如,我們可以使用腳本來增加博客文章中 views
的數量:
curl -X POST "localhost:9200/website/blog/1/_update?pretty" -H 'Content-Type: application/json' -d'
{
"script" : "ctx._source.views+=1"
}
'
我們也可以通過使用腳本給 tags
數組添加一個新的標簽。在這個例子中,我們指定新的標簽作為參數,而不是硬編碼到腳本內部。 這使得 Elasticsearch 可以重用這個腳本,而不是每次我們想添加標簽時都要對新腳本重新編譯:
curl -X POST "localhost:9200/website/blog/1/_update?pretty" -H 'Content-Type: application/json' -d'
{
"script" : "ctx._source.tags+=new_tag",
"params" : {
"new_tag" : "search"
}
}
'
獲取文檔並顯示最后兩次請求的效果:
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 5,
"found": true,
"_source": {
"title": "My first blog entry",
"text": "Starting to get the hang of this...",
"tags": ["testing", "search"],
"views": 1
}
}
我們可以看到search
標簽已追加到tags
數組中,views
字段已遞增。
通過設置 ctx.op
為 delete
來刪除基於其內容的文檔:
curl -X POST "localhost:9200/website/blog/1/_update?pretty" -H 'Content-Type: application/json' -d'
{
"script" : "ctx.op = ctx._source.views == count ? \u0027delete\u0027 : \u0027none\u0027",
"params" : {
"count": 1
}
}
'
更新的文檔可能尚不存在
假設我們需要 在 Elasticsearch 中存儲一個頁面訪問量計數器。 每當有用戶瀏覽網頁,我們對該頁面的計數器進行累加。但是,如果它是一個新網頁,我們不能確定計數器已經存在。 如果我們嘗試更新一個不存在的文檔,那么更新操作將會失敗。
在這樣的情況下,我們可以使用upsert
參數,指定如果文檔不存在就應該先創建它:
curl -X POST "localhost:9200/website/pageviews/1/_update?pretty" -H 'Content-Type: application/json' -d'
{
"script" : "ctx._source.views+=1",
"upsert": {
"views": 1
}
}
'
我們第一次運行這個請求時, upsert
值作為新文檔被索引,初始化 views
字段為 1 。 在后續的運行中,由於文檔已經存在, script
更新操作將替代 upsert
進行應用,對 views
計數器進行累加。
更新和沖突
檢索
和 重建索引
步驟的間隔越小,變更沖突的機會越小。 但是它並不能完全消除沖突的可能性。 還是有可能在 update
設法重新索引之前,來自另一進程的請求修改了文檔。
為了避免數據丟失, update
API 在 檢索
步驟時檢索得到文檔當前的 _version
號,並傳遞版本號到 重建索
引 步驟的 index
請求。 如果另一個進程修改了處於檢索和重新索引步驟之間的文檔,那么_version
號將不匹配,更新請求將會失敗。
對於部分更新的很多使用場景,文檔已經被改變也沒有關系。 例如,如果兩個進程都對頁面訪問量計數器進行遞增操作,它們發生的先后順序其實不太重要; 如果沖突發生了,我們唯一需要做的就是嘗試再次更新。
這可以通過 設置參數 retry_on_conflict
來自動完成, 這個參數規定了失敗之前 update
應該重試的次數,它的默認值為 0
。
指定失敗重試次數,例如失敗之前重試該更新5次:
curl -X POST "localhost:9200/website/pageviews/1/_update?retry_on_conflict=5&pretty" -H 'Content-Type: application/json' -d'
{
"script" : "ctx._source.views+=1",
"upsert": {
"views": 0
}
}
'
在增量操作無關順序的場景,例如遞增計數器等這個方法十分有效,但是在其他情況下變更的順序 是 非常重要的。 類似 index
API , update
API 默認采用 最終寫入生效
的方案,但它也接受一個 version
參數來允許你使用 optimistic concurrency control
指定想要更新文檔的版本。
取回多個文檔
Elasticsearch的速度已經很快了,但還能更快。 將多個請求合並成一個,避免單獨處理每個請求花費的網絡延時和開銷。 如果你需要從 Elasticsearch 檢索很多文檔,那么使用 multi-get
或者 mget
API 來將這些檢索請求放在一個請求中,將比逐個文檔請求更快地檢索到全部文檔。
mget
API 要求有一個 docs
數組作為參數,每個 元素包含需要檢索文檔的元數據, 包括 _index
、 _type
和 _id
。如果你想檢索一個或者多個特定的字段,那么你可以通過 _source
參數來指定這些字段的名字:
curl -X GET "localhost:9200/_mget?pretty" -H 'Content-Type: application/json' -d'
{
"docs" : [
{
"_index" : "website",
"_type" : "blog",
"_id" : 2
},
{
"_index" : "website",
"_type" : "pageviews",
"_id" : 1,
"_source": "views"
}
]
}
'
該響應體也包含一個 docs
數組 , 對於每一個在請求中指定的文檔,這個數組中都包含有一個對應的響應,且順序與請求中的順序相同。 其中的每一個響應都和使用單個 get request
請求所得到的響應體相同:
{
"docs" : [
{
"_index" : "website",
"_id" : "2",
"_type" : "blog",
"found" : true,
"_source" : {
"text" : "This is a piece of cake...",
"title" : "My first external blog entry"
},
"_version" : 10
},
{
"_index" : "website",
"_id" : "1",
"_type" : "pageviews",
"found" : true,
"_version" : 2,
"_source" : {
"views" : 2
}
}
]
}
如果想檢索的數據都在相同的 _index
中(甚至相同的 _type
中),則可以在 URL 中指定默認的 /_index
或者默認的 /_index/_type
。
curl -X GET "localhost:9200/website/blog/_mget?pretty" -H 'Content-Type: application/json' -d'
{
"docs" : [
{ "_id" : 2 },
{ "_type" : "pageviews", "_id" : 1 }
]
}
'
如果所有文檔的 _index
和 _type
都是相同的,你可以只傳一個 ids
數組,而不是整個 docs
數組:
GET /website/blog/_mget
{
"ids" : [ "2", "1" ]
}
我們請求的第二個文檔是不存在的。我們指定類型為 blog
,但是文檔 ID 1
的類型是 pageviews
,這個不存在的情況將在響應體中被報告:
{
"docs" : [
{
"_index" : "website",
"_type" : "blog",
"_id" : "2",
"_version" : 10,
"found" : true,
"_source" : {
"title": "My first external blog entry",
"text": "This is a piece of cake..."
}
},
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"found" : false
}
]
}
found=false
:未找到該文檔。
第二個文檔未能找到並不妨礙第一個文檔被檢索到。每個文檔都是單獨檢索和報告的。
即使有某個文檔沒有找到,上述請求的HTTP狀態碼仍然是200。事實上,即使請求沒有找到任何文檔,它的狀態碼依然是200--因為mget
請求本身已經成功執行。為了確定某個文檔查找是成功或者失敗,你需要檢查found
標記。
代價較小的批量操作
與 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
參數打印。
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
請求有以下形式:
curl -X POST "localhost:9200/_bulk?pretty" -H 'Content-Type: application/json' -d'
{ "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"} }
'
需要注意兩點:
- 請注意
delete
動作不能有請求體,它后面跟着的是另外一個操作。 - 謹記最后一個換行符不要落下。
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,並且在相應的請求報告出錯誤明細:
curl -X POST "localhost:9200/_bulk?pretty" -H 'Content-Type: application/json' -d'
{ "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
}}
]
}
errors=true
:一個或者多個請求失敗。
status=409
:這個請求的HTTP狀態碼報告為 409 CONFLICT
。
error
:解釋為什么請求失敗的錯誤信息。
status=200
:第二個請求成功,返回 HTTP 狀態碼 200 OK
。
這也意味着bulk
請求不是原子的: 不能用它來實現事務控制。每個請求是單獨處理的,因此一個請求的成功或失敗不會影響其他的請求。
不要重復指定Index和Type
也許你正在批量索引日志數據到相同的 index
和 type
中。 但為每一個文檔指定相同的元數據是一種浪費。相反,可以像 mget
API 一樣,在 bulk
請求的 URL 中接收默認的 /_index
或者 /_index/_type
:
curl -X POST "localhost:9200/website/_bulk?pretty" -H 'Content-Type: application/json' -d'
{ "index": { "_type": "log" }}
{ "event": "User logged in" }
'
可以覆蓋元數據行中的 _index
和 _type
, 但是它將使用URL中的這些元數據值作為默認值:
curl -X POST "localhost:9200/website/log/_bulk?pretty" -H 'Content-Type: application/json' -d'
{ "index": {}}
{ "event": "User logged in" }
{ "index": { "_type": "blog" }}
{ "title": "Overriding the default type" }
'
多大是太大了?
整個批量請求都需要由接收到請求的節點加載到內存中,因此該請求越大,其他請求所能獲得的內存就越少。批量請求的大小有一個最佳值,大於這個值,性能將不再提升,甚至會下降。但是最佳值不是一個固定的值。它完全取決於硬件、文檔的大小和復雜度、索引和搜索的負載的整體情況。
幸運的是,很容易找到這個最佳點:通過批量索引典型文檔,並不斷增加批量大小進行嘗試。 當性能開始下降,那么你的批量大小就太大了。一個好的辦法是開始時將 1000 到 5000 個文檔作為一個批次,如果你的文檔非常大,那么就減少批量的文檔個數。
密切關注你的批量請求的物理大小往往非常有用,一千個 1KB 的文檔是完全不同於一千個 1MB 文檔所占的物理大小。一個好的批量大小在開始處理后所占用的物理大小約為 5-15 MB。