9. 数据复制模型

 单处理和批处理操作主要围绕数据复制模型来理解,具体接口如下:

  • 单文档处理接口:
  • Index接口;
  • Get接口;
  • Delete接口;
  • Update接口;
  • 多文档处理接口:
  • Multi Get接口;
  • 批处理接口;
  • Delete By Query API;
  • Update By Query API;
  • Reindex API;

 每个ES索引都被分割为碎片shards,每个碎片又有多个副本(它们组成一个副本群,在文档添加和移除时保持同步),如果不这么干,从不同副本中读到的结果将会有差异,这种保持碎片同步并且从碎片中读取数据的服务就是数据复制模型,ES的数据复制模型是建立在主备份模型上,该模型基于具有来自复制组的单个副本,该副本充当主碎片(primary shard),其他的副本叫副本碎片(replica shards),主碎片充当所有索引操作的入口点,负责验证并确保这些操作是正确的,一旦一个索引操作被主碎片接受,他还负责将该操作复制到其副本上。

基本写模式

 Elasticsearch中的每个索引操作通常基于文档ID首先使用路由解析为复制组,一旦复制组定下来了,那么将这个索引操作将会在复制组内部转发到这个复制组中的主碎片,然后主碎片验证并将该操作复制给组内其他的副本碎片。由于副本碎片可以是离线的状态,通常主碎片没有必要将操作复制到所有的副本碎片上,它只保证一系列的碎片可以接受到上述的操作行为,这些“一系列碎片”就是同步副本(由主节点负责维护),这些是“好的碎片副本”的集合,保证了所有的索引操作和删除操作都已经向用户确认过了。主碎片保持不变并将所有的操作复制给组内的每个副本碎片,主碎片的基本流程如下:

  1. 验证传入的操作,若结构无效则拒绝传入;
  2. 执行本地操作(如索引和删除相关文档document),这一步也会验证字段内容,如果有必要也将拒绝该操作;
  3. 在当前同步副本集合内传达该操作,如果有多个副本,那将会并行传达;
  4. 一旦所有的副本成功执行并给主碎片反馈了执行结果,主碎片向客户端确认请求已成功操作。

基本读模式

 Elasticsearch中的读取可以是通过ID这样的轻量级查找,也可以是具有大量复杂聚合的搜索请求,这些聚合会占用大量CPU资源。主备份模型优点之一就是使得所有碎片相同,因此,单个同步副本可以高效的处理读请求。

 当都请求被一个节点接受,那该节点就负责将这个请求转发到相关碎片的节点上并整理执行结果将其回复给客户端,基本流程如下:

  1. 将读取请求解析到相关碎片上,注意由于大部分的搜索将会被发送到一个或多个索引上,它们需要从多个碎片中读取,每个读取结果都代表了数据的不同子集;
  2. 从碎片复制组中的每个相关碎片中选择一个活动状态的碎片副本,可以是主碎片或副本碎片,默认情况下,Elasticsearch将简单地在碎片副本之间循环这些行为;
  3. 将碎片级读取请求发送到所选副本;
  4. 组装结果进行回复,注意如果仅仅是get by id这样仅有一个相关碎片的查询,那这一步可以省略;

【故障】

昨天ES在升级证书后出现问题,且今天启动Kibana时出现请求超时的异常,做如下处理:

  1. 加大ES内存,由1G增加至2G:
# 修改配置文件jvm.options
-Xms2g
-Xmx2g
  1. 加大Kibana请求的超时时间到60s:
# 修改kibana.yml文件
elasticsearch.requestTimeout: 60000

9.1 单处理接口

9.1.1 Index接口

 创建索引以及参数含义之前章节一致,ES具备自动创建索引的功能,主要流程是一个索引操作,但是这个索引之前并没有创建,如果尚未创建特定类型,还会自动为特定类型创建动态类型映射,映射本身非常灵活,无需架构。新字段和对象将自动添加到指定类型的映射定义中。自动索引创建可以通过设置action.auto_create_index参数为false来取消这种自动创建的动作,自动映射创建同样可以通过将index.mapper.dynamic参数设置为false来取消。另外,自动索引创建的设置可以包含白名单/黑名单,比如action.auto_create_index设置为+aaa*,-bbb*,+ccc*,-*(+表示允许,-表示不允许)。

 创建文档的命令为:

PUT twitter/_doc/1
{
    "user" : "kimchy",
    "post_date" : "2009-11-15T14:12:12",
    "message" : "trying out Elasticsearch"
}

返回的结果为:

{
    "_index": "twitter",
    "_type": "_doc",
    "_id": "1",
    "_version": 2,
    "result": "updated",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 1,
    "_primary_term": 1
}

其中

  • _shards:提供了有关索引复制过程的信息;
  • total:指示应在其上执行索引操作的分片副本(主分片和副本分片)的数量;
  • successful:指示索引操作成功的分片副本数;
  • failed:在副本分片上索引操作失败的情况下包含复制相关错误的数组。

如果索引操作成功,那successful至少为1。

【注意】

当一个索引操作成功返回时副本碎片都可能还没有开始(默认情况下,只有主碎片时必须的,但是这种机制可以更改)。

另外每个索引都有一个版本号,相关的version都作为索引请求返回值的一部分,索引API可选地允许在指定version参数时进行乐观并发控制。这种针对文档操作的版本控制就是为了标记被执行,版本控制一个很好的用例就是是执行事务性读取然后更新(read-then-update)。从最初读取的文档中指定版本可确保在此期间未发生任何更改(当为了更新先进行阅读时,建议将preference设置为_primary)。下面就是一个版本控制的小例子:

PUT twitter/_doc/1?version=2
{
    "message" : "elasticsearch now has versioning support, double cool!"
}

PUT twitter/_doc/1?version=2
{
    "message" : "elasticsearch now has versioning support, double cool!"
}

上述操作先去twitter索引中寻找_doc类型且_id为1的文档,过程并不是简单的去找就完事儿了,还要进行版本匹配,只有当找到的文档version参数为2才能再进行更新,如果version参数不是指定的2,ES就会出现version_conflict_engine_exception。当使用外部版本控制时,就不是进行指定的版本号匹配了,而是检查传递给索引请求的版本号是否大于当前存储文档的版本号,如果版本号大于当前文档的版本号,则将索引文档并使用新版本号,如果提供的版本号小于等于当前文档的版本号,那就会发生版本冲突,索引操作失败。

【注意】

版本控制是完全实时的,并且不受近实时的搜索操作影响,但是如果未提供版本,则执行该操作而不进行任何版本检查。默认情况下,version参数都是从1开始,每次操作(包括删除操作)递增1,具体的version可以不使用这种方式,比如可以使用外部数据库的维护的值(),如果想这么搞,就要将version_type设置为external,另外version值必须为数字且大于等于0,小于等于9.2e+18,虽然使用外部版本号可以从0开始,但是version=0的文档既不能更新也不能删除(这是一个副作用)。使用外部版本号,一个好的副作用是,只要使用数据库中的版本号,就不需要维护由于数据库更改而执行的异步索引操作的严格排序。如果使用外部版本控制,即使使用数据库中的数据更新Elasticsearch索引的简单情况也会简化,因为如果索引操作由于某种原因而无序,则仅使用最新版本。

 除了上述的内部版本号和外部版本(即internalexternal),版本号还有其他的类型:

  • internal:如果给定版本与存储文档的版本相同,则仅索引文档,即第一个例子;
  • externalexternal_gt:只有给出的版本号大于当前文档的版本号或这个文档根本不存在才会进行索引(前提是提供的版本号是一个合法的数值),如果文档不存在就使用提供的版本号创建一个文档。
  • external_gte:只有给定的版本号大于或等于当前文档的版本号才会去索引文档,同样在给定版本号是合法数字的情况下,如果文档不存在,就会使用给定的版本号创建一个新的文档(这个类型的版本慎用,如果用不好会导致数据丢失);

操作类型

 索引操作也接受op_type参数,它可以用于强制执行一个create行为,允许如果数据不存在就创建,当使用create时,如果指定id的文档已经存在了,那么索引操作就会失败,出现版本冲突,下面是一个例子:

PUT twitter/_doc/1?op_type=create
{
    "user" : "kimchy",
    "post_date" : "2009-11-15T14:12:12",
    "message" : "trying out Elasticsearch"
}

// 也可以这样使用
PUT twitter/_doc/1/_create
{
    "user" : "kimchy",
    "post_date" : "2009-11-15T14:12:12",
    "message" : "trying out Elasticsearch"
}

自动生成ID

 如果索引操作没有指定ID,那id将会自动生成,此外,op_type将会自动设置为create,比如下面的例子(注意使用的是POST方法而不是PUT):

POST twitter/_doc/
{
    "user" : "kimchy",
    "post_date" : "2009-11-15T14:12:12",
    "message" : "trying out Elasticsearch"
}

路由

 默认碎片位置(或routing)是由文档ID的哈希值控制的。为了更明确的控制,可以使用路由参数routing在每个操作的基础上直接指定输入到路由器使用的散列函数的值,比如:

POST twitter/_doc?routing=kimchy
{
    "user" : "kimchy",
    "post_date" : "2009-11-15T14:12:12",
    "message" : "trying out Elasticsearch"
}

在上述的例子中,_doc文档是通过routing参数提供的kimchy值路由到一个碎片,在设置显式映射时,可以选择使用_routing字段来指示索引操作从文档本身中提取路由值,这确实是自另一个文档解析过程的(非常小的)成本,如果定义了_routing映射并将其设置为required,则如果未提供或提取路由值,则索引操作将失败。

等待活动的碎片

 为了提高写入系统的弹性,索引操作可以在执行前等待一定数量的活跃副本碎片,如果必要的存活副本碎片没有达到指定的数量,写入操作就必须等待重试,直到有必要的副本碎片或者超时了,默认情况下,写入操作只等待主碎片就绪就执行写入了(配置为wait_for_active_shards=1),这配置在索引配置中可以用index.write.wait_for_active_shards进行动态配置,为了更改每个操作的这种行为,可以使用wait_for_active_shards请求参数。有效值是all或者任何正整数,最大为索引中每个分片的已配置副本总数(即number_of_replicas+1),如果指定一个负数或者大于碎片分本的数目将会抛出一个异常。

 比如一个集群中有3个节点A、B、C,索引副本设置为3(产生4个分片副本,比节点多一个副本),如果进行索引操作,默认情况只需要保证每个碎片的主副本碎片在执行前是就绪的就行了,就算B和C挂了,而节点A托管了主碎片副本,那么索引操作仍将仅使用一个数据副本。但是如果wait_for_active_shards参数在请求设置为3(3个节点必须全部启动就绪),那在执行索引操作前必须有3个就绪的碎片副本,应该满足的要求,因为群集中有3个活动节点,每个活动节点都有一个碎片副本,这个集群中3个就绪的节点刚好满足要求,每个节点托管着一个碎片的副本,然而,如果我们将wait_for_active_shards设置为all(即4),那索引操作将不会执行,因为索引的每个碎片没有4个副本,此时这个操作只能等待超时或者集群启动一个新的节点来托管第4个碎片副本。

 注意,上述的设置大幅度的降低了写入操作(不写入所需数量的分片副本)的可能性,但它并未完全消除这种可能性,因为此检查在写入操作开始之前发生。一旦写入操作正在进行了,复制在任何数量的碎片副本上仍然可能失败,但仍然可以在主碎片上成功。_shards参数是写入操作的响应结果,表现的就是副本碎片的成功和碎片的数量。

空更新(Noop Update)

 当使用索引API更新一个文档时,一个新版本的文档总是会被创建,即使这个文档并没改变,如果不想发生这样的行为,可以使用_update接口,并将detect_noop设置为true。索引api上没有此选项,因为索引api不会获取旧源,也无法将其与新源进行比较。关于何时不接受空更新,没有一条硬性规定,它是由许多因素决定的,例如数据源发送实际noops更新的频率以及Elasticsearch在接收更新时在分片上运行的每秒查询数。

关于超时

 执行索引操作时,被分配执行索引操作的主碎片可能存在暂时不可获取的状态,各种原因都有,可能是主分片当前正在从网关恢复或正在进行重定位。默认情况下,索引操作在返回错误前将等待主碎片1分钟,如果1分钟还没等到主碎片那就返回错误,timeout参数可以用来明确的指定索引操作需要等待多长时间,下面是索引操作时指定超时时间的小栗子:

PUT twitter/_doc/1?timeout=5m
{
    "user" : "kimchy",
    "post_date" : "2009-11-15T14:12:12",
    "message" : "trying out Elasticsearch"
}
9.1.2 Get接口

get请求方式允许通过id获取一个JSON类型的文档,比如下面的请求的结果:

请求

GET twitter/_doc/0

结果

{
    "_index": "twitter",
    "_type": "_doc",
    "_id": "1",
    "_version": 4,
    "found": true,
    "_source": {
        "user": "kimchy",
        "post_date": "2009-11-15T14:12:12",
        "message": "elasticsearch now has versioning support, double cool!3"
    }
}

上面的命令直接是获取了结果,但在执行命令前它并不知道这个文档到底是不是存在,如果有这种需求可以使用HEAD请求方式:

HEAD twitter/_doc/0

这就不是返回具体的文档结果了,如果文档存在,就返回200状态码,如果文档不存就返回404。

实时

 默认情况下,GET方法是实时的,并不会受到索引的刷新率影响(当数据对搜索可见时)。如果一个文档已经被更新但还没有被刷新,GETAPI就可以发出刷新调用使得刚更新的文档可见,这也将使上次刷新后其他文档发生变化,如果不想让GETAPI实时更新,可以将realtime参数设成false

源过滤

 默认情况下,get操作返回一个_source域的文本,除非已经使用了stored_fields参数或者_source本身就是不可得的,我们可以通过_source参数关闭_source的显示,比如:

GET twitter/_doc/0?_source=false

此时返回的结果就是:

{
    "_index": "twitter",
    "_type": "_doc",
    "_id": "1",
    "_version": 4,
    "found": true
}

如果只需要_source中的几个属性,那可以使用_source_include或者_source_exclude参数来包含或者过滤掉所需要的属性,这对于大型文档尤其有用,其中部分检索可以节省网络开销,这两个参数都使用逗号分隔的字段列表或通配符表达式,如:

GET twitter/_doc/0?_source_include=*.id&_source_exclude=entities

如果只想指定特殊的包含,那可以使用更短的符号:

GET twitter/_doc/0?_source=*.id,retweeted

存储的字段(这一块不是很明白)

 get操作允许指定一系列的存储的字段,这些字段可以通过stored_fields参数返回,如果请求的字段没有在存储的文档中,那这个请求的字段将会被忽略,如:

PUT fields
{
   "mappings": {
      "_doc": {
         "properties": {
            "counter": {
               "type": "integer",
               "store": false
            },
            "tags": {
               "type": "keyword",
               "store": true
            }
         }
      }
   }
}

上述的命令就建立了一个fields索引,properties规定了fields这个索引中应该存放哪些字段,上面定义2个属性counter(类型为整数)和tags(类型为关键词),返回的结果为:

{
    "acknowledged": true,
    "shards_acknowledged": true,
    "index": "fields"
}

然后存一个文档:

PUT fields/_doc/1
{
    "counter" : 1,
    "tags" : ["red"]
}

最后获取这个文档:

// 执行的命令
GET twitter/_doc/1?stored_fields=tags,counter

// 返回的结果
{
    "_index": "fields",
    "_type": "_doc",
    "_id": "1",
    "_version": 1,
    "found": true,
    "_source": {
        "counter": 1,
        "tags": [
            "red"
        ]
    }
}

注意,从文档本身获取的字段值始终作为数组返回,因为counter字段没有存储到get请求中(这里高不太明白,存的时候明明是存的,为什么又或说没有存储到get请求中),当获取stored_fields时这个字段将会被忽略。当然还可以使用_routing来获取元数据,如:

// 1.存数据
PUT fields/_doc/2?routing=user1
{
    "counter" : 1,
    "tags" : ["white"]
}

// 2.取数据
GET fields/_doc/2?routing=user1&stored_fields=tags,counter

// 2.2 返回的结果
{
   "_index": "fields",
   "_type": "_doc",
   "_id": "2",
   "_version": 1,
   "_routing": "user1",
   "found": true,
   "fields": {
      "tags": [
         "white"
      ]
   }
}

此外,只能通过stored_field选项返回叶子字段。因此无法返回对象字段,此类请求将失败。

直接获取_source

 使用/{index}/{type}/{id}/_source端点可以只获取文档的_source字段,不会有其他额外的字段,如:

GET twitter/_doc/1/_source

同样的效果可以用以下的命令达到效果:

GET twitter/_doc/1/_source?_source_include=*.id&_source_exclude=entities'

注意,这里同样有一个HEAD的请求方式,如果在映射中禁用了现有文档,则该文档将没有_source,如:

HEAD twitter/_doc/1/_source

路由

 当索引使用这种能力去控制路由,为了去获取一个文档,路由值应该提供,比如:

GET twitter/_doc/2?routing=user1

获取id为2的,但是是基本user进行路由的,如果路由无效,文档是无法获取的;

优先级

 控制哪个碎片作为执行get请求的首选项。默认情况下,操作在碎片是在碎片副本之间随机的。也可以通过preference参数进行指定,可以指定的值有:

  • _primary:操作行为将会发送给主碎片并由主碎片执行;
  • _local:如果可能,操作将优先在本地分配的分片上执行。

刷新

 当refresh这个参数设置为true时,可以在get操作之前刷新相关碎片使这些碎片可以被搜索,如果要将他设置为true,需要仔细考量,是否会导致系统超负荷。

分配

 get操作被散列为特定的碎片ID,然后将get操作重定向到那个碎片的某个副本碎片,最后返回结果,碎片副本是主碎片及其在该碎片ID组中的副本。这意味着我们将拥有的副本越多,我们将获得更好的GET缩放。

版本支持

version参数用于获取文档时匹配的参数之一,除了始终检索文档的版本类型FORCE之外,所有版本类型的此行为都是相同的。但是force已经弃用,在内部,Elasticsearch已将旧文档标记为已删除并添加了一个全新的文档。旧版本的文档不会立即消失,但将无法访问它,当继续索引更多数据时,Elasticsearch会在后台清除已删除的文档。

9.1.3 Delete接口

删除

删除索引twitter_doc类型的、_id为1的文档,命令就是:

DELETE /twitter/_doc/1

版本

ES中每个索引下的文档都是版本化的,当删除一个文档时,version参数可以指定某个特定的版本以确保这个文档确实是我们要删除的文档并且没有发生过改动,对文档的每一次写入操作(包括删除操作)都会使该文档的版本号递增,在删除一个文档时,短时间内版本号还是可获取的,这个短时间的范围具体是多长是由index.gc_deletes参数设置额(默认是60秒)。

路由

 当索引使用路由进行控制时,为了删除一个文档,需要提供一个路由值,如:

DELETE /twitter/_doc/1?routing=kimchy

路由一定要正确,当_routing设置为required时,如果没有指定路由值,删除接口将抛出RoutingMissingException异常并拒绝接受请求。

自动索引创建

 如果使用了外部索引,删除操作将自动创建(如果之前没有创建过这个索引),并且自动一个动态的类型映射(如果之前没有)。

分配

 同样的,删除操作将散列为特定的碎片的ID,然后重定向到碎片ID所在主碎片,复制到ID组中的碎片副本。

等待活跃碎片

 发送删除请求时,可以设置wait_for_active_shards参数指定在执行删除操作前所需要的最小碎片副本数。

超时

 当删除操作被执行的时候,主碎片被分派去执行删除操作时可能不能获取,造成这种情况的一些原因可能是主碎片当前正在从数据库恢复或正在进行重定位。默认情况下,删除操作将在主碎片上等待最多1分钟,然后失败并响应错误,下面时设置超时时间为5分钟:

DELETE /twitter/_doc/1?timeout=5m
9.1.4 _delete_by_query接口

通过_delete_by_query接口删除文档,可以删除所有匹配到的文档,比如:

POST twitter/_delete_by_query
{
  "query": {
    "match": {
      "message": "some message"
    }
  }
}

查询条件必须作为值传递给query,这一点是和Search API是一致的,同样可以使用q参数。当_delete_by_query启动并使用内部版本控制(internal)删除它找到的内容,它获取的是索引的一个快照,就意味着如果文档在快照被获取和删除请求执行之间的这段时间内发生更新,就会出现版本冲突,只有版本匹配的时候文档才会被删除。注意内部版本控制是不允许版本号为0的,所以在版本为0时,会直接请求失败。

 在_delete_by_query执行期间,为了寻找到所有匹配的待删除文档,多个搜索请求是按序执行的,每次发现一批文档,相应的批请求也会执行去删除所有发现的这些文档,如果搜索或者批请求被ES拒绝,_delete_by_query根据默认的策略模式将会重试失败的请求(最多尝试10次,尝试的时间指数增长),达到最大的尝试次数限制后_delete_by_query将会终止,所有的失败都会在响应中的failures中返回,但是已经执行的删除操作仍然有效,从事务的角度来讲就是这个执行过程并不是一个事物,它只会终止,执行的那部分就执行成功了,执行失败的部分不会导致已经执行成功失效,即这个过程不会回滚,当第一个故障导致中止时,失败的批量请求返回的所有故障都将在failure元素中返回,因此,可能存在相当多的失败实体,这个过程如果想统计版本冲突的次数而不是返回原因,可以在在URL中设置conflicts=proceed或者在请求体中带上"conflicts": "proceed",API的例子如下:

POST twitter/_doc/_delete_by_query?conflicts=proceed
{
  "query": {
    "match_all": {}
  }
}

也提供了一次删除多索引,如下:

POST twitter,blog/_docs,post/_delete_by_query
{
  "query": {
    "match_all": {}
  }
}

如果提供了routing值,则路由将复制到滚动查询,将进程限制为与路由值匹配的碎片,如:

POST twitter/_delete_by_query?routing=1
{
  "query": {
    "range" : {
        "age" : {
           "gte" : 10
        }
    }
  }
}

默认情况下,_delete_by_query使用1000的滚动批次,在URL中可以使用scroll_size参数改变批处理的数量,如:

POST twitter/_delete_by_query?scroll_size=5000
{
  "query": {
    "term": {
      "user": "kimchy"
    }
  }
}

URL参数

 除了像preety这样的参数,查询删除接口也支持refreshwait_for_completionwait_for_active_shardstimeout以及scroll参数,下面是具体说明:

  • refresh:发送refresh参数将会在请求完成时刷新所有和这次删除操作相关的碎片,这一点和删除接口不同,删除接口中的refresh参数只刷新收到删除请求的碎片。
  • wait_for_completion=false:如果这个参数设置为false,ES将会进行预检测、发起请求,然后返回可以和TASK接口一起使用的task来取消或者获取任务的状态,ES将会在.tasks/task/${taskId}为这个任务创建一个记录作为文档,保留或删除你认为合适的,当完成后就删除它以便ES可以回收这些空间;
  • wait_for_active_shards:和之前一样,执行请求前需要就绪的碎片数,timeout用于控制写入操作最多等上述碎片就绪的时间,由于_delete_by_query使用的是滚动搜索,可以指定scroll参数来控制“搜索上下文”的存活时间,默认是5分钟(改为10分钟就是scroll=10m);
  • requests_per_second:可以设置为任意正十进制数(如1.4、6)和限制速率(_delete_by_query通过这个参数为发出批量删除的操作填充一个等待时间),第二个参数的限制也可以禁用(将requests_per_second设置为-1);限制是通过批次之间的等待来完成的,以便_delete_by_query在内部使用的滚动可以被赋予一个考虑填充的超时,这个填充的时间是批量的大小除以requests_per_second(这个除了之后的结果)和写入时间的差值,默认批操作的大小为1000,如果将requests_per_second设置为500,计算如下:
target_time = 1000 / 500 per second = 2 seconds
wait_time = target_time - write_time = 2 seconds - .5 seconds = 1.5 seconds

由于批处理是作为单个_bulk请求发出的,因此大批量大小将导致Elasticsearch创建许多请求,然后等待一段时间再开始下一组,使用突发代替了光滑,默认为-1.

响应体

 json的响应类似于:

{
  "took" : 147,
  "timed_out": false,
  "total": 119,
  "deleted": 119,
  "batches": 1,
  "version_conflicts": 0,
  "noops": 0,
  "retries": {
    "bulk": 0,
    "search": 0
  },
  "throttled_millis": 0,
  "requests_per_second": -1.0,
  "throttled_until_millis": 0,
  "failures" : [ ]
}

