本文主要内容:
1、脚本更新文档
   (1).更新常规字段值
   (2).更新数组类型字段值
   (3).通过脚本进行一定判断,再执行更新操作
2、传递部分文档形式更新文档,可以理解成追加文档
   (1).检查noop更新
3、upsert更新,即存在更新,不存在另行其他操作
4、更新操作支持以下查询字符串参数   
5、Java JestClient 更新示例

前言:

在 Elasticsearch 中 文档是不可改变 的,不能修改它们。 相反,如果想要更新现有的文档,需要重建索引或者进行替换, 我们可以使用相同的 index API 进行实现

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 
}

在内部,Elasticsearch已将旧文档标记为已删除,并增加一个全新的文档。 尽管你不能再对旧版本的文档进行访问,但它并不会立即消失。当继续索引更多的数据,Elasticsearch会在后台清理这些已删除文档。

在本章的后面部分,我们会介绍update API,虽然它似乎对文档直接进行了修改,但实际上Elasticsearch按前述完全相同方式执行以下过程:

  • 从旧文档构建JSON
  • 更改该JSON
  • 删除旧文档
  • 索引一个新文档

唯一的区别在于, update API 仅仅通过一个客户端请求来实现这些步骤,而不需要单独的 get 和 index 请求。

脚本更新文档

更新API允许根据提供的脚本更新文档。 该操作从索引获取文档(与碎片并置),运行脚本(使用可选的脚本语言和参数),并索引结果(也允许删除或忽略操作)。 它使用版本控制来确保在“get”“reindex”期间没有更新发生。
请注意,此操作仍然意味着文档的完全重新索引,它只是删除了一些网络往返并减少了获取和索引之间版本冲突的可能性。 需要启用_source字段才能使此功能起作用。

让我们跟着文档来试验一下:

PUT test/type1/1
{
    "counter" : 1,
    "tags" : ["red"]
}

执行脚本一:(如果是批量修改,则改为POST test/type1/_update_by_query)

POST test/type1/1/_update
{
    "script" : {
        "source": "ctx._source.counter += params.count",
        "lang": "painless",
        "params" : {
            "count" : 4
        }
    }
}

查看一下数据:

{
  "_index": "test",
  "_type": "type1",
  "_id": "1",
  "_version": 3,
  "found": true,
  "_source": {
    "counter": 5,
    "tags": [
      "red"
    ]
  }
}

执行脚本二:

POST test/type1/1/_update
{
    "script" : {
        "source": "ctx._source.tags.add(params.tag)",
        "lang": "painless",
        "params" : {
            "tag" : "blue"
        }
    }
}

查看一下数据:

GET /test/type1/1
{
  "_index": "test",
  "_type": "type1",
  "_id": "1",
  "_version": 2,
  "found": true,
  "_source": {
    "counter": 1,
    "tags": [
      "red",
      "blue"
    ]
  }
}

看到这里,我们可以发现:
通过脚本一,我们对counter字段进行了自增。
通过脚本二,我们对tags集合添加了元素。

_source外,通过ctx映射还可以使用以下变量:_index,_type,_id,_version,_routing,_parent和_now(当前时间戳)。

我们还可以在文档中添加一个新字段:

POST test/type1/1/_update
{
    "script" : "ctx._source.new_field = 'value_of_new_field'"
}

查看一下数据:

{
  "_index": "test",
  "_type": "type1",
  "_id": "1",
  "_version": 4,
  "found": true,
  "_source": {
    "counter": 5,
    "tags": [
      "red",
      "blue"
    ],
    "new_field": "value_of_new_field"
  }
}

删除某个字段:

POST test/type1/1/_update
{
    "script" : "ctx._source.remove('new_field')"
}

查看一下数据:

{
  "_index": "test",
  "_type": "type1",
  "_id": "1",
  "_version": 5,
  "found": true,
  "_source": {
    "counter": 5,
    "tags": [
      "red",
      "blue"
    ]
  }
}

而且,我们甚至可以改变执行的操作: 如果标签字段包含蓝色,则此示例将删除该文档,否则它将不执行任何操作(noop)

POST test/type1/1/_update
{
    "script" : {
        "source": "if (ctx._source.tags.contains(params.tag)) { ctx.op = 'delete' } else { ctx.op = 'none' }",
        "lang": "painless",
        "params" : {
            "tag" : "blue"
        }
    }
}

查看一下数据,我们发现,该条数据已经被删除。

GET /test/type1/1
{
  "_index": "test",
  "_type": "type1",
  "_id": "1",
  "found": false
}

传递部分文档

更新API还支持传递部分文档,该文档将被合并到现有文档中(简单的递归合并,对象的内部合并,替换核心“键/值”和数组)。 要完全替换现有文档,应该使用索引API。(一定要注意区别)
以下部分更新为现有文档添加了一个新字段:

POST test/type1/1/_update
{
    "doc" : {
        "name" : "new_name"
    }
}

查看一下数据结果:

GET /test/type1/1
{
  "_index": "test",
  "_type": "type1",
  "_id": "1",
  "_version": 4,
  "found": true,
  "_source": {
    "counter": 5,
    "tags": [
      "red",
      "blue"
    ],
    "name": "new_name"
  }
}

注意:
如果指定了文档和脚本,则doc将被忽略。 最好的办法是将你的部分文档的字段对放入脚本本身。
即脚本内容可覆盖doc。

检测noop更新

如果指定doc,则其值与现有的_source合并。
默认情况下,不更改任何内容的更新会检测到它们不会更改任何内容,并返回“结果”:“noop”。
如下所示:

POST test/type1/1/_update
{
    "doc" : {
        "name" : "new_name"
    }
}

查看一下返回结果和数据:

{
  "_index": "test",
  "_type": "type1",
  "_id": "1",
  "_version": 4,
  "result": "noop",
  "_shards": {
    "total": 0,
    "successful": 0,
    "failed": 0
  }
}
{
  "_index": "test",
  "_type": "type1",
  "_id": "1",
  "_version": 4,
  "found": true,
  "_source": {
    "counter": 5,
    "tags": [
      "red",
      "blue"
    ],
    "name": "new_name"
  }
}

您可以通过设置"detect_noop":false来禁用此行为:

POST test/type1/1/_update
{
    "doc" : {
        "name" : "new_name"
    },
    "detect_noop": false
}

查看一下更新后的数据

{
  "_index": "test",
  "_type": "type1",
  "_id": "1",
  "_version": 5,
  "found": true,
  "_source": {
    "counter": 5,
    "tags": [
      "red",
      "blue"
    ],
    "name": "new_name"
  }
}

这种检测noop更新,个人觉得好像可以防止无用的更新。

upsert
如果文档尚不存在,则会将upsert元素的内容作为新文档插入。 如果文档确实存在,那么脚本将被执行:

POST test/type1/1/_update
{
    "script" : {
        "source": "ctx._source.counter += params.count",
        "lang": "painless",
        "params" : {
            "count" : 4
        }
    },
    "upsert" : {
        "counter" : 1
    }
}

我们查看一下数据。

{
  "_index": "test",
  "_type": "type1",
  "_id": "1",
  "_version": 6,
  "found": true,
  "_source": {
    "counter": 9,
    "tags": [
      "red",
      "blue"
    ],
    "name": "new_name"
  }
}

我们可以试一下_id=2更新一下,如果没预料错的话,会新建一个文档,我们查看一下_id=2的数据:

{
  "_index": "test",
  "_type": "type1",
  "_id": "2",
  "_version": 1,
  "found": true,
  "_source": {
    "counter": 1
  }
}

如果您希望脚本在不管文档是否存在的情况下运行 - 即脚本处理初始化文档而不是upsert元素 - 则将scripted_upsert设置为true

POST sessions/session/dh3sgudg8gsrgl/_update
{
    "scripted_upsert":true,
    "script" : {
        "id": "my_web_session_summariser",
        "params" : {
            "pageViewEvent" : {
                "url":"foo.com/bar",
                "response":404,
                "time":"2014-01-01 12:32"
            }
        }
    },
    "upsert" : {}
}

我们查看kibana执行结果,显然执行了脚本,报错是由于脚本错误。

{
  "error": {
    "root_cause": [
      {
        "type": "remote_transport_exception",
        "reason": "[CNHQ-17070696T][127.0.0.1:9300][indices:data/write/update[s]]"
      }
    ],
    "type": "illegal_argument_exception",
    "reason": "failed to execute script",
    "caused_by": {
      "type": "resource_not_found_exception",
      "reason": "unable to find script [my_web_session_summariser] in cluster state"
    }
  },
  "status": 400
}

当我们去掉"scripted_upsert":true再执行一次,发现我们执行了upsert,创建了新索引、类型…
这里写图片描述

doc_as_upsert设置为true,将不会发送部分文档和upsert文档,而是将doc的内容用作upsert值:

POST test/type1/1/_update
{
    "doc" : {
        "name" : "new_name"
    },
    "doc_as_upsert" : true
}

更新操作支持以下查询字符串参数

retry_on_conflict
在更新的获取和索引阶段之间,可能有另一个进程可能已经更新了同一个文档。默认情况下,更新将失败并出现版本冲突异常。该retry_on_conflict 参数控制最终抛出异常之前重试更新的次数。

routing
如果正在更新的文档不存在,路由用于将更新请求路由到右分片并设置upsert请求的路由。不能用于更新现有文档的路由。

parent
Parent用于将更新请求路由到右分片,并在正在更新的文档不存在时为upsert请求设置父项。不能用于更新parent现有文档。如果指定了别名索引路由,则它会覆盖父路由,并将其用于路由请求。

timeout
超时等待碎片变为可用。

wait_for_active_shards
在继续更新操作之前,需要激活的分片副本的数量。

refresh
控制此请求所做的更改对搜索是否可见。

_source
允许控制是否以及如何在响应中返回更新的源。默认情况下,更新的源不会被返回。

version & version_type
更新API在内部使用Elasticsearch的版本控制支持来确保文档在更新期间不会更改。您可以使用该version 参数来指定仅当文档的版本与指定的版本匹配时才更新文档。通过设置版本类型,force您可以在更新后强制更新文档的新版本(小心使用!force 并不保证文档没有更改)。

到这里,我们发现,ES的索引约束好像很松,我们刚才可以随意修改字段的值,更恐怖的是随意的为索引类型增加字段、删除字段。其实strict属性是可以约束我们的。这个等后面再聊。

Java JestClient 更新示例

这里只写一个demo,详细可以去JestClientgithub上查看单元测试,个人觉得这是学习JestClient API比较合适的方式。

git地址:
https://github.com/searchbox-io/Jest/blob/master/jest/src/test/java/io/searchbox/core/UpdateIntegrationTest.java

//通过传递部分文档覆盖更新
String script = "{\n" +
                "\"doc\":{\n" +
                "  \"id\":\"6fb8934f829e41a0849f153c1a38a1c1\",\n" +
                "  \"service\":\"lalala\"\n" +
                "}\n" +
                "}";
DocumentResult documentResult =  jestService.client.execute(new Update.Builder(script).index("sfeicuss")
                .id("6fb8934f829e41a0849f153c1a38a1c1").type("servicelog").build());
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