ElasticSearch入門系列(五)數據


序言:無論程序如何寫,最終都是為了組織數據為我們服務。在實際應用中,並不是所有相同類型的實體的看起來都是一樣的。傳統上我們使用行和列將數據存儲在關系型數據庫中相當於使用電子表格,這種固定的存儲方式導致對象的靈活性不復存在了。Elasticsearch是一個分布式的文檔存儲引擎,默認每一個字段的數據都是可以被索引的。

一、什么是文檔

程序中大多的實體或對象能夠被序列化為包含鍵值對的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" } ] } 

 

通常,我們認為對象和文檔是等價相同的,不過還是有一些區別的,對象是一個JSON結構體,類似於哈希。hashmap、字典或者關聯數組;對象中害了能包含其他對象。在Elasticsearch中,文檔 是指頂層結構或者根對象序列化成的JSON數據

文檔元數據:

一個文檔不只有數據,還包含了元數據,三個必須的元數據節點是:

_index   文檔存儲的地方

_type     文檔代表的對象的類

_id         文檔的唯一標識

①:_index

索引類似於關系型數據庫里的數據庫,是我們存儲和索引關聯數據的地方。索引名必須是全部小寫,不能以下划線開頭,不能包含逗號。

②:_type 

在關系型數據庫中,我們經常使用相同類的對象存儲在一個表里,因為他們有着相同的結構。在Elasticsearch中我們使用相同類型的文檔表示相同的事物。

每個類型都有自己的映射或結構定義,就像傳統數據庫表中的列一樣,所有類型下的文檔被存儲在同一個索引下,但是類型的映射會告訴Elasticsearch不同的文檔如何被索引。

_type的名字可以是大寫或小寫,不能包含下划線或逗號。

③:_id

僅僅是一個字符串,用於位子標識一個文檔,也可以讓Elasticsearch幫你自動生成。

二、索引一個文檔

文檔通過index API被索引--使數據可以被存儲和搜索,但是首先我們需要覺得文檔所在。我們可以通過_index   _type  _id唯一確定,也可以使用index  API為我們自動生成一個。

1、使用自己的ID

如果你的文檔有自然的標識符,我們可以提供自己的_id

PUT /{index}/{type}/{id}
{
  "field": "value", ... }

eg:索引website 類型blog  ID為123 

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

2、自增ID

如果我們沒有自然ID。可以讓Elasticsearch自動為我們生成,PUT方法變為 POST方法。此時的URL只包含_index 和_type兩個字段

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

響應為:

{
   "_index": "website", "_type": "blog", "_id": "wM0OSFhDQXGZAWDf0-drSA", "_version": 1, "created": true }

自動增長的ID有22個字符,UUID

 

三、檢索文檔

想要從Elasticsearch中獲取文檔我們使用同樣的_index  _type  _id但是HTTP方法改為GET

GET  /website/blog/123?pretty

響應里增加了_source字段,他包含了在創建索引時我們發送給Elasticsearch的原始文檔

{
  "_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:用於美化輸出

GET請求返回的響應內容包括found:true這意味着文檔已經找到,如果請求不存在的文檔,found就會變成false,狀態碼也會變為404 Not  Found 可以通過在curl后加-i參數得到響應頭 

curl  -i -XGET http://localhost:9200/website/blog/124?pretty現在的響應變為

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

檢索文檔的一部分:

通常GET會返回全部的文檔,如果只是需要一部分,可以使用_source參數,多個字段使用逗號分隔。

GET /website/blog/123?_source=title,text
{
  "_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" } }

如果只是想得到_source字段而不用其他元數據

GET /website/blog/123/_source
{
   "title": "My first blog entry", "text": "Just trying this out...", "date": "2014/01/01" }

四、檢查文檔是否存在

如果只是想檢查文檔是否存在,使用HEAD方法來代替GET,HEAD請求不會反悔響應體,只有HTTP頭。

curl -i -XHEAD http://localhosy:9200/website/blog/123

如果存在:

HTTP/1.1 200 OK Content-Type: text/plain; charset=UTF-8 Content-Length: 0

如果不存在

HTTP/1.1 404 Not Found Content-Type: text/plain; charset=UTF-8 Content-Length: 0

五、更新整個文檔

文檔在Elasticsearch中是不可改變的,我們不能修改他們,如果要更新已存在的文檔,我們可以使用index API重新索引或者替換掉他。

eg:

PUT /website/blog/123 { "title": "My first blog entry", "text": "I am starting to get the hang of this...", "date": "2014/01/02" }

響應:

{
  "_index" : "website", "_type" : "blog", "_id" : "123", "_version" : 2, "created": false <1> }

<1>created標識為false因為同索引同類型下已經存在同ID的文檔,從上面響應中我們可以看到version增加了。

在內部,Elasticsearch已經標記舊文檔為刪除並添加了一個完整的新文檔,舊文檔不會立即刪除,但是也不能去訪問。

六、創建一個新文檔

當索引一個文檔我們如何確定是完全創建了一個新的還是覆蓋了已經存在的呢?

_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

如果創建成功將返回一個響應狀態嗎201 如果有沖突將返回409

{
  "error" : "DocumentAlreadyExistsException[[website][4] [blog][123]:
             document already exists]",
  "status" : 409
}

七、刪除文檔

語法:DELETE /website/blog/123

如果文檔被找到將返回200狀態碼並且version數字增加

{
  "found" : true, "_index" : "website", "_type" : "blog", "_id" : "123", "_version" : 3 }

如果文檔未被找到,將返回404,version同樣增加了,為了確保多節點不同操作的正確順序

{
  "found" : false, "_index" : "website", "_type" : "blog", "_id" : "123", "_version" : 4 }

八、處理沖突

當使用index API更新文檔的時候。我們讀取原始文檔,做修改,然后將整個文檔一次性重新索引。做進的索引請求會生效,如果其他人同時也修改了這個文檔,衙門的修改將會丟失。。

web_1讓stock_count失效是因為web_2沒有察覺到stock_count的拷貝已經過期。變化越頻繁,或讀取和更新間的時間越長,越容易丟失我們的更高

在數據庫中,有兩種通用的方法確保在並發更新時修改不會丟失:

悲觀並發控制:

這在關系型數據庫中被廣泛的使用,假設沖突的更改經常發生,為了解決沖突我們把訪問區塊化,典型的例子是在讀一行數據前鎖定這行,然后確保只有枷鎖的那個線程可以修改這行數據。

樂觀並發控制:

被Elasticsearch使用,假設沖突不經常發生,也不區塊化訪問。然而如果在讀寫過程中數據發生了變化,更新操作失敗,這時由主觀覺得如何解決。

 

Elasticsearch是分布式的,當文檔被創建、更新或刪除時,文檔的新版本會被復制到集群的其他節點。

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

響應體:

{
  "_index" : "website", "_type" : "blog", "_id" : "1", "_version" : 1, "found" : true, "_source" : { "title": "My first blog entry", "text": "Just trying this out..." } }

修改:

PUT /website/blog/1?version=1 <1> { "title": "My first blog entry", "text": "Starting to get the hang of this..." }

<1>我們只希望在version是1的時候更新才生效

請求成功的響應體

{
  "_index": "website", "_type": "blog", "_id": "1", "_version": 2 "created": false }

請求失敗的響應體

{
  "error" : "VersionConflictEngineException[[website][2] [blog][1]:
             version conflict, current [2], provided [1]]",
  "status" : 409
}

使用外部版本控制系統:

一種常見的結果是使用一些其他的數據庫作為主數據庫,然后使用Elasticsearch搜索數據,這意味着所有主數據庫發生變化,就要將其拷貝到Elasticsearch中,如果多個進程負責數據同步,就會遇上並發問題。

如果主數據庫中有版本字段或一些類似於timestamp等用於版本控制的字段,可以在查詢字符串后面添加version_type=external來使用這些版本號。

eg:創建一個包含外部版本號為5的新博客