各参数的含义如下:

  • took:整个操作花费的时间;
  • time_out:如果在查询执行删除期间执行的任何请求超时,则此标志就为true
  • total:成功处理的文档数量;
  • deleted:成功删除的文档数量;
  • batches:通过查询删除拉回的滚动响应数。
  • version_conflicts:按查询删除的版本冲突数;
  • noop:这个字段对于查询删除通常是0,它只存在于查询的过程中,以便通过查询删除,按查询更新和重新索引接口返回具有相同结构的响应。
  • retries:删除查询的重试次数,bulk是批处理的重试次数,search是搜索行为重试的次数;
  • throttled_millis:每个请求根据requests_per_second参数睡眠的时间;
  • requests_per_second:在查询删除期间每秒有效请求的数量;
  • throttled_until_millis:在查询删除中这个字段总是为0,只有在使用TASK接口时这个参数才有意义,其中它指示下一次(单位为毫秒),将再次执行受限制的请求以符合requests_per_second
  • failures:在操作过程中不可恢复的错误数组,和上面所说的一样,导致操作终止的原因。

TASK接口

 使用TASK接口可以拉取任何正在运行的查询删除的请求,如:

GET _tasks?detailed=true&actions=*/delete/byquery

结果为:

{
  "nodes" : {
    "r1A2WoRbTwKZ516z6NEs5A" : {
      "name" : "r1A2WoR",
      "transport_address" : "127.0.0.1:9300",
      "host" : "127.0.0.1",
      "ip" : "127.0.0.1:9300",
      "attributes" : {
        "testattr" : "test",
        "portsfile" : "true"
      },
      "tasks" : {
        "r1A2WoRbTwKZ516z6NEs5A:36619" : {
          "node" : "r1A2WoRbTwKZ516z6NEs5A",
          "id" : 36619,
          "type" : "transport",
          "action" : "indices:data/write/delete/byquery",
          "status" : {    
            "total" : 6154,
            "updated" : 0,
            "created" : 0,
            "deleted" : 3500,
            "batches" : 36,
            "version_conflicts" : 0,
            "noops" : 0,
            "retries": 0,
            "throttled_millis": 0
          },
          "description" : ""
        }
      }
    }
  }
}

使用任务的id可以直接查找任务,比如:

GET /_tasks/task_id

TASK接口的优势是和wait_for_completion=false整合可以清晰的返回完成任务的状态,如果任务完成且设置了wait_for_completion=false那么TASK接口将返回results或者error字段,这一个特性的成本就是wait_for_completion=false.tasks/task/${taskId}路径下创建的文档,这个文档是可以删除的。

Cancel Task接口

 任何查询删除操作可以使用Task Cancel接口取消,如:

POST _tasks/task_id/_cancel

其中task_id可以通过上面的TASK接口获取到,取消的操作可以快速执行但也可能需要几秒钟,上述的任务状态接口会继续列出任务直到它被唤醒来取消自身。

函数节流(rethrottle)

requests_per_second的值可以在查询删除执行期间使用_rethrottle接口改变,比如:

POST _delete_by_query/task_id/_rethrottle?requests_per_second=-1

task_id同上获取,和_delete_by_query接口一样,可以通过设置为-1禁用,也可以设置为任意正整数,加速查询的Rethrottling会立即生效,但是在完成当前批处理后,重新启动会降低查询速度,这样可以防止滚动超时。

切片(Slicing)

查询删除支持切片滚动以并行执行删除操作,这种并行方式提高效率,并且可以提供了将请求分隔为更小部分的方便方式,主要有以下的方式:

1.手动切片

将一个查询删除的进行切片,通过为每个请求提供切片ID和切片总数,如:

POST twitter/_delete_by_query
{
  "slice": {
    "id": 0,
    "max": 2
  },
  "query": {
    "range": {
      "likes": {
        "lt": 10
      }
    }
  }
}
POST twitter/_delete_by_query
{
  "slice": {
    "id": 1,
    "max": 2
  },
  "query": {
    "range": {
      "likes": {
        "lt": 10
      }
    }
  }
}

校验:

GET _refresh
POST twitter/_search?size=0&filter_path=hits.total
{
  "query": {
    "range": {
      "likes": {
        "lt": 10
      }
    }
  }
}

2.自动切片

使用“切片滚动”自动并行查询,以便在_uid上进行切片,使用slices指定使用的切片数量:

POST twitter/_delete_by_query?refresh&slices=5
{
  "query": {
    "range": {
      "likes": {
        "lt": 10
      }
    }
  }
}

验证:

POST twitter/_search?size=0&filter_path=hits.total
{
  "query": {
    "range": {
      "likes": {
        "lt": 10
      }
    }
  }
}

如果将slices设置为auto将由ES自动选择使用的切片数,这种设置是每个碎片使用一个切片,达到一定的限制,如果有多个源索引,它将会基于碎片的最小数量选择切片的数量。向_delete_by_query添加切片只会自动执行上一节中使用的手动切片过程,创建子请求有一些特殊要求:

  • 子请求都是具有切片请求任务的子任务;
  • 使用slices获取请求的任务状态仅包含已完成切片的状态;
  • 这些子请求可单独寻址,例如取消和重新限制;
  • 使用slices重新处理请求将按比例重新调整未完成的子请求;
  • 使用slices取消请求将取消每个子请求;
  • 由于slices的性质,每个子请求都不会获得完全均匀的文档部分。将解决所有文档,但某些切片可能比其他文件更大。期望更大的切片具有更均匀的分布;
  • slice_per_secondsize这样参数请求和带有slices请求按比例分配给每个子请求。结合上面关于分布不均匀的点,您应该得出结论,使用带有sizeslices可能不会导致确切size的文档为_delete_by_query
  • 每个子请求的快照都有些不同,尽管它们几乎同时从源索引派生出来。

