更新文檔中的一部分
在《更新》一章中,我們講到了要是想更新一個文檔,那么就需要去取回數據,更改數據然后將整個文檔進行重新索引。當然,你還可以通過使用更新API來做部分更新,比如增加一個計數器。
正如我們提到的,文檔不能被修改,它們只能被替換掉。更新API也必須遵循這一法則。從表面看來,貌似是文檔被替換了。對內而言,它必須按照找回-修改-索引的流程來進行操作與管理。不同之處在于這個流程是在一個片(shard) 中完成的,因此可以節省多個請求所帶來的網絡開銷。除了節省了步驟,同時我們也能減少多個進程造成沖突的可能性。
使用更新請求最簡單的一種用途就是添加新數據。新的數據會被合并到現有數據中,而如果存在相同的字段,就會被新的數據所替換。例如我們可以為我們的博客添加tags和views字段:
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>
}
}
~~~
新的數據已經添加到了字段_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>
}
}
~~~
tags數組中出現了search。
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
}
}
~~~
失敗前重新嘗試5次
這個參數非常適用于類似于增加計數器這種無關順序的請求,但是還有些情況的順序就是很重要的。例如上一節提到的情況,你可以參考樂觀并發控制以及悲觀并發控制來設定文檔的版本號。