Elasticsearch 教程--數據

 

在Elasticsearch中,每個文檔都有一個版本號碼。每當文檔產生變化時(包括刪除),_version就會增大。在《版本控制》中,咱們將會詳細講解如何使用_version的數字來確認你的程序不會隨意替換掉不想覆蓋的數據。html

自增ID

若是咱們的數據中沒有自然的標示符,咱們可讓Elasticsearch爲咱們自動生成一個。請求的結構發生了變化:咱們把PUT——「把文檔存儲在這個地址中」變量變成了POST——「把文檔存儲在這個地址下」。web

這樣一來,請求中就只包含 _index_type了:數據庫

POST /website/blog/
{
  "title": "My second blog entry", "text": "Still trying this out...", "date": "2014/01/01" } 

此次的反饋和以前基本同樣,只有_id改爲了系統生成的自增值:json

{
   "_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

索引 相似於傳統數據庫中的"數據庫"——也就是咱們存儲而且索引相關數據的地方。app


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,或者也使用indexAPI 幫咱們生成一個。

使用本身的ID

若是你的文檔擁有自然的標示符(例如user_account字段或者文檔中其餘的標識值),這時你就能夠提供你本身的_id,這樣使用indexAPI:

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中的文檔是不可改變的。因此若是咱們須要改變已經存在的文檔,咱們可使用《索引》中提到的indexAPI來從新索引或者替換掉它:

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> } 
    1. created被標記爲 false是由於在同索引、同類型下已經存在同ID的文檔。

在內部,Elasticsearch已經將舊文檔標記爲刪除而且添加了新的文檔。舊的文檔並不會當即消失,可是你也沒法訪問他。Elasticsearch會在你繼續添加更多數據的時候在後臺清理已經刪除的文件。

在本章的後面,咱們將會在《局部更新》中介紹最新更新的API。這個API容許你修改局部,可是原理和下方的徹底同樣:

    1. 從舊的文檔中檢索JSON
    2. 修改它
    3. 刪除修的文檔
    4. 索引一個新的文檔

惟一不一樣的是,使用了updateAPI你就不須要使用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..." } 

反饋告訴咱們這是一個新建的文檔,它的_version1。假設咱們要編輯它,把這個數據加載到網頁表單中,修改完畢而後保存新版本。

首先咱們先要獲得文檔:

GET /website/blog/1 

返回結果顯示_version1

{
  "_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..." } 
    1. 咱們只但願當索引中文檔的_version1時,更新才生效。

請求成功相應,返回內容告訴咱們_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..." } 

在返回結果中,咱們能夠發現_version5

{
  "_index": "website", "_type": "blog", "_id": "2", "_version": 5, "created": true } 

如今咱們更新這個文檔,並指定version10

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) 中完成的,所以能夠節省多個請求所帶來的網絡開銷。除了節省了步驟,同時咱們也能減小多個進程形成衝突的可能性。

使用更新請求最簡單的一種用途就是添加新數據。新的數據會被合併到現有數據中,而若是存在相同的字段,就會被新的數據所替換。例如咱們能夠爲咱們的博客添加tagsviews字段:

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> } } 
    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> } } 
    1. tags數組中出現了search
    2. 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 } } 
    1. 失敗前從新嘗試5次

這個參數很是適用於相似於增長計數器這種無關順序的請求,可是還有些狀況的順序就是很重要的。例如上一節提到的狀況,你能夠參考樂觀併發控制以及悲觀併發控制來設定文檔的版本號。

3.10 Mget

獲取多個文檔

儘管Elasticsearch已經很快了,可是它依舊能夠更快。你能夠將多個請求合併到一個請求中以節省網絡開銷。若是你須要從Elasticsearch中獲取多個文檔,你可使用multi-get 或者 mget API來取代一篇又一篇文檔的獲取。

mgetAPI須要一個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> } ] } 
    1. 文檔沒有被找到。

當第二篇文檔沒有被找到的時候也不會影響到其它文檔的獲取結果。每個文檔都會被獨立展現。

注意:上方請求的HTTP狀態碼依舊是200,儘管有個文檔沒有找到。事實上,即便全部的文檔都沒有被找到,響應碼也依舊是200。這是由於mget這個請求自己已經成功完成。要肯定獨立的文檔是否被成功找到,你須要檢查found標識。

3.11 Bulk

批量更高效

mget能同時容許幫助咱們獲取多個文檔相同,bulk API能夠幫助咱們同時完成執行多個請求,好比:createindexupdate以及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必須是indexcreateupdate或者deletemetadata 須要指明須要被操做文檔的_index,_type以及_id,例如刪除命令就能夠這樣填寫:

{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }} 

在你進行index以及create操做時,request body 行必需要包含文檔的_source數據——也就是文檔的全部內容。

一樣,在執行update API: docupsert,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> 
    1. 注意delete操做是如何處理request body的,你能夠在它以後直接執行新的操做。

    2. 請記住最後有換行符

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 }} ] }} 
    1. 全部的請求都被成功執行。

每個子請求都會被單獨執行,因此一旦有一個子請求失敗了,並不會影響到其餘請求的成功執行。若是一旦出現失敗的請求,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已經存在,可是以後針對文檔123index操做依舊被成功執行:

{
   "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> }} ] } 
    1. 至少有一個請求錯誤發生。
    2. 這條請求的狀態碼爲409 CONFLICT
    3. 錯誤信息解釋了致使錯誤的緣由。
    4. 第二條請求的狀態碼爲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安全管理你的文檔所使用的內部過程。

相關文章
相關標籤/搜索