挑选切片的数量

 如果选择slices设置为auto就不用操心了,ES将会为大部分索引选一个合理的数值,如果手动切片或以其他方式调整自动切片,参照下面的原则:当切片数等于索引中的碎片数时,查询性能最有效,如果该数字很大(例如500),则选择较小的数字,因为太多的切片会损害性能,设置高于碎片数量的切片通常不会提高效率反而会增加开销,删除性能在可用资源上以切片数量线性扩展。查询或删除性能是否主导运行时取决于重新编制索引的文档和群集资源。

9.1.5 Update接口

 更新的操作允许使用基于脚本的方式进行更新,更新操作从索引中获取文档,运行脚本索引返回结果,使用版本号确保在getreindex之间没有更新操作发生。这个操作仍然意味着文档的完全重新索引,它只是减少了网络的往返、减少了在getindex之间的版本冲突的几率,_source字段必须提供给更新操作才能进行更新。

 比如先放入一个文档:

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

然后利用脚本进行更新,下面是增加counter字段的值为5,如下:

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

上述的脚本命令只是简单进行加和操作,当然还可以在文档中添加字段,比如向tags数组添加额外的元素blue(就算数组中已存在该元素还是会再添加一次),如下:

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

除了可以向已有数组字段中添加元素外,还可以通过脚本向文档中添加新的字段,比如:

// 向文档中添加一个新字段,字段名为new_field,字段的值为value_of_new_field
POST test/_doc/1/_update
{
    "script" : "ctx._source.new_field = 'value_of_new_field'"
}

除了可以添加字段,还可以将文档中已有的字段删除(以上面添加的new_field字段为例),如下:

// 移除文档中的new_field字段
POST test/_doc/1/_update
{
    "script" : "ctx._source.remove('new_field')"
}

除此之外,利用脚本语言还可以实现类似于if-else的条件更新操作,比如:

// 如果文档的tags数组中包含green这个元素就删除整个文档,否则啥事儿不干
POST test/_doc/1/_update
{
    "script" : {
        "source": "if (ctx._source.tags.contains(params.tag)) { ctx.op = 'delete' } else { ctx.op = 'none' }",
        "lang": "painless",
        "params" : {
            "tag" : "green"
        }
    }
}

上述的命令对文档的具体字段的操作都是通过_source这个字段的子属性去进行操作的,因为文档整体内容存储就是存储在这个字段上的,ctx中除了这个字段还有其他的字段,一般只有是get出来的字段都可以通过ctx获取(有点类似于java中的上下文了):

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

所以在ctx映射中还有_index_type_id_version_routing_now(当前时间戳)属性。

使用局部文档进行更新

 更新接口也支持通过局部文档进行更新,它将会被合并到已有的文档中(简单的递归合并,对象的内部合并,替换核心键值对和数组)。如果要完全替换掉对应的文档,那应该使用index接口,下面就是向文档中添加一个新的字段:

// 像文档中添加一个name字段,它的值为new_name
POST test/_doc/1/_update
{
    "doc" : {
        "name" : "new_name"
    }
}

// 如果要替换掉整个文档,就使用index接口,即将上面的URL最后的_update字段去除即可

上述更新文档的方式有两种,一种是使用脚本语言,一种是只用局部文档,如果在一个请求中同时指定上述两种更新方式,ES将会执行脚本方式的命令,局部文档更新的方式将会被忽略掉。最好的方式是将部分文档的字段对放在脚本本身中。

空更新(noop)检测

 如果使用doc的方式指定特定的值和现有的_source进行合并,默认情况下,不会改变任何内容的更新操作将会检测到它们不会改变任何东西并返回"result": "noop",比如下面的:

// 之前test索引下_doc类型中id为1的文档已经存在下面的字段和字段值
POST test/_doc/1/_update
{
    "doc" : {
        "name" : "new_name"
    }
}

// 操作返回的结果
{
    "_index": "test",
    "_type": "_doc",
    "_id": "1",
    "_version": 4,
    "result": "noop",
    "_shards": {
        "total": 0,
        "successful": 0,
        "failed": 0
    }
}

像上面的,如果name字段是还是new_name,那么整个更新操作就会直接被忽略,响应中的result字段直接返返回noop(空操作),这种情况文档的版本号不变。当然也可以禁用这种“检测空操作”的行为(设置"detect_noop": false):

// 更新并禁用检测空行为,此时的更新操作不会被忽略,文档版本号自增1
POST test/_doc/1/_update
{
    "doc" : {
        "name" : "new_name"
    },
    "detect_noop": false
}

Upserts

 如果文档不存在,那么upsert中的元素将会以一个新文档的形式插入到ES中;如果文档已经存在,script部分将会被执行,比如:

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

脚本式的upsert

 如果希望不论文档是否存在都运行脚本(即脚本处理初始化文档而不是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" : {}
}

上述的命令中的param不太看得懂作用。

