前面的文章里面主要讲的是查询的用法,还是延续之前的文章格式,这里讲讲修改。
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
引用:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。