PUT /website/blog/2?version=5&version_type=external { "title": "My first external blog entry", "text": "Starting to get the hang of this..." }

響應體:

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

響應體:

{
  "_index": "website", "_type": "blog", "_id": "2", "_version": 10, "created": false }

如果運行第二次會返回沖突錯誤

九、文檔局部更新

示例:更新views字段

POST /website/blog/1/_update { "doc" : { "tags" : [ "testing" ], "views": 0 } }

請求成功:

{
   "_index" :   "website",
   "_id" :      "1",
   "_type" :    "blog",
   "_version" : 3
}

檢索查看結果:

{
   "_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字段中

 

使用腳本局部更新:

當API不能瞞住要求時,Elasticsearch允許使用腳本實現自己的邏輯。默認腳本語言是Groovy

腳本能夠使用update  API改變_source字段的內容,他在腳本內部以ctx._source表示,例如我們可以使用腳本增加博客的views數量

POST /website/blog/1/_update { "script" : "ctx._source.views+=1" }

增加一個新標簽到tags數組中:

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>search標簽已經被添加到tags數組中

<2>views字段已經被添加

通過設置ctx.op為delete可以根據內容刪除文檔

POST /website/blog/1/_update { "script" : "ctx.op = ctx._source.views == count ? 'delete' : 'none'", "params" : { "count": 1 } }

更新不可能存在的文檔

比如我們記錄瀏覽器計數器,當有新用戶訪問,我們增加,如果是新頁面則會更新失敗,我們可以使用upsert定義文檔來使其不存在時被創建。

POST /website/pageviews/1/_update
{
   "script" : "ctx._source.views+=1",
   "upsert": {
       "views": 1
   }
}

更新和沖突:

如果發生沖突而我們又不關心其執行順序。只要設置重新嘗試次數就可以

POST /website/pageviews/1/_update?retry_on_conflict=5 <1> { "script" : "ctx._source.views+=1", "upsert": { "views": 0 } }

<1>在錯誤發生前重新更新5次

十、檢索多個文檔

像Elasticsearch一樣,檢索多個文檔依舊非常快,合並多個請求可以避免每個請求單獨的網絡開銷。使用multi-get或者mget  API

mget API參數是一個docs數組,數組的每個節點定義一個文檔的_index   _type _id元數據。如果只是檢索一個或幾個確定的字段也可以定義/_source參數

POST /website/pageviews/1/_update?retry_on_conflict=5 <1> { "script" : "ctx._source.views+=1", "upsert": { "views": 0 } }

響應體也包含docs數組。每個文檔還包含一個相應,他們按照請求定義的順序排列:

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

POST /website/blog/_mget
{
   "docs" : [ { "_id" : 2 }, { "_type" : "pageviews", "_id" : 1 } ] }

也可以簡寫為:

POST /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> } ] }

 

十、批量

像mget允許我們一次性檢索多個文檔一樣,bulk API允許我們使用單一請求來實現多個文檔的create  index  update  delete

bulk請求體:

{ action: { metadata }}\n
{ request body        }\n
{ action: { metadata }}\n
{ request body        }\n

類似於\n符號連接起來的一行一行的JSON文檔流

action/metadata這一行定義了文檔行為發生在哪個文檔上。

行為必須是以下幾種:

create:當文檔不存在時創建

index:創建新文檔或替換已有文檔

update:局部更新文檔

delete刪除一個文檔

在索引,創建更新或刪除時必須制定文檔的_index    _type  _id的元數據

例如刪除請求看起來像這樣:

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

請求體由文檔的_source組成

刪除的時候不需要請求體,如果定義_id,ID將會自動創建

{ "create": { "_index": "website", "_type": "blog", "_id": "123" }} { "title": "My first blog post" }
{ "index": { "_index": "website", "_type": "blog" }} { "title": "My second blog post" }

放在一起:

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沒有請求體,緊跟着另一個行為

<2>記得最后一個換行符

響應結果中包含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" }

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

<3>錯誤消息說明了什么請求錯誤

<4>請求成功


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM