1. 程式人生 > >關於Elasticsearch文件的描述以及如何操作文件的詳細總結

關於Elasticsearch文件的描述以及如何操作文件的詳細總結

文件

什麼是文件

在大多數應用中,多數實體或物件可以被序列化為包含鍵值對的 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按前述完全相同方式執行以下過程:

  1. 從舊文件構建JSON
  2. 更改該JSON
  3. 刪除舊文件
  4. 索引一個新文件
    唯一的區別在於, update API僅僅通過一個客戶端請求來實現這些步驟,而不需要單獨的getindex請求。

建立新文件

當我們索引一個文件,怎麼確認我們正在建立一個完全新的文件,而不是覆蓋現有的呢?

請記住,_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 CreatedHTTP響應碼。

另一方面,如果具有相同的_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需要一種方法確保文件的舊版本不會覆蓋新的版本。

我們之前說到的 indexGETdelete 請求時,我們指出每個文件都有一個 _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 的引數, 它只是與現有的文件進行合併。物件被合併到一起,覆蓋現有的欄位,增加新的欄位。 例如,我們增加欄位 tagsviews 到我們的部落格文章,如下所示:

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.opdelete 來刪除基於其內容的文件:

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 允許在單個步驟中進行多次 createindexupdatedelete 請求。如果你需要索引一個數據流比如日誌事件,它可以排隊和索引數百或數千批次。

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 本身組成--文件包含的欄位和值。它是 indexcreate 操作所必需的,你必須提供文件以索引。

它也是 update 操作所必需的,並且應該包含你傳遞給 update API 的相同請求體: docupsertscript 等等。 刪除操作不需要 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

也許你正在批量索引日誌資料到相同的 indextype 中。 但為每一個文件指定相同的元資料是一種浪費。相反,可以像 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。

原文地址:https://www.lifengdi.com/archives/article/tech/