Elastcisearch 是分布式的 文檔 存儲。它能存儲和檢索復雜的數據結構–序列化成為JSON文檔–以 實時 的方式。 換句話說,一旦一個文檔被存儲在 Elasticsearch 中,它就是可以被集群中的任意節點檢索到。
在 Elasticsearch 中, 每個字段的所有數據 都是 默認被索引的 。 即每個字段都有為了快速檢索設置的專用倒排索引。而且,不像其他多數的數據庫,它能在 相同的查詢中 使用所有這些倒排索引,並以驚人的速度返回結果。
文檔
不同於傳統數據庫,為了完美表現對象的靈活性,elasticsearch使用JSON這種標准格式,以一種人可讀的文本表示對象。
在大多數應用中,多數實體或對象可以被序列化為包含鍵值對的 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" } ] }
在 Elasticsearch 中,術語 文檔 有着特定的含義。它是指最頂層或者根對象, 這個根對象被序列化成 JSON 並存儲到 Elasticsearch 中,指定了唯一 ID。
字段的名字可以是任何合法的字符串,但不可以包含時間段
文檔元數據
一個文檔不僅僅包含它的數據 ,也包含 元數據 —— 有關 文檔的信息。 三個必須的元數據元素如下:
_index 文檔在哪存放 _type 文檔表示的對象類別 _id 文檔唯一標識
索引
這是一種邏輯上的命名,實際文檔存儲在分片中,一個索引中包含多個分片。這種命名是為了我們更好的理解,它表示一類共性事務的集合。比如說產品一類都放入一個索引,索引為product。
類型
也是為了更好的理解,在同一個索引下(同一類事務下),不同種類的數據的邏輯區分。例如一個product產品索引下,有玩具類型(type = toy)的產品,也可以有食品類型的(type=food)的產品,他們在同一索引下,因為他們是同一類事務下有共性特征的產品,比如他們都有價格,都有成本,都有生產日期等。
id
標識。和索引,類型組合就可以唯一確定當前的產品了。
索引文檔
在創建一個新的文檔的時候,你可以選擇自定義id或是讓elasticsearch為你自動生成一個文檔id
自定義id
在put語法后增加自定義的id
PUT /{index}/{type}/{id}
{
"field": "value", ... }
Head插件示例
會發現自動增加了索引和類型,也就是自動成功索引了:
Client程序演示
客戶端示例之前介紹過:
client.prepareIndex("website","blog","123")//index type id(表特定雇員) .setSource(XContentFactory.jsonBuilder() .startObject() .field("title","My first blog entry") .field("text","Just trying this out") .field("date","2014/01/01") .endObject()).get();
自動生成id
在post后不增加自定義id
POST /{index}/{type}/
{
"field": "value", ... }
自動生成的 ID 是 URL-safe、 基於 Base64 編碼且長度為20個字符的 GUID 字符串。 這些 GUID 字符串由可修改的 FlakeID 模式生成,這種模式允許多個節點並行生成唯一 ID ,且互相之間的沖突概率幾乎為零,大概長這樣:AVFgSgVHUP18jI2wRx0w
創建文檔操作會響應一個新的_version元素,在 Elasticsearch 中每個文檔都有一個版本號。當每次對文檔進行修改時(包括刪除), _version 的值會遞增。
Head插件示例
Client程序演示
Client示例:不寫id即可,其他相同
client.prepareIndex("website1","blog1")//index type (表特定雇員) .setSource(XContentFactory.jsonBuilder() .startObject() .field("title","My first blog entry") .field("text","Just trying this out") .field("date","2014/01/01") .endObject()).get();
創建新文檔
當我們索引一個文檔, 怎么確認我們正在創建一個完全新的文檔,而不是覆蓋現有的呢?
1.確保創建一個新文檔的最簡單辦法是,使用索引請求的 POST 形式讓 Elasticsearch 自動生成唯一 _id
2.然而,如果已經有自己的 _id ,那么我們必須告訴 Elasticsearch ,只有在相同的 _index 、 _type 和 _id 不存在時才接受我們的索引請求。這里有兩種方式,他們做的實際是相同的事情。使用哪種,取決於哪種使用起來更方便。
第一種方法使用 op_type 查詢 -字符串參數:
PUT /website/blog/123?op_type=create
{ … }
第二種方法是在 URL 末端使用 /_create :
PUT /website/blog/123/_create
{ … }
如果創建新文檔的請求成功執行,Elasticsearch 會返回元數據和一個 201 Created 的 HTTP 響應碼。
另一方面,如果具有相同的 _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
}
Head插件示例
第一種方法:
第二種方法:
Client程序演示
增加新方法:
// 確認ID不存在才索引 設置OpType為CREATE
private static void insertBlogConfirmID(Client client) throws IOException {
IndexResponse response = client.prepareIndex("website1","blog1","1")//index type id(表特定雇員) .setSource(XContentFactory.jsonBuilder() .startObject() .field("title","My first blog entry") .field("text","Just trying this out") .field("date","2014/01/01") .endObject()) .setOpType(OpType.CREATE).get(); System.out.println(response); }
調用:
// 5.確認ID不存在才索引
insertBlogConfirmID(client);
- 1
結果顯示:
客戶端操作錯誤:[blog1][1]: version conflict, document already exists (current version [1])
搜索一個文檔
get語法后加上元數據索引
GET /{index}/{type}/{id}
- 1
響應會在_source中表現出我們索引的文檔,GET 請求的響應體包括 {“found”: true} ,這證實了文檔已經被找到。 如果我們請求一個不存在的文檔,我們仍舊會得到一個 JSON 響應體,但是 found 將會是 false 。 此外, HTTP 響應碼將會是 404 Not Found ,而不是 200 OK 。
Head插件示例
Client程序演示
Client示例之前學習過:
GetResponse response3 = client.prepareGet(index, type, id).execute().actionGet(); System.out.println(response3.getSourceAsString());//這是_source部分
- 1
- 2
- 3
一部分文檔返回
默認情況下, GET 請求 會返回整個文檔,這個文檔正如存儲在 _source 字段中的一樣。但是也許你只對其中的 title 字段感興趣。單個字段能用 _source 參數請求得到,多個字段也能使用逗號分隔的列表來指定。
GET /website/blog/123?_source=title,text
該 _source 字段現在包含的只是我們請求的那些字段,並且已經將 date 字段過濾掉了。
{
“_index” : “website”,
“_type” : “blog”,
“_id” : “123”,
“_version” : 1,
“found” : true,
“_source” : {
“title”: “My first blog entry” ,
“text”: “Just trying this out…”
}
}
Head插件示例
或者,如果你只想得到 _source 字段,不需要任何元數據,你能使用 _source 端點:
GET /website/blog/123/_source
View in Sense
那么返回的的內容如下所示:
{
“title”: “My first blog entry”,
“text”: “Just trying this out…”,
“date”: “2014/01/01”
}
Client程序演示
增加一個方法:
/** * 檢索部分文檔 在客戶端中使用_source返回指定的字段 * SearchRequestBuilder.setFetchSource(inludes,excludes);可指示顯示返回和不返回的字段 * @param client * @param index * @param type * @param id */ private static void getPartBlogByPK(Client client, String index, String type, String id) { SearchRequestBuilder srb = new SearchRequestBuilder(client,SearchAction.INSTANCE); srb.setIndices(index); srb.setTypes(type); srb.setQuery(QueryBuilders.termQuery("_id", id)); srb.setFetchSource(new String[] {"title","text"}, null); SearchResponse response = srb.get(); System.out.println(response); }
Main方法增加調用:
// 4.檢索部分文檔 getPartBlogByPK(client,"website1","blog1","1");
結果顯示:
{“took”:3,”timed_out”:false,”_shards”:{“total”:5,”successful”:5,”failed”:0},”hits”:{“total”:1,”max_score”:1.0,”hits”:[{“_index”:”website1”,”_type”:”blog1”,”_id”:”1”,”_score”:1.0,”_source”:{“text”:”Just trying this out”,”title”:”My first blog entry”}}]}}
可看到_source只返回text和tile字段
檢查文檔是否存在
如果只想檢查一個文檔是否存在 –根本不想關心內容–那么用 HEAD 方法來代替 GET 方法。 HEAD 請求沒有返回體,只返回一個 HTTP 請求報頭:
curl -i -XHEAD http://localhost:9200/website/blog/123
如果文檔存在, Elasticsearch 將返回一個 200 ok 的狀態碼:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
若文檔不存在, Elasticsearch 將返回一個 404 Not Found 的狀態碼:
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 中文檔是 不可改變 的,不能修改它們。 相反,如果想要更新現有的文檔,需要 重建索引 或者進行替換
只需put語法后id參數是已有的,那么傳遞過去的文檔將會將就文檔替換掉
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
}
created 標志設置成 false ,是因為相同的索引、類型和 ID 的文檔已經存在。
在內部,Elasticsearch 已將舊文檔標記為已刪除,並增加一個全新的文檔。 盡管你不能再對舊版本的文檔進行訪問,但它並不會立即消失。當繼續索引更多的數據,Elasticsearch 會在后台清理這些已刪除文檔。
文檔的部分更新
它其實可以表面理解為一種合並文檔的操作(結果來看),
然而在內部, update API 簡單使用與之前描述相同的 檢索-修改-重建索引 的處理過程。 區別在於這個過程發生在分片內部,這樣就避免了多次請求的網絡開銷。通過減少檢索和重建索引步驟之間的時間,我們也減少了其他進程的變更帶來沖突的可能性。
POST /website/blog/1/_update { "doc" : { "tags" : [ "testing" ], "views": 0 } }
在 更新整個文檔 , 我們已經介紹過 更新一個文檔的方法是檢索並修改它,然后重新索引整個文檔,這的確如此。然而,使用 update API 我們還可以部分更新文檔,例如在某個請求時對計數器進行累加。
我們也介紹過文檔是不可變的:他們不能被修改,只能被替換。 update API 必須遵循同樣的規則。 從外部來看,我們在一個文檔的某個位置進行部分更新。然而在內部, update API 簡單使用與之前描述相同的 檢索-修改-重建索引 的處理過程。 區別在於這個過程發生在分片內部,這樣就避免了多次請求的網絡開銷。通過減少檢索和重建索引步驟之間的時間,我們也減少了其他進程的變更帶來沖突的可能性。
update 請求最簡單的一種形式是接收文檔的一部分作為 doc 的參數, 它只是與現有的文檔進行合並。對象被合並到一起,覆蓋現有的字段,增加新的字段。 例如,我們增加字段 tags 和 views 到我們的博客文章,如下所示:
POST /website/blog/1/_update
{
“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
}
}
可參考https://www.cnblogs.com/xing901022/p/5330778.html
Client程序演示
/* * 更新部分文檔doc * 我們知道兩次索引(put操作--add),第一次執行的是create第二次的結果其實就相當於update整個文檔 */ private static void updateBlogByDoc(Client client, String index, String type, String id) throws IOException, InterruptedException, ExecutionException { UpdateRequest updateRequest = new UpdateRequest(); updateRequest.index(index); updateRequest.type(type); updateRequest.id(id); updateRequest.doc(XContentFactory.jsonBuilder() .startObject() .field("views","0") .array("tags", "testing") .endObject()); client.update(updateRequest).get(); } 調用: updateBlogByDoc(client,"website1","blog1","1"); getBlogByPK(client,"website1","blog1","1");
結果顯示:
{“title”:”My first blog entry”,”text”:”Just trying this out”,”date”:”2014/01/01”,”views”:”0”,”tags”:[“testing”]}
Head插件演示
使用腳本部分更新
你可以在 scripting reference documentation 獲取更多關於腳本的資料。
我們也可以通過使用腳本給 tags 數組添加一個新的標簽。在這個例子中,我們指定新的標簽作為參數,而不是硬編碼到腳本內部。 這使得 Elasticsearch 可以重用這個腳本,而不是每次我們想添加標簽時都要對新腳本重新編譯:
POST /website/blog/1/_update
{
“script” : “ctx._source.tags+=new_tag”,
“params” : {
“new_tag” : “search”
}
}
View in Sense
獲取文檔並顯示最后兩次請求的效果:
{
“_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 數組中。
Client程序演示
/* * 更新部分文檔script * 我們知道兩次索引(put操作--add),第一次執行的是create第二次的結果其實就相當於update整個文檔 * inline script默認是被禁止的,但是可以使用file script的模式 * 如果有多個node, 必須在每個node的elasticsearch.yml中, 都加入上述配置, 否則, script不能使用。 * 要打開, 需要在config/elasticsearch.yml中添加如下配置: script.inline:true script.indexed:true */ private static void updateBlogByScript(Client client, String index, String type, String id) throws InterruptedException, ExecutionException { Map<String,Object> map = new HashMap<String,Object>(); map.put("new_tag", "search"); UpdateRequest updateRequest = new UpdateRequest(index, type, id) .script("ctx._source.tags.add(params.new_tag)", ScriptType.INLINE, map); client.update(updateRequest).get(); System.out.println(updateRequest.scriptString()); } 調用: updateBlogByScript(client,"website1","blog1","1"); getBlogByPK(client,"website1","blog1","1");
結果顯示:
ctx._source.tags.add(params.new_tag)
{“title”:”My first blog entry”,”text”:”Just trying this out”,”date”:”2014/01/01”,”views”:0,”tags”:[“testing”,”search”]}
Head插件示例
我們甚至可以選擇通過設置 ctx.op 為 delete 來刪除基於其內容的文檔:
POST /website/blog/1/_update
{
“script” : “ctx.op = ctx._source.views == count ? ‘delete’ : ‘none’”,
“params” : {
“count”: 1
}
}
更新的文檔可能尚不存在
腳本可以在 update API中用來改變 _source 的字段內容, 它在更新腳本中稱為 ctx._source 。 例如,我們可以使用腳本來增加博客文章中 views 的數量:
POST /website/blog/1/_update
{
“script” : “ctx._source.views+=1”
}
用 Groovy 腳本編程
對於那些 API 不能滿足需求的情況,Elasticsearch 允許你使用腳本編寫自定義的邏輯。 許多API都支持腳本的使用,包括搜索、排序、聚合和文檔更新。 腳本可以作為請求的一部分被傳遞,從特殊的 .scripts 索引中檢索,或者從磁盤加載腳本。
默認的腳本語言 是 Groovy,一種快速表達的腳本語言,在語法上與 JavaScript 類似。 它在 Elasticsearch V1.3.0 版本首次引入並運行在 沙盒 中,然而 Groovy 腳本引擎存在漏洞, 允許攻擊者通過構建 Groovy 腳本,在 Elasticsearch Java VM 運行時脫離沙盒並執行 shell 命令。
因此,在版本 v1.3.8 、 1.4.3 和 V1.5.0 及更高的版本中,它已經被默認禁用。 此外,您可以通過設置集群中的所有節點的 config/elasticsearch.yml 文件來禁用動態 Groovy 腳本:
script.groovy.sandbox.enabled: false
這將關閉 Groovy 沙盒,從而防止動態 Groovy 腳本作為請求的一部分被接受, 或者從特殊的 .scripts 索引中被檢索。當然,你仍然可以使用存儲在每個節點的 config/scripts/ 目錄下的 Groovy 腳本。
如果你的架構和安全性不需要擔心漏洞攻擊,例如你的 Elasticsearch 終端僅暴露和提供給可信賴的應用, 當它是你的應用需要的特性時,你可以選擇重新啟用動態腳本。
假設我們需要 在 Elasticsearch 中存儲一個頁面訪問量計數器。 每當有用戶瀏覽網頁,我們對該頁面的計數器進行累加。但是,如果它是一個新網頁,我們不能確定計數器已經存在。 如果我們嘗試更新一個不存在的文檔,那么更新操作將會失敗。
在這樣的情況下,我們可以使用 upsert 參數,指定如果文檔不存在就應該先創建它:
POST /website/pageviews/1/_update
{
“script” : “ctx._source.views+=1”,
“upsert”: {
“views”: 1
}
}
我們第一次運行這個請求時, upsert 值作為新文檔被索引,初始化 views 字段為 1 。 在后續的運行中,由於文檔已經存在, script 更新操作將替代 upsert 進行應用,對 views 計數器進行累加。
Head插件示例
加入字段的文檔id不存在,會報錯
使用upsert字段:
Client程序演示
//更新部分文檔script 不報錯 upset private static void updateBlogByScriptNoException(Client client, String index, String type, String id) throws InterruptedException, ExecutionException { Map<String,Object> map = new HashMap<String,Object>(); map.put("new_tag", "search"); UpdateRequest updateRequest = new UpdateRequest(index, type, id) .script("ctx._source.tags.add(params.new_tag)", ScriptType.INLINE, map).upsert(map); client.update(updateRequest).get(); System.out.println(updateRequest.scriptString()); } // 10.更新部分文檔script腳本 id不存在的文檔直接插入不報錯 updateBlogByScriptNoException(client,"website1","blog1","5"); getBlogByPK(client,"website1","blog1","5");
顯示
連接成功…
ctx._source.tags.add(params.new_tag)
{“new_tag”:”search”}
website1–blog1–5–1
更新和沖突
在本節的介紹中,我們說明 檢索 和 重建索引 步驟的間隔越小,變更沖突的機會越小。 但是它並不能完全消除沖突的可能性。 還是有可能在 update 設法重新索引之前,來自另一進程的請求修改了文檔。
為了避免數據丟失, update API 在 檢索 步驟時檢索得到文檔當前的 _version 號,並傳遞版本號到 重建索引 步驟的 index 請求。 如果另一個進程修改了處於檢索和重新索引步驟之間的文檔,那么 _version 號將不匹配,更新請求將會失敗。
對於部分更新的很多使用場景,文檔已經被改變也沒有關系。 例如,如果兩個進程都對頁面訪問量計數器進行遞增操作,它們發生的先后順序其實不太重要; 如果沖突發生了,我們唯一需要做的就是嘗試再次更新。
這可以通過 設置參數 retry_on_conflict 來自動完成, 這個參數規定了失敗之前 update 應該重試的次數,它的默認值為 0 。
POST /website/pageviews/1/_update?retry_on_conflict=5
{
“script” : “ctx._source.views+=1”,
“upsert”: {
“views”: 0
}
}
失敗之前重試該更新5次。
在增量操作無關順序的場景,例如遞增計數器等這個方法十分有效,但是在其他情況下變更的順序 是 非常重要的。 類似 index API , update API 默認采用 最終寫入生效 的方案,但它也接受一個 version 參數來允許你使用 optimistic concurrency control 指定想要更新文檔的版本。
刪除文檔
DELETE 語法后增加索引等參數
DELETE /{index}/{type}/{id}
- 1
如果找到該文檔,Elasticsearch 將要返回一個 200 ok 的 HTTP 響應碼
如果文檔沒有 找到,我們將得到 404 Not Found 的響應碼
即使文檔不存在( Found 是 false ), _version 值仍然會增加。這是 Elasticsearch 內部記錄本的一部分,用來確保這些改變在跨多節點時以正確的順序執行。
正如已經在更新整個文檔中提到的,刪除文檔不會立即將文檔從磁盤中刪除,只是將文檔標記為已刪除狀態。隨着你不斷的索引更多的數據,Elasticsearch 將會在后台清理標記為已刪除的文檔。
Head插件示例
再次執行:
Client程序演示
// 刪除一個文檔 private static void delBlogByPk(Client client, String index, String type, String id) { DeleteResponse response4 = client.prepareDelete(index,type,id).get(); System.out.println(response4.getResult().toString()); } // 5.刪除一個文檔 delBlogByPk(client,"website1","blog1","1"); 調用顯示: DELETED
處理沖突
Elasticsearch 需要一種方法確保文檔的舊版本不會覆蓋新的版本。 Elasticsearch 使用這個 _version 號來確保變更以正確順序得到執行。如果舊版本的文檔在新版本之后到達,它可以被簡單的忽略。
我們可以利用 _version 號來確保 應用中相互沖突的變更不會導致數據丟失。我們通過指定想要修改文檔的 version 號來達到這個目的。 如果該版本不是當前版本號,我們的請求將會失敗。
但是我們所有的操作都將更改這個version號,即elasticsearch采取的樂觀鎖的方式來處理的並發事件。即我們的數據總是最新操作的數據。
樂觀並發控制
在數據庫領域中,有兩種方法通常被用來確保並發更新時變更不會丟失:
悲觀並發控制
這種方法被關系型數據庫廣泛使用,它假定有變更沖突可能發生,因此阻塞訪問資源以防止沖突。 一個典型的例子是讀取一行數據之前先將其鎖住,確保只有放置鎖的線程能夠對這行數據進行修改。
樂觀並發控制
Elasticsearch 中使用的這種方法假定沖突是不可能發生的,並且不會阻塞正在嘗試的操作。 然而,如果源數據在讀寫當中被修改,更新將會失敗。應用程序接下來將決定該如何解決沖突。 例如,可以重試更新、使用新的數據、或者將相關情況報告給用戶。
Elasticsearch 是分布式的。當文檔創建、更新或刪除時, 新版本的文檔必須復制到集群中的其他節點。Elasticsearch 也是異步和並發的,這意味着這些復制請求被並行發送,並且到達目的地時也許 順序是亂的 。 Elasticsearch 需要一種方法確保文檔的舊版本不會覆蓋新的版本。
當我們之前討論 index , GET 和 delete 請求時,我們指出每個文檔都有一個 _version (版本)號,當文檔被修改時版本號遞增。 Elasticsearch 使用這個 _version 號來確保變更以正確順序得到執行。如果舊版本的文檔在新版本之后到達,它可以被簡單的忽略。
我們可以利用 _version 號來確保 應用中相互沖突的變更不會導致數據丟失。我們通過指定想要修改文檔的 version 號來達到這個目的。 如果該版本不是當前版本號,我們的請求將會失敗。
讓我們創建一個新的博客文章:
PUT /website/blog/1/_create
{
“title”: “My first blog entry”,
“text”: “Just trying this out…”
}
響應體告訴我們,這個新創建的文檔 _version 版本號是 1 。現在假設我們想編輯這個文檔:我們加載其數據到 web 表單中, 做一些修改,然后保存新的版本。
首先我們檢索文檔:
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
{
“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 。
我們現在怎么做取決於我們的應用需求。我們可以告訴用戶說其他人已經修改了文檔,並且在再次保存之前檢查這些修改內容。 或者,在之前的商品 stock_count 場景,我們可以獲取到最新的文檔並嘗試重新應用這些修改。
所有文檔的更新或刪除 API,都可以接受 version 參數,這允許你在代碼中使用樂觀的並發控制,這是一種明智的做法。
Head插件示例
Client程序演示
先兩次執行put操作讓version=2,之后我們調用:
//檢索一個固定版本的文檔
private static void getBlogWithVersion(Client client, String index, String type, String id,
long version) {
GetResponse response3 = client.prepareGet(index, type, id).setVersion(version).execute().actionGet(); System.out.println(response3.getSourceAsString());//這是_source部分 System.out.println(response3.getIndex()+"--"+response3.getType()+"--"+response3.getId()+"--"+response3.getVersion()); } // 7.檢索一個固定版本文檔 getBlogWithVersion(client,"website1","blog1","1",1L);
調用結果如下:
客戶端操作錯誤:[blog1][1]: version conflict, current version [2] is different than the one provided [1]
通過外部系統使用版本控制
一個常見的設置是使用其它數據庫作為主要的數據存儲,使用 Elasticsearch 做數據檢索, 這意味着主數據庫的所有更改發生時都需要被復制到 Elasticsearch ,如果多個進程負責這一數據同步,你可能遇到類似於之前描述的並發問題。
如果你的主數據庫已經有了版本號 — 或一個能作為版本號的字段值比如 timestamp — 那么你就可以在 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 的當前版本號。