關於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/934

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章