在Elasticsearch中,每一個文檔都有一個版本號碼。每當文檔產生變化時(包括刪除),_version
就會增大。在《版本控制》中,我們將會詳細講解如何使用_version
的數字來確認你的程序不會隨意替換掉不想覆蓋的數據。
自增ID
如果我們的數據中沒有天然的標示符,我們可以讓Elasticsearch為我們自動生成一個。請求的結構發生了變化:我們把PUT
——“把文檔存儲在這個地址中”變量變成了POST
——“把文檔存儲在這個地址下”。
這樣一來,請求中就只包含 _index
和_type
了:
POST /website/blog/
{
"title": "My second blog entry", "text": "Still trying this out...", "date": "2014/01/01" }
這次的反饋和之前基本一樣,只有_id
改成了系統生成的自增值:
{
"_index": "website",
"_type": "blog",
"_id": "wM0OSFhDQXGZAWDf0-drSA",
"_version": 1,
"created": true
}
自生成ID是由22個字母組成的,安全 universally unique identifiers 或者被稱為UUIDs。
3.1 文檔
文檔是什么?
在很多程序中,大部分實體或者對象都被序列化為包含鍵和值的JSON對象。鍵是一個字段或者屬性的名字,值可以是一個字符串、數字、布爾值、對象、數組或者是其他的特殊類型,比如代表日期的字符串或者代表地理位置的對象:
{
"name": "John Smith", "age": 42, "confirmed": true, "join_date": "2014-06-01", "home": { "lat": 51.5, "lon": 0.1 }, "accounts": [ { "type": "facebook", "id": "johnsmith" }, { "type": "twitter", "id": "johnsmith" } ] }
通常情況下,我們使用可以互換對象和文檔。然而,還是有一個區別的。對象(object )僅僅是一個JSON對象,類似於哈希,哈希映射,字典或關聯數組。對象(Objects)則可以包含其他對象(Objects)。
在Elasticsearch中,文檔這個單詞有特殊的含義。它指的是在Elasticsearch中被存儲到唯一ID下的由最高級或者根對象 (root object )序列化而來的JSON。
文檔元數據
一個文檔不只包含了數據。它還包含了元數據(metadata) —— 關於文檔的信息。有三個元數據元素是必須存在的,它們是:
名字 | 說明 |
---|---|
_index |
文檔存儲的地方 |
_type |
文檔代表的對象種類 |
_id |
文檔的唯一編號 |
_index
索引 類似於傳統數據庫中的"數據庫"——也就是我們存儲並且索引相關數據的地方。
TIP:
在Elasticsearch中,我們的數據都在分片中被存儲以及索引,索引只是一個邏輯命名空間,它可以將一個或多個分片組合在一起。然而,這只是一個內部的運作原理——我們的程序可以根本不用關心分片。對於我們的程序來說,我們的文檔存儲在索引中。剩下的交給Elasticsearch就可以了。
我們將會在《索引管理》章節中探討如何創建並管理索引。但是現在,我們只需要讓Elasticsearch幫助我們創建索引。我們只需要選擇一個索引的名字。這個名稱必須要全部小寫,也不能以下划線開頭,不能包含逗號。我們可以用website
作為我們索引的名字。
_type
在程序中,我們使用對象代表“物品”,比如一個用戶、一篇博文、一條留言或者一個郵件。每一個對象都屬於一種類型,類型定義了對象的屬性或者與數據的關聯。用戶類的對象可能就會包含名字、性別、年齡以及郵箱地址等。
在傳統的數據庫中,我們總是將同類的數據存儲在同一個表中,因為它們的數據格式是相同的。同理,在Elasticsearch中,我們使用同樣類型的文檔來代表同類“事物”,也是因為它們的數據結構是相同的。
每一個類型都擁有自己的映射(mapping)或者結構定義,它們定義了當前類型下的數據結構,類似於數據庫表中的列。所有類型下的文檔會被存儲在同一個索引下,但是映射會告訴Elasticsearch不同的數據應該如何被索引。
我們將會在《映射》中探討如何制定或者管理映射,但是目前為止,我們只需要依靠Elasticsearch來自動處理數據結構。
_id
id是一個字符串,當它與_index
以及_type
組合時,就可以來代表Elasticsearch中一個特定的文檔。我們創建了一個新的文檔時,你可以自己提供一個_id
,或者也可以讓Elasticsearch幫你生成一個。
其他元數據
在文檔中還有一些其他的元數據,我們將會在《映射》章節中詳細講解。使用上面羅列的元素,我們已經可以在Elasticsearch中存儲文檔或者通過ID來搜索已經保存的文檔了。
3.2 索引
索引一個文檔
文檔通過索引
API被索引——存儲並使其可搜索。但是最開始我們需要決定我們將文檔存儲在哪里。正如之前提到的,一篇文檔通過_index
, _type
以及_id
來確定它的唯一性。我們可以自己提供一個_id
,或者也使用index
API 幫我們生成一個。
使用自己的ID
如果你的文檔擁有天然的標示符(例如user_account
字段或者文檔中其他的標識值),這時你就可以提供你自己的_id
,這樣使用index
API:
PUT /{index}/{type}/{id}
{
"field": "value", ... }
幾個例子。如果我們的索引叫做"website"
,我們的類型叫做 "blog"
,然后我們選擇"123"
作為ID的編號。這時,請求就是這樣的:
PUT /website/blog/123 { "title": "My first blog entry", "text": "Just trying this out...", "date": "2014/01/01" }
Elasticsearch返回內容:
{
"_index": "website", "_type": "blog", "_id": "123", "_version": 1, "created": true }
這個返回值意味着我們的索引請求已經被成功創建,其中還包含了_index
, _type
以及_id
的元數據,以及一個新的元素_version
。
在Elasticsearch中,每一個文檔都有一個版本號碼。每當文檔產生變化時(包括刪除),_version
就會增大。在《版本控制》中,我們將會詳細講解如何使用_version
的數字來確認你的程序不會隨意替換掉不想覆蓋的數據。
自增ID
如果我們的數據中沒有天然的標示符,我們可以讓Elasticsearch為我們自動生成一個。請求的結構發生了變化:我們把PUT
——“把文檔存儲在這個地址中”變量變成了POST
——“把文檔存儲在這個地址下”。
這樣一來,請求中就只包含 _index
和_type
了:
POST /website/blog/
{
"title": "My second blog entry", "text": "Still trying this out...", "date": "2014/01/01" }
這次的反饋和之前基本一樣,只有_id
改成了系統生成的自增值:
{
"_index": "website",
"_type": "blog",
"_id": "wM0OSFhDQXGZAWDf0-drSA",
"_version": 1,
"created": true
}
自生成ID是由22個字母組成的,安全 universally unique identifiers 或者被稱為UUIDs。
3.3 Get
搜索文檔
要從Elasticsearch中獲取文檔,我們需要使用同樣的_index
,_type
以及 _id
但是不同的HTTP變量GET
:
GET /website/blog/123?pretty
返回結果包含了之前提到的內容,以及一個新的字段_source
,它包含我們在最初創建索引時的原始JSON文檔。
{
"_index" : "website", "_type" : "blog", "_id" : "123", "_version" : 1, "found" : true, "_source" : { "title": "My first blog entry", "text": "Just trying this out..." "date": "2014/01/01" } }
pretty
在任意的查詢字符串中添加pretty
參數,類似上面的請求,Elasticsearch就可以得到優美打印的更加易於識別的JSON結果。_source
字段不會執行優美打印,它的樣子取決於我們錄入的樣子。
GET請求的返回結果中包含{"found": true}
。這意味着這篇文檔確實被找到了。如果我們請求了一個不存在的文檔,我們依然會得到JSON反饋,只是found
的值會變為false
。
同樣,HTTP返回碼也會由'200 OK'
變為'404 Not Found'
。我們可以在curl
后添加-i
,這樣你就能得到反饋頭文件:
curl -i -XGET /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
得到指定字段。如果需要多個字段你可以使用逗號分隔:
GET /website/blog/123?_source=title,text
現在_source
字段中就只會顯示你指定的字段:
{
"_index" : "website", "_type" : "blog", "_id" : "123", "_version" : 1, "exists" : true, "_source" : { "title": "My first blog entry" , "text": "Just trying this out..." } }
或者你只想得到_source
字段而不要其他的元數據,你可以這樣請求:
GET /website/blog/123/_source
這樣結果就只返回:
{
"title": "My first blog entry", "text": "Just trying this out...", "date": "2014/01/01" }
3.4 存在
檢查文檔是否存在
如果確實想檢查一下文檔是否存在,你可以試用HEAD
來替代GET
方法,這樣就是會返回HTTP頭文件:
curl -i -XHEAD /website/blog/123
如果文檔存在,Elasticsearch將會返回200 OK
的狀態碼:
HTTP/1.1 200 OK Content-Type: text/plain; charset=UTF-8 Content-Length: 0
如果不存在將會返回404 Not Found
狀態碼:
curl -i -XHEAD /website/blog/124
HTTP/1.1 404 Not Found Content-Type: text/plain; charset=UTF-8 Content-Length: 0
當然,這個反饋只代表了你查詢的那一刻文檔不存在,但是不代表幾毫秒后它不存在,很可能與此同時,另一個進程正在創建文檔。
3.5 更新
更新整個文檔
在Documents中的文檔是不可改變的。所以如果我們需要改變已經存在的文檔,我們可以使用《索引》中提到的index
API來重新索引或者替換掉它:
PUT /website/blog/123 { "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 <1> }
-
created
被標記為false
是因為在同索引、同類型下已經存在同ID的文檔。
在內部,Elasticsearch已經將舊文檔標記為刪除並且添加了新的文檔。舊的文檔並不會立即消失,但是你也無法訪問他。Elasticsearch會在你繼續添加更多數據的時候在后台清理已經刪除的文件。
在本章的后面,我們將會在《局部更新》中介紹最新更新的API。這個API允許你修改局部,但是原理和下方的完全一樣:
-
- 從舊的文檔中檢索JSON
- 修改它
- 刪除修的文檔
- 索引一個新的文檔
唯一不同的是,使用了update
API你就不需要使用get
然后再操作index
請求了。
3.6 創建
創建一個文檔
當我們索引一個文檔時,如何確定我們是創建了一個新的文檔還是覆蓋了一個已經存在的文檔呢?
請牢記_index
,_type
以及_id
組成了唯一的文檔標記,所以為了確定我們創建的是全新的內容,最簡單的方法就是使用POST
方法,讓Elasticsearch自動創建不同的_id
:
POST /website/blog/
{ ... }
然而,我們可能已經決定好了_id
,所以需要告訴Elasticsearch只有當_index
,_type
以及_id
這3個屬性全部相同的文檔不存在時才接受我們的請求。實現這個目的有兩種方法,他們實質上是一樣的,你可以選擇你認為方便的那種:
第一種是在查詢中添加op_type
參數:
PUT /website/blog/123?op_type=create { ... }
或者在請求最后添加 /_create
:
PUT /website/blog/123/_create { ... }
如果成功創建了新的文檔,Elasticsearch將會返回常見的元數據以及201 Created
的HTTP反饋碼。
而如果存在同名文件,Elasticsearch將會返回一個409 Conflict
的HTTP反饋碼,以及如下方的錯誤信息:
{
"error" : "DocumentAlreadyExistsException[[website][4] [blog][123]:
document already exists]",
"status" : 409
}
3.7 刪除
刪除一個文檔
刪除文檔的基本模式和之前的基本一樣,只不過是需要更換成DELETE
方法:
DELETE /website/blog/123
如果文檔存在,那么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將會在你之后添加更多索引的時候才會在后台進行刪除內容的清理。
3.8 版本控制
處理沖突
當你使用索引
API來更新一個文檔時,我們先看到了原始文檔,然后修改它,最后一次性地將整個新文檔進行再次索引處理。Elasticsearch會根據請求發出的順序來選擇出最新的一個文檔進行保存。但是,如果在你修改文檔的同時其他人也發出了指令,那么他們的修改將會丟失。
很長時間以來,這其實都不是什么大問題。或許我們的主要數據還是存儲在一個關系數據庫中,而我們只是將為了可以搜索,才將這些數據拷貝到Elasticsearch中。或許發生多個人同時修改一個文件的概率很小,又或者這些偶然的數據丟失並不會影響到我們的正常使用。
但是有些時候如果我們丟失了數據就會出大問題。想象一下,如果我們使用Elasticsearch來存儲一個網店的商品數量。每當我們賣出一件,我們就會將這個數量減少一個。
突然有一天,老板決定來個大促銷。瞬間,每秒就產生了多筆交易。並行處理,多個進程來處理交易:
web_1
中庫存量
的變化丟失的原因是web_2
並不知道它所得到的庫存量
數據是是過期的。這樣就會導致我們誤認為還有很多貨存,最終顧客就會對我們的行為感到失望。
當我們對數據修改得越頻繁,或者在讀取和更新數據間有越長的空閑時間,我們就越容易丟失掉我們的數據。
以下是兩種能避免在並發更新時丟失數據的方法:
悲觀並發控制(PCC)
這一點在關系數據庫中被廣泛使用。假設這種情況很容易發生,我們就可以阻止對這一資源的訪問。典型的例子就是當我們在讀取一個數據前先鎖定這一行,然后確保只有讀取到數據的這個線程可以修改這一行數據。
樂觀並發控制(OCC)
Elasticsearch所使用的。假設這種情況並不會經常發生,也不會去阻止某一數據的訪問。然而,如果基礎數據在我們讀取和寫入的間隔中發生了變化,更新就會失敗。這時候就由程序來決定如何處理這個沖突。例如,它可以重新讀取新數據來進行更新,又或者它可以將這一情況直接反饋給用戶。
樂觀並發控制
Elasticsearch是分布式的。當文檔被創建、更新或者刪除時,新版本的文檔就會被復制到集群中的其他節點上。Elasticsearch即是同步的又是異步的,也就是說復制的請求被平行發送出去,然后可能會混亂地到達目的地。這就需要一種方法能夠保證新的數據不會被舊數據所覆蓋。
我們在上文提到每當有索引
、put
和刪除
的操作時,無論文檔有沒有變化,它的_version
都會增加。Elasticsearch使用_version
來確保所有的改變操作都被正確排序。如果一個舊的版本出現在新版本之后,它就會被忽略掉。
我們可以利用_version
的優點來確保我們程序修改的數據沖突不會造成數據丟失。我們可以按照我們的想法來指定_version
的數字。如果數字錯誤,請求就是失敗。
我們來創建一個新的博文:
PUT /website/blog/1/_create { "title": "My first blog entry", "text": "Just trying this out..." }
反饋告訴我們這是一個新建的文檔,它的_version
是1
。假設我們要編輯它,把這個數據加載到網頁表單中,修改完畢然后保存新版本。
首先我們先要得到文檔:
GET /website/blog/1
返回結果顯示_version
為1
:
{
"_index" : "website", "_type" : "blog", "_id" : "1", "_version" : 1, "found" : true, "_source" : { "title": "My first blog entry", "text": "Just trying this out..." } }
現在,我們試着重新索引文檔以保存變化,我們這樣指定了version
的數字:
PUT /website/blog/1?version=1 <1> { "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
的響應碼,返回內容如下:
{
"error" : "VersionConflictEngineException[[website][2] [blog][1]:
version conflict, current [2], provided [1]]",
"status" : 409
}
這里面指出了文檔當前的_version
數字是2
,而我們要求的數字是1
。
我們需要做什么取決於我們程序的需求。比如我們可以告知用戶已經有其它人修改了這個文檔,你應該再保存之前看一下變化。而對於上文提到的庫存量
問題,我們可能需要重新讀取一下最新的文檔,然后顯示新的數據。
所有的有關於更新或者刪除文檔的API都支持version
這個參數,有了它你就通過修改你的程序來使用樂觀並發控制。
使用外部系統的版本
還有一種常見的情況就是我們還是使用其他的數據庫來存儲數據,而Elasticsearch只是幫我們檢索數據。這也就意味着主數據庫只要發生的變更,就需要將其拷貝到Elasticsearch中。如果多個進程同時發生,就會產生上文提到的那些並發問題。
如果你的數據庫已經存在了版本號碼,或者也可以代表版本的時間戳
。這是你就可以在Elasticsearch的查詢字符串后面添加version_type=external
來使用這些號碼。版本號碼必須要是大於零小於9.2e+18
(Java中long的最大正值)的整數。
Elasticsearch在處理外部版本號時會與對內部版本號的處理有些不同。它不再是檢查_version
是否與請求中指定的數值相同,而是檢查當前的_version
是否比指定的數值小。如果請求成功,那么外部的版本號就會被存儲到文檔中的_version
中。
外部版本號不僅可以在索引和刪除請求時使用,還可以在創建時使用。
例如,創建一篇使用外部版本號為5
的博文,我們可以這樣操作:
PUT /website/blog/2?version=5&version_type=external { "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
:
PUT /website/blog/2?version=10&version_type=external { "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中的版本號。
3.9 局部更新
更新文檔中的一部分
在《更新》一章中,我們講到了要是想更新一個文檔,那么就需要去取回數據,更改數據然后將整個文檔進行重新索引。當然,你還可以通過使用更新
API來做部分更新,比如增加一個計數器。
正如我們提到的,文檔不能被修改,它們只能被替換掉。更新
API也必須遵循這一法則。從表面看來,貌似是文檔被替換了。對內而言,它必須按照找回-修改-索引的流程來進行操作與管理。不同之處在於這個流程是在一個片(shard) 中完成的,因此可以節省多個請求所帶來的網絡開銷。除了節省了步驟,同時我們也能減少多個進程造成沖突的可能性。
使用更新
請求最簡單的一種用途就是添加新數據。新的數據會被合並到現有數據中,而如果存在相同的字段,就會被新的數據所替換。例如我們可以為我們的博客添加tags
和views
字段:
POST /website/blog/1/_update { "doc" : { "tags" : [ "testing" ], "views": 0 } }
如果請求成功,我們就會收到一個類似於索引
時返回的內容:
{
"_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" ], <1> "views": 0 <1> } }
-
- 新的數據已經添加到了字段
_source
中。
- 新的數據已經添加到了字段
使用腳本進行更新
我們將會在《腳本》一章中學習更詳細的內容,我們現在只需要了解一些在Elasticsearch中使用API無法直接完成的自定義行為。默認的腳本語言叫做MVEL,但是Elasticsearch也支持JavaScript, Groovy 以及 Python。
MVEL是一個簡單高效的JAVA基礎動態腳本語言,它的語法類似於Javascript。你可以在Elasticsearch scripting docs 以及 MVEL website了解更多關於MVEL的信息。
腳本語言可以在更新
API中被用來修改_source
中的內容,而它在腳本中被稱為ctx._source
。例如,我們可以使用腳本來增加博文中views
的數字:
POST /website/blog/1/_update { "script" : "ctx._source.views+=1" }
我們同樣可以使用腳本在tags
數組中添加新的tag。在這個例子中,我們把新的tag聲明為一個變量,而不是將他寫死在腳本中。這樣Elasticsearch就可以重新使用這個腳本進行tag的添加,而不用再次重新編寫腳本了:
POST /website/blog/1/_update { "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"], <1> "views": 1 <2> } }
-
tags
數組中出現了search
。views
字段增加了。
我們甚至可以使用ctx.op
來根據內容選擇是否刪除一個文檔:
POST /website/blog/1/_update { "script" : "ctx.op = ctx._source.views == count ? 'delete' : 'none'", "params" : { "count": 1 } }
更新一篇可能不存在的文檔
想象一下,我們可能需要在Elasticsearch中存儲一個頁面計數器。每次用戶訪問這個頁面,我們就增加一下當前頁面的計數器。但是如果這是個新的頁面,我們不能確保這個計數器已經存在。如果我們試着去更新一個不存在的文檔,更新操作就會失敗。
為了防止上述情況的發生,我們可以使用upsert
參數來設定文檔不存在時,它應該被創建:
POST /website/pageviews/1/_update { "script" : "ctx._source.views+=1", "upsert": { "views": 1 } }
首次運行這個請求時,upsert
的內容會被索引成新的文檔,它將views
字段初始化為1
。當之后再請求時,文檔已經存在,所以腳本
更新就會被執行,views
計數器就會增加。
更新和沖突
在本節的開篇我們提到了當取回與重新索引兩個步驟間的時間越少,發生改變沖突的可能性就越小。但它並不能被完全消除,在更新
的過程中還可能存在另一個進程進行重新索引的可能性。
為了避免丟失數據,更新
API會在獲取步驟中獲取當前文檔中的_version
,然后將其傳遞給重新索引步驟中的索引
請求。如果其他的進程在這兩部之間修改了這個文檔,那么_version
就會不同,這樣更新就會失敗。
對於很多的局部更新來說,文檔有沒有發生變化實際上是不重要的。例如,兩個進程都要增加頁面瀏覽的計數器,誰先誰后其實並不重要 —— 發生沖突時只需要重新來過即可。
你可以通過設定retry_on_conflict
參數來設置自動完成這項請求的次數,它的默認值是0
。
POST /website/pageviews/1/_update?retry_on_conflict=5 <1> { "script" : "ctx._source.views+=1", "upsert": { "views": 0 } }
-
- 失敗前重新嘗試5次
這個參數非常適用於類似於增加計數器這種無關順序的請求,但是還有些情況的順序就是很重要的。例如上一節提到的情況,你可以參考樂觀並發控制以及悲觀並發控制來設定文檔的版本號。
3.10 Mget
獲取多個文檔
盡管Elasticsearch已經很快了,但是它依舊可以更快。你可以將多個請求合並到一個請求中以節省網絡開銷。如果你需要從Elasticsearch中獲取多個文檔,你可以使用multi-get 或者 mget
API來取代一篇又一篇文檔的獲取。
mget
API需要一個docs
數組,每一個元素包含你想要的文檔的_index
, _type
以及_id
。你也可以指定_source
參數來設定你所需要的字段:
GET /_mget
{
"docs" : [ { "_index" : "website", "_type" : "blog", "_id" : 2 }, { "_index" : "website", "_type" : "pageviews", "_id" : 1, "_source": "views" } ] }
返回值包含了一個docs
數組,這個數組以請求中指定的順序每個文檔包含一個響應。每一個響應都和獨立的get
請求返回的響應相同:
{
"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
。
你也可以在單獨的請求中重寫這個參數:
GET /website/blog/_mget
{
"docs" : [ { "_id" : 2 }, { "_type" : "pageviews", "_id" : 1 } ] }
事實上,如果所有的文檔擁有相同的_index
以及 _type
,直接在請求中添加ids
的數組即可:
GET /website/blog/_mget
{
"ids" : [ "2", "1" ] }
請注意,我們所請求的第二篇文檔不存在,這是就會返回如下內容:
{
"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 <1> } ] }
-
- 文檔沒有被找到。
當第二篇文檔沒有被找到的時候也不會影響到其它文檔的獲取結果。每一個文檔都會被獨立展示。
注意:上方請求的HTTP狀態碼依舊是200
,盡管有個文檔沒有找到。事實上,即使所有的文檔都沒有被找到,響應碼也依舊是200
。這是因為mget
這個請求本身已經成功完成。要確定獨立的文檔是否被成功找到,你需要檢查found
標識。
3.11 Bulk
批量更高效
與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。
3.12 總結
現在你應該知道如何作為分布式文檔存儲來使用Elasticsearch。你可以對文檔進行存儲,更新,獲取,刪除操作,而且你還知道該如何安全的執行這些操作。這已經非常有用處了,即使我們現在仍然沒有嘗試更激動人心的方面 -- 在文檔中進行查詢操作。不過我們先探討下分布式環境中Elasticsearch安全管理你的文檔所使用的內部過程。