文章目录
PUT 是 强制创建、全量更新操作
POST 是 创建、局部更新操作
看上去PUT和POST都可以创建、更新document,那么在创建document、更新document的时候有什么区别呢?
1、创建document 区别
使用ES自动生成id
我们如何确定是完全创建了一个新的还是覆盖了一个已经存在的呢?
请记住 _index 、 _type 、 _id 三者唯一确定一个文档。所以要想保证文档是新加入的,最简单的方式是使用 POST 方法让Elasticsearch自动生成唯一 _id :
POST /website/blog/
{ ... }
使用自定义id
然而,如果想使用自定义的 _id ,我们必须告诉Elasticsearch应该在 _index 、 _type 、 _id 三者都不同时才接受请求。为了做到这点有两种方法,它们其实做的是同一件事情。你可以选择适合自己的方式:
第一种方法使用 op_type 查询参数:
PUT /website/blog/123?op_type=create
{ ... }
或者第二种方法是在URL后加 /_create 做为端点:
PUT /website/blog/123/_create
{ ... }
如果请求成功的创建了一个新文档,Elasticsearch将返回正常的元数据且响应状态码是201 Created 。
另一方面,如果包含相同的 _index 、 _type 和 _id 的文档已经存在,Elasticsearch将返回 409 Conflict 响应状态码,错误信息类似如下:
{
"error" : "DocumentAlreadyExistsException[[website][4] [blog][123]:
document already exists]",
"status" : 409
}
小结:
POST 可以创建ES自动生成id类型的document。
PUT id存在为创建,否则为全量替换。
另外PUT还可以 使用op_type=create或_create实行强制创建document。
2、更新document 区别
PUT
全量替换
文档在Elasticsearch中是不可变的——我们不能修改他们。如果需要更新已存在的文档,我们可以使用《索引文档》 章节提到的 index API 重建索引(reindex) 或者替换掉它。
PUT,就是全量替换,POST可以实现局部替换。
这里说的全量、局部,其实是指针对用户而言的,底层ES处理的时候,都会将旧的数据标记delete,重新创建一条记录,当然版本号都会+1.
例如,id为123是一条已经存在的记录,使用PUT时,那么id=123的这条数据,不管之前有多少个字段,那么是100个字段,那么执行完以下这条数据之后,也就只剩下3个字段了:title、text、date。
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>
}
我们可以看到Elasticsearch把 _version 增加了。
<1> created 标识为 false 因为同索引、同类型下已经存在同ID的文档。在内部,Elasticsearch已经标记旧文档为删除并添加了一个完整的新文档。旧版本文档不会立即消失,但你也不能去访问它。Elasticsearch会在你继续索引更多数据时清理被删除的文档。
POST
update API必须遵循相同的规则。表面看来,我们似乎是局部更新了文档的位置,内部却是像我们之前说的一样简单的使用 update API处理相同的检索-修改-重建索引流程,我们也减少了其他进程可能导致冲突的修改。
(1)最简单的 update 请求
最简单的 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" ], <1>
"views": 0 <1>
}
}
<1> 我们新添加的字段已经被添加到 _source 字段中。
这个API 似乎 允许你修改文档的局部,但事实上Elasticsearch遵循与之前所说完全相同的过程,这个过程如下:
1. 从旧文档中检索JSON
2. 修改它
3. 删除旧文档
4. 索引新文档
唯一的不同是 update API完成这一过程只需要一个客户端请求既可,不再需要 get 和 index 请求了。
(2)使用脚本局部更新
使用Groovy脚本
这时候当API不能满足要求时,Elasticsearch允许你使用脚本实现自己的逻辑。脚本支持非常多的API,例如搜索、排序、聚合和文档更新。脚本可以通过请求的一部分、检索特殊的 .scripts 索引或者从磁盘加载方式执行。
默认的脚本语言是Groovy,一个快速且功能丰富的脚本语言,语法类似于Javascript。它在一个沙盒(sandbox)中运行,以防止恶意用户毁坏Elasticsearch或攻击服务器。
i. 修改_source字段内容
脚本能够使用 update API改变 _source 字段的内容,它在脚本内部以 ctx._source 表示。例如,我们可以使用脚本增加博客的 views 数量:
POST /website/blog/1/_update
{
"script" : "ctx._source.views+=1"
}
ii. 添加数组字段的值
我们还可以使用脚本增加一个新标签到 tags 数组中。在这个例子中,我们定义了一个新标签做为参数而不是硬编码在脚本里。这允许Elasticsearch未来可以重复利用脚本,而不是在想要增加新标签时必须每次编译新脚本:
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 字段已经被增加。
iii. 根据内容删除文档
通过设置 ctx.op 为 delete 我们可以根据内容删除文档
POST /website/blog/1/_update
{
"script" : "ctx.op = ctx._source.views == count ? 'delete' : 'none'",
"params" : {
"count": 1
}
}
iv. 更新可能不存在的文档
想象我们要在Elasticsearch中存储浏览量计数器。每当有用户访问页面,我们增加这个页面的浏览量。但如果这是个新页面,我们并不确定这个计数器存在与否。当我们试图更新一个不存在的文档,更新将失败。
在这种情况下,我们可以使用 upsert 参数定义文档来使其不存在时被创建。
POST /website/pageviews/1/_update
{
"script" : "ctx._source.views+=1",
"upsert": {
"views": 1
}
}
第一次执行这个请求, upsert 值被索引为一个新文档,初始化 views 字段为 1 .接下来文档已经存在,所以 script 被更新代替,增加 views 数量。
v. 更新和冲突
这这一节的介绍中,我们介绍了如何在检索(retrieve)和重建索引(reindex)中保持更小的窗口,如何减少冲突性变更发生的概率,不过这些无法被完全避免,像一个其他进程在 update 进行重建索引时修改了文档这种情况依旧可能发生。
为了避免丢失数据, update API在检索(retrieve)阶段检索文档的当前 _version ,然后在
建索引(reindex)阶段通过 index 请求提交。如果其他进程在检索(retrieve)和重建索引(reindex)阶段修改了文档, _version 将不能被匹配,然后更新失败。
对于多用户的局部更新,文档被修改了并不要紧。例如,两个进程都要增加页面浏览量,增加的顺序我们并不关心——如果冲突发生,我们唯一要做的仅仅是重新尝试更新既可。
这些可以通过 retry_on_conflict 参数设置重试次数来自动完成,这样 update 操作将会在发生错误前重试——这个值默认为0。
POST /website/pageviews/1/_update?retry_on_conflict=5 <1>
{
"script" : "ctx._source.views+=1",
"upsert": {
"views": 0
}
}
<1> 在错误发生前重试更新5次
这适用于像增加计数这种顺序无关的操作,但是还有一种顺序非常重要的情况。例如indexAPI,使用“保留最后更新(last-write-wins)”的 update API,但它依旧接受一个 version 参数以允许你使用乐观并发控制(optimistic concurrency control)来指定你要更细文档的版本。
3、HTTP 协议之PUT POST
这俩个方法初看一下好像都是更新资源,但是有本质上的区别,那就是语义。在HTTP中,PUT被定义为幂等(idempotent)的方法,POST则不是,这是一个很重要的区别。
首先解释幂等,幂等是数学的一个用语,对于单个输入或者无输入的运算方法,如果每次都是同样的结果,则称其是幂等的。也就是说,如果一个网络重复执行多次,产生的效果是一样的,那就是幂等(idempotent)。
POST
用于提交请求,可以更新或者创建资源,是非幂等的,举个例子:
在用户注册功能上,每次提交都是创建一个用户账号,这个时候就用POST。
ESTful URL地址应为:/user/creation?user_name=&pwd=
PUT
用于向指定URL传送更新资源,是幂等的。
还是用户模块,比如修改用户密码,虽然提交的还是账户名跟用户密码这个俩个必填参数,但是每次提交都只是更新该用户密码,每次请求都只是覆盖原先的值。此时就该用PUT。
ESTful URL地址应为:/user/{user_id}/modify?pwd=**
用PUT还是POST
当需要以更新的形式来修改某一具体资源的时候,如何判断用PUT还是POST呢?
很简单,如果该更新对应的URI多次调用的结果一致,则PUT。如果每次提交相同的内容,最终结果不一致的时候,用POST。
HTML4.0只支持POST和GET,所以无论DELETE还是PUT操作,都用POST去模拟了
参考链接:
https://www.cnblogs.com/ximenxiazi/p/5850273.html
https://blog.csdn.net/dream_follower/article/details/90048425
4、总结:
以上内容多数资料来自于《ES权威指南》这本书,个人认为把PUT和POST放到一起来讲,更加容易理解和接受。
另外,通过了解了PUT和POST的区别,更能深入理解HTTP的请求方式,或者说restful风格。当然深入理解了HTTP的PUT和POST,也能反过来更加理解ES的PUT和POST操作。
举例,给view字段+1操作
如果用PUT,全量替换的意义+HTTP PUT协议说明,那就是无论我请求几次,给定的view值,都是一样的,比如 view=5,请求10次,最终view的值都是5,但_version变化了10次。
而POST请求,则是每次请求都+1,假设view初始值是5,执行了10次POST请求,则最终view的值将变成15.
疑问
POST最简单的updage操作,给定doc参数,变化的值,似乎与PUT请求效果一样,那么这种操作,除了ES语法和底层的逻辑略有区别之外,这里的PUT和POST,在HTTP协议方面有何区别呢?如何体现POST的非幂等性呢?