前面的文章裡面主要講的是查詢的用法,還是延續之前的文章格式,這裡講講修改。
1. 單文件修改
1.1. insert
其實在資料準備階段已經有新增的例子了。
DSL
POST /operation_log/_doc
{
"ip": "0.0.0.0",
"module": "測試資料"
}
spring
OperationLog operationLog=new OperationLog();
operationLog.setIp("0.0.0.0");
operationLog.setModule("測試資料");
return esRestTemplate.save(operationLog);
1.2. update-(save)
新增時,springboot 用到的是 save 方法,更新時也一樣可以。不過得拿到文件的id,這裡id=13OkA4QBMgWicIn2wBwM。
DSL
PUT /operation_log/_doc/13OkA4QBMgWicIn2wBwM
{
"ip": "0.0.0.0",
"module": "測試資料1"
}
spring
esRestTemplate.save(operationLog);
1.3. update-(document)
DSL
POST /operation_log/_update/13OkA4QBMgWicIn2wBwM
{
"doc": {
"module":"測試資料1"
}
}
spring
Document document = Document.create();
document.put("module", "測試資料1");
UpdateQuery updateQuery = UpdateQuery
.builder(id)
.withDocument(document)
.build();
esRestTemplate.update(updateQuery,IndexCoordinates.of("operation_log"));
1.4. update-(script)
DSL
POST /operation_log/_update/13OkA4QBMgWicIn2wBwM
{
"script": {
"source": "ctx._source.module = params.module",
"params": {
"module": "測試資料1"
}
}
}
spring
Map<String, Object> params = new HashMap<>();
params.put("module", "測試資料1");
UpdateQuery updateQuery = UpdateQuery
.builder(id)
.withScript("ctx._source.module = params.module")
.withParams(params)
.build();
esRestTemplate.update(updateQuery, IndexCoordinates.of("operation_log"));
1.5. delete
DSL
DELETE /operation_log/_doc/13OkA4QBMgWicIn2wBwM
spring
esRestTemplate.delete(id, OperationLog.class);
2. 批次修改 bulk
批次新增 DSL
POST /operation_log/_bulk
{"create":{"_index":"operation_log"}}
{"ip":"0.0.0.0","module":"測試資料1"}
{"create":{"_index":"operation_log"}}
{"ip":"0.0.0.0","module":"測試資料2"}
{"create":{"_index":"operation_log"}}
{"ip":"0.0.0.0","module":"測試資料3"}
批次更新 DSL
POST /operation_log/_bulk
{"update":{"_id":"2HP9A4QBMgWicIn26BzR"}}
{"doc":{"module":"測試資料11"}}
{"update":{"_id":"2XP9A4QBMgWicIn26BzR"}}
{"script":{"source":"ctx._source.module = params.module","params":{"module":"測試資料22"}}}
批次刪除 DSL
POST /operation_log/_bulk
{"delete":{"_id":"2HP9A4QBMgWicIn26BzR"}}
{"delete":{"_id":"2XP9A4QBMgWicIn26BzR"}}
{"delete":{"_id":"2nP9A4QBMgWicIn26BzR"}}
不知是否注意到,在批次更新的語句中,支援同時 doc、script 兩種更新方式。實際上來說,_bulk
其實支援同時將上述的三種語句一起提交執行。
不過專案上一般不會如此應用,都是單獨分開來。像批次新增,save
方法就支援批次新增操作,雖然底層程式碼還是呼叫 bulkOperation
。
spring bulkUpdate
@PatchMapping("bulk-update")
public void bulkUpdate() {
Map<String, Object> params = new HashMap<>();
params.put("module", "測試資料2");
String scriptStr = "ctx._source.module = params.module";
Query query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.termQuery("ip", "0.0.0.0"))
.build();
List<UpdateQuery> updateQueryList = esRestTemplate.search(query, OperationLog.class)
.stream()
.map(SearchHit::getContent)
.map(obj -> UpdateQuery.builder(obj.getId())
.withScript(scriptStr)
.withParams(params)
.build())
.collect(Collectors.toList());
esRestTemplate.bulkUpdate(updateQueryList, OperationLog.class);
}
有關更詳細、更好使用 bulk的部分,建議檢視 es官網資料
3. 修改ByQuery
3.1. updateByQuery
DSL
POST /operation_log/_update_by_query
{
"script": {
"source": "ctx._source.module = params.module",
"params": {
"module": "測試資料1"
}
},
"query": {
"term": {
"ip": "0.0.0.0"
}
}
}
spring
@PatchMapping("update-by-query")
public void updateByQuery() {
Map<String, Object> params = new HashMap<>();
params.put("module", "測試資料2");
String scriptStr = "ctx._source.module = params.module";
UpdateQuery updateQuery = UpdateQuery
.builder(new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.termQuery("ip", "0.0.0.0"))
.build())
.withScript(scriptStr)
.withScriptType(ScriptType.INLINE)
.withLang("painless")
.withParams(params)
.build();
esRestTemplate.updateByQuery(updateQuery, IndexCoordinates.of("operation_log"));
}
可以對比一下上面的 bulkUpdate
方法,發現有些不同:
- updateByQuery 只支援
Script
,不支援Document
的方式更新。 - updateByQuery 使用 Script 方式更新時,必須傳遞
scriptType
、Lang
這些輔助引數。原本 bulkUpdate 中也是要傳的,只不過底層方法封裝了,但是沒有給 updateByQuery 封裝。(實際踩過坑,看封裝方法才得知)
3.2. deleteByQuery
DSL
POST /operation_log/_delete_by_query
{
"query": {
"term": {
"ip": "0.0.0.0"
}
}
}
spring
Query query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.termQuery("ip", "0.0.0.0"))
.build();
esRestTemplate.delete(query, OperationLog.class);
delete_by_query並不是真正意義上物理文件刪除,而是隻是版本變化並且對文件增加了刪除標記。當我們再次搜尋的時候,會搜尋全部然後過濾掉有刪除標記的文件。因此,該索引所佔的空間並不會隨著該API的操作磁碟空間會馬上釋放掉,只有等到下一次段合併的時候才真正被物理刪除,這個時候磁碟空間才會釋放。相反,在被查詢到的文件標記刪除過程同樣需要佔用磁碟空間,這個時候,你會發現觸發該API操作的時候磁碟不但沒有被釋放,反而磁碟使用率上升了。
3.3. 調優引數
可參考es官網 ElasticSearch API guide,在批次修改文件時,有很多引數可以配合調優。
這裡先列舉幾個常用的,剩下詳細的請看官方文件:
1. refresh
ES的索引資料是寫入到磁碟上的。但這個過程是分階段實現的,因為IO的操作是比較費時的。
- 先寫到記憶體中,此時不可搜尋。
- 預設經過 1s 之後會被寫入 lucene 的底層檔案 segment 中 ,此時可以搜尋到。
- refresh 之後才會寫入磁碟
以上過程由於隨時可能被中斷導致資料丟失,所以每一個過程都會有 translog 記錄,如果中間有任何一步失敗了,等伺服器重啟之後就會重試,保證資料寫入。translog也是先存在記憶體裡的,然後預設5秒刷一次寫到硬碟裡。
在 index ,Update , Delete , Bulk 等操作中,可以設定 refresh 的值。如下:
false:預設值。不要重新整理相關的動作。在請求返回後,此請求所做的更改將在某個時刻顯示。如:
建立一個文件,而不做任何使其可以搜尋的事情: PUT /test/test/1 PUT /test/test/2?refresh=false
true或空字串:更新資料之後,立刻對相關的分片(包括副本) 重新整理,這個重新整理操作保證了資料更新的結果可以立刻被搜尋到。
建立一個文件並立即重新整理索引,使其可見: PUT /test/test/1?refresh PUT /test/test/2?refresh=true
wait_for:等待請求所做的更改在返回之前透過沖刷顯示。這不會強制立即重新整理,而是等待重新整理發生。 Elasticsearch會自動每隔index.refresh_interval重新整理已經更改的分片,預設為1秒。該設定是動態的。
建立一個文件並等待它成為搜尋可見: PUT /test/test/1?refresh=wait_for
2. scroll_size
這個引數是執行刪除的時候,每次每個執行緒會查詢的資料量,然後進行刪除。預設值是100,就是說每個執行緒每次都會查詢出100條資料然後再刪除。
3. slices
可以理解為,預設值是一個執行緒在進行查詢資料並刪除,當設定這個slices值時,會將es下的資料進行切分,啟動多個task去做刪除,理解為多執行緒執行操作。
但是就像不建議濫用多執行緒一樣,不建議設定slices值太大,否則會導致es出問題。建議設為索引分片數量的倍數(如:1倍、2倍),有助於基於每個分片的資料做切分。
4. conflicts
如果按查詢刪除遇到版本衝突,該怎麼辦,有兩個值:
- abort:預設值,衝突時中止。
- proceed:衝突時繼續。
舉前面updateByQuery的例子。_update_by_query 在啟動時獲取索引的快照,並使用內部版本控制對其進行索引。這意味著如果文件在拍攝快照和處理索引請求之間發生變化,則會發生版本衝突。當版本匹配文件被更新並且版本號增加。
所有更新和查詢失敗導致 _update_by_query 中止並在響應失敗中返回。已執行的更新仍然堅持。換句話說,程式沒有回滾,只會中止。當第一個故障導致中止時,失敗批次請求返回的所有故障都會返回到故障元素中;因此,有可能會有不少失敗的實體。
如果你想簡單地計算版本衝突,不會導致 _update_by_query中止,你可以在url設定conflicts=proceed
或在請求體設定"conflicts": "proceed"。如上例中改成:
POST /operation_log/_update_by_query?conflicts=proceed
4. 鎖
Elasticsearch和資料庫一樣,在多執行緒併發訪問修改的情況下,會有一個鎖機制來控制每次修改的均為最新的文件,核心是使用樂觀鎖的機制。
_version
在 Elasticsearch 透過 _version
來記錄文件的版本。第一次建立一個document的時候,它的_version內部版本號就是1;以後,每次對這個document執行修改或者刪除操作,都會對這個_version版本號自動加1;哪怕是刪除,也會對這條資料的版本號加1
由於 segment 時不能被修改的,所以當對一個文件執行 DELETE 之後,在插入相同id的文件,version 版本不會是0,而是在 DELETE 操作的version上遞增。
在對文件進行修改和刪除時,version 會遞增,也可以由使用者指定。只有當版本號大於當前版本時,才會修改刪除成功,否則失敗。當併發請求時,先修改成功的,version 會增加,這個時候其他請求就會猶豫 version 不匹配從而修改失敗。
external version
es提供了一個feature,就是說,你可以不用它提供的內部_version版本號來進行併發控制,可以基於你自己維護的一個版本號來進行併發控制。
舉個例子,假如你的資料在mysql裡也有一份,然後你的應用系統本身就維護了一個版本號,無論是什麼自己生成的或程式控制的。這個時候,你進行樂觀鎖併發控制的時候,可能並不是想要用es內部的_version來進行控制,而是用你自己維護的那個version來進行控制。
?version=1 基於_version
?version=1&version_type=external 基於external version
_version與version_type=external唯一的區別在於:
- _version,只有當你提供的version與es中的_version一模一樣的時候,才可以進行修改,只要不一樣,就報錯。
- 當version_type=external的時候,只有當你提供的version比es中的_version大的時候,才能完成修改。
es,_version=1, ?version=1,才能更新成功
es,_version=1, ?version>1&version_type=external,才能成功,
比如說:?version=2&version_type=external
引用: