更新整个文档 ,我们已经介绍过更新一个文档的方法是检索并修改他,然后重新索引整个文档,这的确如此。然而,使用update API 我们还可以部分更新文档,例如在某个请求时对计数器进行累加。

我们也介绍过文档是不可变的:他们不能被修改,只能被替换。update API必须遵循同样的规则。从外部来看,我们在一个文档的某个位置进行部分更新。然而在内部,update API简单使用与之前描述相同的检索-修改-重建索引的处理过程。区别在于这个过程发生在分片内部,这样就避免了多次 请求的网络开销。通过减少检索和重建索引步骤之间的时间,我们也减少了其他进程的变更带来冲突的可能性。

update请求最简单的一种形式是接收文档的一部分作为doc 的参数,他只是与现有的文档进行合并。对象被合并到一起,覆盖现有的字段,增加新的字段。例如,我们增加字段tags 和 views 到我们的博客文章,如下所示

  1. POST /website/blog/1/_update
  2. {
  3. "doc" : {
  4. "tags" : [ "testing" ],
  5. "views": 0
  6. }
  7. }

如果请求成功,我们看到类似于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" ], 
      "views":  0 
   }
}

新的字段已被添加到_source中。

使用脚本部分更新文档

脚本可以 在update API 中用来改变_source 的字段内容,他在更新脚本中称为 ctx._source。例如,我们可以使用脚本来增加博客文章中views 的数量。

POST /website/blog/1/_update
{
   "script" : "ctx._source.views+=1"
}

用Groovy脚本教程

对于那些API 不能满足需求的情况,Elasticsearch允许你使用脚本编写自定义的逻辑。许多API 都支持脚本的使用,包括搜索、排序、聚合和文档更新。脚本可以作为请求的一部分被传递,从特殊的 .scripts 索引中检索,或者从磁盘加载脚本。

默认地脚本语言是Groovy,一种快速表达的脚本语言,在语法上与JavaScript类似。他在ElasticsearchV1.3.0 版本首次引入并运行在沙盒中,然而Groovy脚本引擎存在漏洞,允许攻击者通过构建Groovy脚本,在Elasticsearch Java VM 运行时脱离沙盒并执行shell 命令。

因此,在版本v1.3.8、1.4.3和V1.5.0及更高的版本中,他已经被默认禁用。此外,您可以通过设置集群中的所有节点的config/elasticsearch.yml 文件来禁用动态Groovy脚本:

script.groovy.sandbox.enabled: false

这将关闭Groovy沙盒,从而防止动态Groovy脚本作为请求的一部分被接受,或者从特殊的.scripts 索引中被检索。当然,你仍然可以使用存储在每个节点的config/scripts/ 目录下的Groovy脚本。

如果你的架构和安全性不需要担心漏洞攻击,例如你的Elasticsearch终端仅暴露和提供给可信赖的应用,当他是你的应用需要的特性时,你可以选择重新启动动态脚本。

你可以在 scripting reference documentation 获取更改关于脚本的资料。

我们也可以通过脚本给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"], 
      "views":  1 
   }
}
  1. search 标签已追加到tags 数组中
  2. views 字段已递增。

我们甚至可以选择通过设置ctx.op 为delete 来删除基于其内容的文档:

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。在后续的运行中,由于文档已经存在,script 更新操作将替代upsert 进行应用,对views 计数器进行累加。

更新和冲突

在本节的介绍中,我们说明检索和重建索引步骤的间隙越小,变更冲突的机会越小。但是他并不能完全消除冲突的可能性。还是有可能在update设法重新索引之前,来自另一进程的请求修改了文档。

为了避免数据丢失,update API在检索步骤时检索得到文档当前的_version 号,并传递版本号到重建索引步骤的index请求。如果另一个进程修改了处于检索和重建索引步骤之间的文档,那么_version 号将不匹配,更新请求将会失败。

对于部分更新的很多使用场景,文档已经被改变也没有关系。例如,如果两个进程都对页面访问量计数器进行递增操作,他们发生的先后顺序其实不太重要;如果冲突发生了,我们唯一需要做的就是尝试再次更新。

这可以通过设置参数 retry_on_conflict 来自动完成,这个参数规定了失败之前update 应该重试的次数,他的默认值为0

POST /website/pageviews/1/_update?retry_on_conflict=5 
{
   "script" : "ctx._source.views+=1",
   "upsert": {
       "views": 0
   }
}

失败之前重试该更新5次。

在增量操作无关顺序的场景,例如递增计数器等这个方法十分有效,但是在其他情况下变更的顺序是非常重要的。类似 index API , update API默认采用最终写入生效的方案,但他也接受一个version 参数来允许你使用 optimistic concurrency control 指定想要更新文档的版本。