💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
更新文档中的一部分 在《更新》一章中,我们讲到了要是想更新一个文档,那么就需要去取回数据,更改数据然后将整个文档进行重新索引。当然,你还可以通过使用更新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次 这个参数非常适用于类似于增加计数器这种无关顺序的请求,但是还有些情况的顺序就是很重要的。例如上一节提到的情况,你可以参考乐观并发控制以及悲观并发控制来设定文档的版本号。