doc_as_upsert

 将doc_as_upsert设置为true将使用doc的内容作为upsert值,而不是发送部分doc加上upsert文档,比如:

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

参数

 更新操作支持如下的查询参数:

参数

含义

retry_on_conflict

在更新的getindexing阶段之间,另一个进程可能已经更新了同一文档,默认情况下这个更新操作将会发生版本冲突而失败,retry_on_conflict参数用于控制在最终异常抛出来之前可以重试多少次。

routing

路由用于将更新请求路由到正确的碎片上;如果带更新的文档不存在,它设置upsert请求的路由。

timeout

等待节点就绪的时间。

wait_for_active_shards

更新请求发出时必须等待多少碎片副本就绪。

refresh

控制何时更新请求所做的更改对搜索可见。

_source

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

version

更新API使用Elasticsearch的内部版本控制支持,以确保在更新期间文档不会更改。可以使用version参数指定仅在文档版本与指定版本匹配时才更新文档。

9.1.6 Update By Query接口

 这个接口和delete by query接口类似,即先查询再更新,update by query接口最简单的用法就是对索引中的每个文档进行更新而不改变源(不太懂),这对于获取新属性或其他一些在线映射更改很有用,下面是是一个案例:

// 操作
POST twitter/_update_by_query?conflicts=proceed

// 返回的结果
{
  "took": 271,
  "timed_out": false,
  "total": 2,
  "updated": 2,
  "deleted": 0,
  "batches": 1,
  "version_conflicts": 0,
  "noops": 0,
  "retries": {
    "bulk": 0,
    "search": 0
  },
  "throttled_millis": 0,
  "requests_per_second": -1,
  "throttled_until_millis": 0,
  "failures": []
}

_update_by_query在索引启动时获取索引的快照,并使用内部版本控制索引它的内容。如果在获取文档快照和索引请求执行期间文档发生变化就会出现版本冲突,只有当文档匹配上才会进行更新,版本号随之增加(注意内部版本控制是从1开始,不支持0)。所有的更新和查询的失败将会导致_update_by_query中止并返回failures字段的响应,同样无法回滚,成功的就成功了,失败的就失败了,如果只想统计版本冲突的次数而不是导致_update_by_query中止的次数,可以在请求体中加入"conflicts": "proceed"或者在URL中加入conflicts=proceed,比如:

POST twitter/_doc/_update_by_query?conflicts=proceed

也可以使用Query DSL来限制_update_by_query,比如下面的可以更新在twitter索引下所有名字为kimchy的用户:

POST twitter/_update_by_query?conflicts=proceed
{
  "query": {
    "term": {
      "user": "kimchy"
    }
  }
}

更新文档而不改变文档源,除此之外,_update_by_query还支持脚本更新文档,比如:

// 如果user是kimchy(所有符合的用户),将它的likes属性增加1
POST twitter/_update_by_query
{
  "script": {
    "source": "ctx._source.likes++",
    "lang": "painless"
  },
  "query": {
    "term": {
      "user": "kimchy"
    }
  }
}

也可以通过设置ctx.op去改变执行的行为,比如ctx.op = "noop"(空操作)、ctx.op = "delete"(删除操作),设置为其他任何行为都是错误的,这里就不使用conflicts=proceed了,在这种情况下想要获取一个版本冲突以便中止这些操作,好让我们可以处理这些错误。这个接口不允许移动它接触的文档,仅仅是修改,这样的设计是有意这么去干的,并没有规定将文档的原始位置移除,通过多索引和多类型操作也可以做这些事情,比如:

// 多索引间用逗号分隔,多类型之间用逗号分隔
POST twitter,blog/_doc,post/_update_by_query

如果提供了routing参数,路由被复制到滚动查询,将进程限制为与该路由值匹配的碎片,如:

POST twitter/_update_by_query?routing=1

默认情况下,_update_by_query使用1000滚动批次,这个数值可以通过scroll_sizeURL参数改变,如:

POST twitter/_update_by_query?scroll_size=100

_update_by_query也可以通过指定pipeline来使用“摄取节点”功能,如:

PUT _ingest/pipeline/set-foo
{
  "description" : "sets foo",
  "processors" : [ {
      "set" : {
        "field": "foo",
        "value": "bar"
      }
  } ]
}
POST twitter/_update_by_query?pipeline=set-foo

剩余的和_delete_by_query一样。

捡取新属性

 假设创建了一个没有动态映射的索引,用数据填充它,然后添加了一个映射值以从数据中获取更多字段,比如:

PUT test
{
  "mappings": {
    "_doc": {
      // 表示创建的字段不会被索引,仅仅是存储在_source中
      "dynamic": false,
      "properties": {
        "text": {"type": "text"}
      }
    }
  }
}

POST test/_doc?refresh
{
  "text": "words words",
  "flag": "bar"
}
POST test/_doc?refresh
{
  "text": "words words",
  "flag": "foo"
}

// 更新映射添加新的字段flag,要获取新字段,必须使用它重新索引所有文档
PUT test/_mapping/_doc
{
  "properties": {
    "text": {"type": "text"},
    "flag": {"type": "text", "analyzer": "keyword"}
  }
}