增删改查:文档操作

814 阅读28分钟

本文主要介绍一下几种文档操作,实际开发中如果需要使用更多的文档操作,建议参考Es官网。

  1. 新建文档,提供了索引文档(Index doc)和创建文档(create doc)两种方式。
  2. 通过文档 ID 获取文档,通过 Get API 来获取文档内容。
  3. 批量获取文档,使用 MGET API 来批量获取文档。
  4. 更新文档,使用 Update API 来更新文档。
  5. 删除文档,使用 Delete API 来删除文档。
  6. 批量操作文档,使用 Bulk API 来批量操作文档。

所有操作基于kibana来执行,本篇大部分案例以订单操作为模型,另一部分参考Es官方文档。

该索引包含四个字段,订单ID,订单名称,订单编号,子订单编号。

PUT order
{
  "mappings": {
    "properties": {
        "id": {
          "type": "keyword"
        },
        "name": {
          "type": "text"
        },
        "sub_order_no": {
          "type": "keyword"
        },
        "order_no": {
          "type": "keyword"
        }
      }
  },
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1
  }
}

1. 新建文档

可以使用 _doc_create 资源来索引一个新的 JSON 文档。使用 _create 能够确保只有在文档不存在时才进行索引。如果要更新已存在的文档,必须使用 _doc 资源。

1.1 使用Index API索引文档

PUT order/_doc/1
{
  "id": "1",
  "name": "小米手机-小米11-星空灰-128G-黑色-交易订单",
  "order_no": "1",
  "sub_order_no": [
    "2",
    "3",
    "4"
  ]
}

# 返回结果
{
  "_index" : "order",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

如果我们索引的文档已经存在,Es会先删除掉旧文档,然后再写入新文档的内容,并且增加文档版本号。

1.2 使用Create API创建文档

使用 Create API 创建文档有两种写法:PUT 和 POST 方式。

# 使用 PUT /POST 的方式创建文档 ,这里使用PUT和POST都可以
PUT order/_create/2
{
  "id": "2",
  "name": "小米11手机壳",
  "order_no": "2",
  "sub_order_no": []
}

# PUT 方式返回的结果
{
  "_index" : "order",
  "_type" : "_doc",
  "_id" : "2",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

# 使用 POST 的方式,不需要指定文档 ID, 系统自动生成
POST order/_doc
{
  "id": "3",
  "name": "小米11蓝牙耳机",
  "order_no": "3",
  "sub_order_no": []
}

# POST操作返回的结果
{
  "_index" : "order",
  "_type" : "_doc",
  "_id" : "qwRGD4kBjV6RAsb0RYfn",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

上面的 PUT 方式创建文档需要指定文档ID,如果文档ID已经存在,则返回HTTP状态码为409的错误。使用POST方式并不需要指定文档ID,Es会自动生成ID并返回。

请求方式描述
PUT /<target>/_doc/<_id>添加或更新指定ID的文档。如果文档不存在,将被创建;如果文档已存在,将被覆盖。
POST /<target>/_doc/添加新的文档,不需要指定文档ID。Elasticsearch会自动生成唯一的ID,并将文档添加到索引中。
PUT /<target>/_create/<_id>添加或更新指定ID的文档。如果文档已存在,不会被覆盖,而是返回错误。
POST /<target>/_create/<_id>添加新的文档,不需要指定文档ID。如果文档已存在,不会被添加,而是返回错误。

如果业务需求是存在即覆盖,建议使用第一种,如果存在唯一性校验应该使用第三/四种,如果需要系统自动生成文档ID,应该使用第二种。相对于第一种方式而言,第二种方式的效率更高,因为不需要判断文档是否已经存在,并进行后续的文档删除。

路径参数

参数名类型说明
<target>必选,字符串指定要操作的数据流或索引的名称。如果目标不存在并且与一个具有数据流定义的索引模板的名称或通配符 (*) 模式匹配,该请求将创建数据流。如果目标不存在并且不匹配数据流模板,则该请求将创建索引。可以使用 resolve index API 来检查现有的 target
<_id>可选,字符串文档的唯一标识符。以下请求格式需要提供此参数:PUT /<target>/_doc/<_id>PUT /<target>/_create/<_id>POST /<target>/_create/<_id>。如果要自动生成文档ID,请使用 POST /<target>/_doc/ 请求格式,并省略该参数。

查询参数

参数名类型说明
if_seq_no可选,整数仅在文档具有此序列号时执行操作。
if_primary_term可选,整数仅在文档具有此主要项时执行操作。
op_type可选,枚举设置为 create 仅在文档不存在时才进行索引(put if absent)。如果具有指定的 _id 的文档已经存在,则索引操作将失败。与使用 /_create 端点相同。有效值:index、create。如果指定了文档 ID,则默认为 index。否则,默认为 create。如果请求的目标是数据流,则需要 op_type 为 create。
pipeline可选,字符串预处理传入文档的流水线的 ID。
refresh可选,枚举如果为 true,则 Elasticsearch 刷新受影响的分片,使该操作对搜索可见;如果为 wait_for,则等待刷新以使该操作对搜索可见;如果为 false,则不执行刷新操作。有效值:true、false、wait_for。默认值:false。
routing可选,字符串用于将操作路由到特定分片的自定义值。
timeout可选,时间单位请求等待以下操作的时间段:自动索引创建、动态映射更新、等待活动分片。默认为 1m(一分钟)。这保证 Elasticsearch 在超时之前等待至少一次。实际等待时间可能更长,特别是在发生多次等待时。
version可选,整数显式版本号以进行并发控制。指定的版本必须与请求的文档的当前版本匹配,才能使请求成功。
version_type可选,枚举特定版本类型:external、external_gte。
wait_for_active_shards可选,字符串在继续操作之前必须处于活动状态的分片副本数量。设置为 all 或任何正整数,最多为索引中的分片总数(number_of_replicas+1)。默认值:1,即主分片。
require_alias可选,布尔值如果为 true,则目标必须是索引别名。默认为 false。

请求体

参数名是否必需数据类型说明
<field>RequiredString请求体中包含的文档数据的 JSON 源。

响应体

字段描述
_shards提供有关索引操作的复制过程的信息。
_shards.total指示索引操作应在多少个分片副本(主分片和副本分片)上执行。
_shards.successful指示索引操作成功的分片副本数量。当索引操作成功时,successful 至少为 1。
_shards.failed一个包含复制相关错误的数组,表示索引操作在副本分片上失败的情况。0 表示没有任何失败。
_index文档被添加到的索引的名称。
_type文档类型。Elasticsearch 现在只支持单个文档类型 _doc
_id添加文档的唯一标识符。
_version文档版本。每次更新文档时会递增。
_seq_no分配给文档的序列号,用于索引操作。序列号用于确保旧版本的文档不会覆盖新版本的文档。
_primary_term分配给文档的主要术语,用于索引操作。
result索引操作的结果,创建或更新。

如果请求的目标在索引模板中具有数据流定义并且不存在,索引操作会自动创建数据流。如果目标不存在并且不匹配数据流模板,操作会自动创建索引并应用任何匹配的索引模板。

Es内部包含一些内部的模板,请注意不要冲突。

如果不存在Mapping,则索引操作会创建动态Mapping。默认情况下,如果需要,新字段和对象会自动添加到Mapping中。

自动索引创建由 action.auto_create_index 设置控制。该设置默认为true,允许自动创建任何索引。您可以修改此设置以明确允许或阻止匹配指定模式的索引的自动创建,或者将其设置为false以完全禁用自动索引创建。指定一个逗号分隔的模式列表,想要允许的模式,或者在每个模式前加上+或-以指示它是允许还是阻止的。当指定了一个列表时,默认行为是不允许。

action.auto_create_index 设置只影响索引的自动创建,不影响数据流的创建。

# 允许自动创建名为my-index-000001或index10的索引,阻止创建与模式index1匹配的索引,并允许创建与模式ind匹配的其他索引。模式按照指定的顺序进行匹配。
PUT _cluster/settings
{
  "persistent": {
    "action.auto_create_index": "my-index-000001,index10,-index1*,+ind*" 
  }
}

# 禁止自动创建索引
PUT _cluster/settings
{
  "persistent": {
    "action.auto_create_index": "false" 
  }
}

# 允许自动创建索引,默认值。
PUT _cluster/settings
{
  "persistent": {
    "action.auto_create_index": "true" 
  }
}

你可以通过使用 _create 资源或将 op_type 参数设置为 create 来强制执行创建操作。在这种情况下,如果在索引中已经存在具有指定ID的文档,则索引操作将失败。换句话说,使用这种方式创建文档时,要求索引中的文档ID必须是唯一的,否则操作将失败。

当使用 POST /<target>/_doc/ 请求格式时,op_type 会自动设置为 create,并且索引操作会为文档生成一个唯一的 ID。换句话说,如果你使用这种请求格式创建文档,你不需要指定文档的 ID,索引操作会自动生成一个唯一的 ID,并将文档存储到索引中。

For Example:

POST my-index-000001/_doc/
{
  "@timestamp": "2099-11-15T13:12:00",
  "message": "GET /search HTTP/1.1 200 1070000",
  "user": {
    "id": "kimchy"
  }
}

响应结果:

{
  "_shards": {
    "total": 2,
    "failed": 0,
    "successful": 2
  },
  "_index": "my-index-000001",
   "_type": "_doc",
  "_id": "W0tpsmIBdwcYyG50zbta",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "result": "created"
}

索引操作可以被设定为有条件的,只有在文档的最后修改具有与 if_seq_noif_primary_term 参数指定的序列号和主要术语(primary term)相匹配时才会执行。如果检测到不匹配,操作将导致 VersionConflictException 异常,并返回状态码 409。这样的设定可以用于实现乐观并发控制,确保在执行索引操作之前先检查文档是否已被其他操作修改。

默认情况下,分片的位置或路由是通过使用文档ID值的哈希来控制的。为了更明确地控制路由,可以在每个操作中直接指定传递给路由器使用的哈希函数的值,使用 routing 参数。

POST my-index-000001/_doc?routing=kimchy
{
  "@timestamp": "2099-11-15T13:12:00",
  "message": "GET /search HTTP/1.1 200 1070000",
  "user": {
    "id": "kimchy"
  }
}

在上面的SQL中,根据提供的 routing 参数,文档将被路由到一个分片中:"kimchy"。

在设置显式映射时,还可以使用 _routing 字段来指示索引操作从文档本身中提取路由值。这会带来(非常小的)额外文档解析的开销。如果 _routing 映射被定义并设置为必需,如果未提供或提取到路由值,索引操作将失败。

无法针对数据流进行自定义路由操作,而是需要直接操作与数据流关联的底层索引。

索引操作首先在主分片上执行,然后再同步到副本分片上。

更多内容 -> 官方文档

2.获取文档

2.1 GET

以下是四种查看文档的方式的区别:

请求方法功能返回内容
GET <index>_doc/<_id>获取文档的元数据和内容包含文档的元数据和内容
HEAD <index>_doc/<_id>获取文档的元数据仅包含文档的元数据
GET <index>_source/<_id>获取文档的内容仅包含文档的内容
HEAD <index>_source/<_id>获取文档的内容元数据仅包含文档的内容元数据

通过GET请求获取_doc_source路径下的文档可以获得文档的完整内容,包括元数据和字段值。而使用HEAD请求,则只返回文档的元数据,不返回具体的内容。这在需要获取文档的元数据信息而不需要实际内容时很有用。

默认情况下,GET API是实时的,不受索引的刷新速率(数据何时可见于搜索)的影响。如果请求了存储字段(使用 stored_fields 参数),并且文档已更新但尚未刷新,则GET API将需要解析和分析元数据以提取存储字段。如果想禁用实时GET,可以将 realtime 参数设置为false。

GET my-index-000001/_doc/0

响应结果

{
  "_index": "my-index-000001",
  "_type": "_doc",
  "_id": "0",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "@timestamp": "2099-11-15T14:12:12",
    "http": {
      "request": {
        "method": "get"
      },
      "response": {
        "status_code": 200,
        "bytes": 1070000
      },
      "version": "1.1"
    },
    "source": {
      "ip": "127.0.0.1"
    },
    "message": "GET /search HTTP/1.1 200 1070000",
    "user": {
      "id": "kimchy"
    }
  }
}
HEAD my-index-000001/_doc/0

文档存在返回 200,文档不存在返回 404 。

默认情况下,get操作会返回 _source 字段的内容,可以通过 stored_fields 参数或者禁用 _source 字段来禁止返回 _source 字段的内容。

GET my-index-000001/_doc/0?_source=false

如果你只需要 _source 中的一个或两个字段,可以使用 _source_includes_source_excludes 参数来包含或过滤特定字段。这在处理大型文档时特别有帮助,部分检索可以减少网络开销。这两个参数都可以使用逗号分隔的字段列表或通配符表达式。

GET my-index-000001/_doc/0?_source_includes=*.id&_source_excludes=entities

如果只想指定包含字段,可以使用更简洁的表示方式:

GET my-index-000001/_doc/0?_source=*.id

获取 stored fields

准备数据

PUT my-index-000001
{
   "mappings": {
       "properties": {
          "counter": {
             "type": "integer",
             "store": false
          },
          "tags": {
             "type": "keyword",
             "store": true
          }
       }
   }
}

PUT my-index-000001/_doc/1
{
  "counter": 1,
  "tags": [ "production" ]
}
GET my-index-000001/_doc/1?stored_fields=tags,counter

返回结果

{
   "_index": "my-index-000001",
   "_type": "_doc",
   "_id": "1",
   "_version": 1,
   "_seq_no" : 22,
   "_primary_term" : 1,
   "found": true,
   "fields": {
      "tags": [
         "production"
      ]
   }
}

当使用GET请求获取文档时,返回的字段值总是作为数组返回。对于未存储的字段,例如"counter"字段,获取请求将忽略该字段,不返回其值。这意味着如果尝试获取一个未存储的字段的值,返回的结果中将不包含该字段的值,而对于存储的字段,则会将其值作为数组返回。

上面所说取决于字段的存储设置和查询请求中指定的字段列表。如果要获取特定字段的值,请确保该字段已存储或在查询请求中明确指定。

如果在索引操作中使用了路由(routing),在检索文档时也需要指定相应的路由值。

GET my-index-000001/_doc/2?routing=user1

这个请求获取了ID为2的文档,但它是根据用户进行路由的。如果没有指定正确的路由值,文档将不会被获取。

refresh=true 参数可以在执行get操作之前将相关分片刷新,并使其可搜索。将refresh参数设置为true应该经过慎重考虑和验证,确保这不会对系统造成过重的负载(并降低索引速度)。

GET操作会根据特定的分片ID进行哈希操作,然后将请求重定向到该分片ID内的一个副本,并返回结果。副本包括主分片和该分片ID组内的其他副本。通过增加分片的副本数量,可以提高GET操作的性能和并发处理能力。副本分担了主分片的负载,并且可以同时处理更多的读请求,从而提高整个系统的性能和响应能力。


更多参数 -> 官方文档

2.2 MGET

如果一次想要获取多条数据,通过 GET 的方式一条条获取需要发多次 HTTP 请求,这样很不划算的,建议使用 MGET 一次性获取。MGET API 请求的三种格式如下:

# 1:在 body 中指定 index
GET /_mget
{
  "docs": [
    { "_index": "order", "_id": "1" },
    { "_index": "order", "_id": "2" }
  ]
}

# 2:直接指定 index
GET /order/_mget
{
  "docs": [
    { "_id": "1" },
    { "_id": "2" }
  ]
}

# 3:也可以简写为一下例子
GET /order/_mget
{
  "ids" : ["1", "2"]
}

更多参数 -> 官方文档


3.更新文档

3.1 根据ID更新

更新一个文档,需要指定文档的 ID 和需要更新的字段与其对应的值。

# 更新文档
POST order/_update/2
{
  "doc": {
    "name":"小米11-无线蓝牙耳机"
  }
}

# 返回结果
{
  "_index" : "order",
  "_type" : "_doc",
  "_id" : "2",
  "_version" : 3,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 2,
  "_primary_term" : 1
}

从返回结果可以看到,更新成功后的返回结果中, _version 字段会增加,result字段为 updated。

这个更新API和索引文档时候的覆盖更新有什么区别呢?索引文档API的更新是先删除旧的文档然后重新写入新的文档,是覆盖式更新,更新API可以实现仅更新某些字段的需求。

路径参数

参数描述
<index>(必填)目标索引的名称。如果索引不存在,默认情况下会自动创建。
<_id>(必填)要更新的文档的唯一标识符。

查询参数

参数描述
if_seq_no(可选,整数)仅当文档具有此序列号时执行操作。
if_primary_term(可选,整数)仅当文档具有此主要期限时执行操作。
lang(可选,字符串)脚本语言。默认值:painless。
require_alias(可选,布尔值)如果为true,则目标必须是索引别名。默认值为false。
refresh(可选,枚举)如果为true,则Elasticsearch将刷新受影响的分片,使此操作对搜索可见;如果为wait_for,则等待刷新以使此操作对搜索可见;如果为false,则不执行刷新操作。有效值:true、false、wait_for。默认值为false。
retry_on_conflict(可选,整数)指定在发生冲突时重试操作的次数。默认值为0。
routing(可选,字符串)用于将操作路由到特定分片的自定义值。
_source(可选,列表)设置为false以禁用源检索(默认值:true)。还可以指定要检索的字段的逗号分隔列表。
_source_excludes(可选,列表)指定要排除的源字段。
_source_includes(可选,列表)指定要检索的源字段。
timeout(可选,时间单位)等待以下操作的时间段:动态映射更新、等待活动分片。默认值为1分钟。这确保Elasticsearch在超时之前等待。实际等待时间可能会更长,特别是当发生多个等待时。
wait_for_active_shards(可选,字符串)在继续操作之前必须处于活动状态的分片副本数量。设置为all或任何正整数,最多为索引中的分片总数(副本数+1)。默认值为1,即主分片。

1)Upsert

当执行更新操作时,如果文档存在,则使用指定的脚本对文档进行更新;如果文档不存在,则将 upsert 元素的内容作为新文档插入到索引中。

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

2)Scripted Upsert

scripted_upsert 参数的作用是确保无论文档是否存在,都会执行指定的脚本操作。

POST test/_update/1
{
  "scripted_upsert": true,
  "script": {
    "source": """
      if ( ctx.op == 'create' ) {
        ctx._source.counter = params.count
      } else {
        ctx._source.counter += params.count
      }
    """,
    "params": {
      "count": 4
    }
  },
  "upsert": {}
}

通常情况下,更新操作需要同时发送部分文档和 upsert 文档(用于插入新文档)。但是,可以将 doc_as_upsert 参数设置为 true,这样可以直接使用 doc 参数作为 upsert 的内容。

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

更多内容 -> 官方文档

3.2 根据查询更新

除了根据ID更新以外,还支持根据查询结果进行更新update_by_query

POST order/_update_by_query
{
  "query": {
    "term": {
      "order_no": {
        "value": "1"
      }
    }
  },
  "script": {
    "source": "ctx._source.name='小米手机-小米13-星空灰-128G-黑色-交易订单'",
    "lang": "painless"
  }
}

上面的Example,我们将订单号=1的订单的name进行了更新。

如果query的结果很大,那么这个接口在kibana中执行的时候可能会超时。

  1. 重新设置timeout参数。
  2. 使用异步的方式执行请求,并限制查询条件分批更新数据。

?wait_for_completion=false Es的异步操作都可以通过这个参数来指定。

快照与版本冲突:当提交一个 Update By Query 请求时,Elasticsearch 在开始处理请求时获取数据流或索引的快照,并使用内部版本控制更新匹配的文档。当版本号匹配时,文档会被更新,并且版本号会递增。如果在获取快照和执行更新操作之间文档发生了变化,就会导致版本冲突,更新操作将失败。

处理版本冲突:可以选择在发生版本冲突时计数而不是停止并返回结果,通过将 conflicts 参数设置为 proceed。需要注意的是,如果选择计数版本冲突,操作可能会尝试从源数据中更新更多的文档,直到成功更新了 max_docs 个文档,或者遍历了源查询中的每个文档。

不能使用 Update By Query 更新版本号为 0 的文档,因为版本号为 0 被认为是无效的。

在处理 Update By Query 请求时,Elasticsearch会逐个执行搜索请求来查找匹配的文档,并按批进行更新。即使其中一批更新请求失败,已成功完成的更新请求仍然会生效,不会被回滚。

更多参数&内容 -> 官方文档

4.删除文档

4.1 根据ID删除

DELETE order/_doc/2

如果文档存在则删除成功,"result" 字段为 "deleted"。如果文档本身不存在,则返回 404。

更多内容->官方文档

4.2 根据查询删除

POST /order/_delete_by_query
{
  "query": {
    "term": {
      "order_no": {
        "value": "1"
      }
    }
  }
}

这里同样注意尽量使用异步,并通过分批查询控制查询结果的大小。

在提交 Delete By Query 请求时,Elasticsearch会获取一个数据快照并使用内部版本控制来删除匹配的文档。如果在获取快照和删除操作处理期间文档发生了变化,就会导致版本冲突并导致删除操作失败。

不能使用 Delete By Query 更新版本号为 0 的文档,因为版本号为 0 被认为是无效的。

在处理 Delete By Query 请求时,Elasticsearch会执行多个搜索请求来查找所有匹配的要删除的文档。对于每个匹配的文档批量删除请求都会被执行。如果有搜索或批量请求被拒绝,系统会进行最多10次的重试,并采用指数回退策略。如果达到最大重试次数,处理会停止,并将所有失败的请求返回到响应中。任何成功完成的删除请求都是最终的,它们不会被回滚。可以选择记录版本冲突次数而不是停止操作,并通过设置 conflicts 参数为 proceed 实现。

如果选择记录版本冲突次数,操作可能会尝试删除更多文档,直到成功删除了指定的最大文档数量(max_docs)或遍历了源查询中的所有文档。

更多内容->官方文档


5.批量写文档

Es的读操作支持批量读,写操作自然也支持批量写。

Bulk API 支持在一次调用中操作不同的索引,使用时可以在 Body 中指定索引也可以在 URI 中指定索引。而且还可以同时支持 4 中类型的操作:

  1. Index
  2. Create
  3. Update
  4. Delete

Bulk API 的格式是用换行符分隔 JSON 的结构,第一行指定操作类型和元数据(索引、文档id等),紧接着的一行是这个操作的内容(文档数据,如果有的话。像简单的删除就没有。)。

For Example:

POST _bulk
# 第一行指定操作类型和元数据(索引、文档id等)
{ "index" : { "_index" : "order", "_id" : "1" } } 
# 紧接着的一行是这个操作的内容(文档数据,如果有的话。像简单的删除就没有)
{ "id": "1","name": "小米手机-小米13-星空灰-128G-黑色-交易订单", ......}

通过bulk进行多种类型的文档操作:

# 在 Bulk API 中同时使用多种操作类型的实例
POST _bulk
{"index":{"_index":"order","_id":"3"}}
{"id":"3","name":"小米13-无线充电器","order_no":"3","sub_order_no":[]}
{"delete":{"_index":"order","_id":"2"}}
{"create":{"_index":"order","_id":"4"}}
{"id":"4","name":"米家床头灯2","order_no":"4","sub_order_no":[]}
{"update":{"_index":"order","_id":"4"}}
{"doc":{"name":"米家加湿器"}} 

注意:在kibana执行的时候,需要先将json体压缩,否则执行会报错。

因为一个请求中有多个操作,所以返回结果中会对每个操作有相应的执行结果。如果其中一条操作失败,是不会影响其他操作的执行。

更多细节->官方文档


6.?refresh参数

Index、Update、Delete和Bulk API中可以使用refresh参数来控制何时将此请求所做的更改显示在搜索结果中。refresh参数支持以下几种取值:

描述
空字符串或true立即在操作发生后刷新相关的主分片和副本分片(而不是整个索引),以使更新后的文档立即出现在搜索结果中。使用此选项时需要仔细考虑和验证,确保不会导致性能下降,无论是从索引还是搜索的角度来看。
wait_for在回复之前等待请求所做的更改通过刷新可见。这不会强制立即刷新,而是等待刷新发生。Elasticsearch会自动按照索引的index.refresh_interval(默认为1秒)刷新已更改的分片。该设置是动态的。调用Refresh API或将refresh参数设置为true的任何支持该参数的API也会导致刷新操作,从而导致已经使用refresh=wait_for的请求返回结果。
false不执行任何与刷新相关的操作。此请求所做的更改将在请求返回后的某个时间点后才会对外可见。

如何选择使用哪一个值?

如果业务要求必须等待更改可见之后才返回,可以考虑设置 refresh 的值为 wait_fortrue 。否则建议使用默认值 false

  1. 使用refresh=wait_for相比于refresh=true,对于索引发生的更多变更能够节省更多的工作量。如果索引每隔index.refresh_interval只发生一次变更,那么refresh=wait_for并不会带来额外的工作量节省。

  2. refresh=true会创建效率较低的索引结构(较小的段),这些索引结构在后续需要合并为更高效的索引结构(较大的段)。这意味着refresh=true的代价会在索引时用于创建小段、在搜索时用于搜索小段,并在合并时将小段合并为大段。

  3. 不要连续发起多个refresh=wait_for请求。相反,将它们批量放入一个带有refresh=wait_for的批量请求中,Elasticsearch会并行启动它们,并在所有请求完成后才返回结果。

  4. 如果将刷新间隔设置为-1,即禁用自动刷新,则带有refresh=wait_for的请求将一直等待,直到某个操作触发了刷新。相反,将index.refresh_interval设置为比默认值更短的时间(如200毫秒)会使refresh=wait_for更快返回结果,但仍会生成效率较低的段。

  5. refresh=wait_for只影响当前请求,而refresh=true会立即刷新并影响其他正在进行的请求。一般来说,如果你有一个正在运行的系统并且不想干扰它,那么使用refresh=wait_for是较小的修改。

refresh=wait_for 可以触发强制刷新。

当已经有大量请求在等待刷新时(达到了 index.max_refresh_listeners(默认为1000)的限制),使用 refresh=wait_for 的请求可能会被强制执行刷新操作,以保证搜索结果的一致性。同时,bulk请求只会占据一个槽位,无论其修改了多少次分片。

For Example:

这些操作将创建一个文档并立即刷新索引,以使其可见:

PUT /test/_doc/1?refresh
{"test": "test"}
PUT /test/_doc/2?refresh=true
{"test": "test"}

这些操作将创建一个文档,但不会采取任何措施使其对搜索可见:

PUT /test/_doc/3
{"test": "test"}
PUT /test/_doc/4?refresh=false
{"test": "test"}

这将创建一个文档,并等待它在搜索中可见:

PUT /test/_doc/4?refresh=wait_for
{"test": "test"}

7.乐观并发控制

Elasticsearch是分布式的。当文档被创建、更新或删除时,新版本的文档需要被复制到集群中的其他节点上。Elasticsearch是异步和并发的,这意味着复制请求是并行发送的,并且可能以无序的方式到达目的地。Elasticsearch需要一种方式来确保旧版本的文档永远不会覆盖新版本。

为了确保旧版本的文档不会覆盖新版本,每个对文档执行的操作都由协调该更改的主分片分配一个序列号 _seq_no_seq_no随着每个操作的执行而增加,因此较新的操作保证具有比较旧的操作更高的 _seq_no。然后,Elasticsearch可以使用操作的 _seq_no 来确保较新的文档版本永远不会被具有较小 _seq_no 的更改覆盖。

以下索引命令将创建一个文档并为其分配初始 _seq_no_primary_term

PUT products/_doc/1567
{
  "product" : "r2d2",
  "details" : "A resourceful astromech droid"
}

可以再响应结果中看到 _seq_no_primary_term 这两个字段。

{
  "_shards": {
    "total": 2,
    "failed": 0,
    "successful": 1
  },
  "_index": "products",
  "_type": "_doc",
  "_id": "1567",
  "_version": 1,
  "_seq_no": 362,
  "_primary_term": 2,
  "result": "created"
}

Elasticsearch会跟踪每个存储的文档中最后一次更改操作的_seq_no_primary_term_seq_no_primary_term会在GET API的响应中以_seq_no_primary_term字段的形式返回:

GET products/_doc/1567

响应结果

{
  "_index": "products",
  "_type": "_doc",
  "_id": "1567",
  "_version": 1,
  "_seq_no": 362,
  "_primary_term": 2,
  "found": true,
  "_source": {
    "product": "r2d2",
    "details": "A resourceful astromech droid"
  }
}

通过设置 seq_no_primary_term 参数,Search API 可以返回每个搜索结果命中的文档的 _seq_no_primary_term 信息。

序列号(sequence number)和主要术语(primary term)唯一标识一次变更。通过查询返回的 _seq_no_primary_term ,您可以确保只在检索文档后没有对其进行其他更改时才进行文档的修改。这可以通过设置 index API、update API或delete APIif_seq_noif_primary_term 参数来实现。

以下的索引调用将确保在不丢失描述的任何潜在更改或通过其他API添加另一个标签的情况下,向文档添加一个tag:

PUT products/_doc/1567?if_seq_no=362&if_primary_term=2
{
  "product": "r2d2",
  "details": "A resourceful astromech droid",
  "tags": [ "droid" ]
}

我有一个订单索引,里面主要包含,id,name,createTime,updateTime字段,现在我的需求是想判断指定name的文档是否存在,如果存在就更新updatetime为当前时间,否则新建文档,并且当发生冲突的时候,判断如果当前时间大于文档中的updatetime时间,仍然要更新文档的updatetime为当前时间,这条es语句要怎么写?

可以使用Elasticsearch的Update API结合乐观并发控制来执行更新操作。

POST your-index/_update
{
  "script": {
    "source": "if (ctx._source.containsKey('updateTime') && ctx._source.updateTime < params.currentTimestamp) { ctx._source.updateTime = params.currentTimestamp }",
    "lang": "painless",
    "params": {
      "currentTimestamp": "当前时间"
    }
  },
  "upsert": {
    "id": "指定的文档ID",
    "name": "指定的名称",
    "createTime": "创建时间",
    "updateTime": "当前时间"
  },
  "refresh": true
}
        

该语句首先尝试更新指定文档的updateTime字段,如果该字段存在并且小于当前时间,则将其更新为当前时间。如果文档不存在,则执行upsert操作,即新建一个文档,并设置其字段包括id、name、createTime和updateTime。

使用乐观并发控制,当发生冲突时,通过比较当前时间和文档中的updateTime来判断是否仍然需要更新updateTime字段。另外,为了保证操作的可见性,可以通过将refresh参数设置为true来立即刷新索引,使更新操作对搜索可见。

如果在并发读写的情况下存在多个线程同时尝试更新同一文档,仍然有可能抛出版本冲突异常。这是由于乐观并发控制的机制,每个线程在执行更新操作时会检查文档的版本号,如果发现版本号已经被其他线程修改,则会导致版本冲突异常。

为了避免版本冲突异常,可以通过以下几种方式来优化并发更新:

  1. 使用重试机制:在遇到版本冲突异常时,可以在代码中实现重试逻辑,重新执行更新操作。通过适当的重试次数和延迟时间,可以增加成功更新的机会。

  2. 使用乐观并发控制的自动重试:Elasticsearch的Update API支持在发生版本冲突时自动重试操作。你可以在请求中添加retry_on_conflict参数来指定重试次数。例如,将retry_on_conflict设置为3,表示在发生版本冲突时最多进行3次自动重试。

  3. 使用版本号控制:可以在更新文档时指定具体的版本号,通过控制版本号的方式来避免冲突。在更新操作中使用version参数,并将其设置为当前文档的版本号。如果版本号匹配,则执行更新操作,否则会抛出版本冲突异常。

以上方法可以在一定程度上减少版本冲突异常的发生,但在高并发环境下,完全消除版本冲突是非常困难的。因此,在设计系统时应考虑如何处理版本冲突异常,以确保数据的一致性和可靠性。


8.Read & Write

在 Elasticsearch 中,每个索引都被分割为多个分片(shard),而每个分片可以有多个副本(replica)。这些副本组成一个复制组(replication group),在添加或删除文档时需要保持同步。如果未能保持同步,从一个副本读取的结果将与从另一个副本读取的结果非常不同。将分片副本保持同步并从中提供读取的过程称为数据复制模型。

Elasticsearch 的数据复制模型基于主备份(primary-backup)模型。该模型基于在复制组中有一个作为主分片(primary shard)的单个副本。其他副本称为副本分片(replica shards)。主分片作为所有索引操作的主要入口点。它负责验证这些操作并确保其正确性。一旦主分片接受了索引操作,它还负责将操作复制到其他副本。

本节的目的是对 Elasticsearch 的复制模型进行高级别的概述,并分析它对写入和读取操作之间各种交互的影响。在数据复制模型下,可以提供高可用性和容错性,同时保证数据的一致性和准确性。

8.1 基本的写入模型

Elasticsearch 中索引操作的处理过程包括协调阶段(coordinating stage)、主分片阶段(primary stage)和复制分片阶段(replica stage)。

  1. 协调阶段:在进行索引操作时,Elasticsearch 首先使用路由机制将操作请求定位到特定的复制分片组。路由通常基于文档的唯一标识符(ID)。一旦复制分片组确定,操作请求将被内部转发到该组的当前主分片上。在此阶段,协调节点负责协调请求的处理。

  2. 主分片阶段:主分片是负责验证和处理索引操作的分片。在主分片阶段,主分片首先对操作进行验证,如果操作结构上无效,则拒绝操作。然后,主分片在本地执行操作,即索引或删除相关文档。完成本地操作后,主分片将操作转发到当前所有的复制分片。

  3. 复制分片阶段:复制分片是主分片的副本,用于提供高可用性和冗余。在复制分片阶段,主分片将操作请求并行地转发到每个复制分片。每个复制分片会在本地执行索引操作,以保持数据的一致性。一旦每个复制分片成功执行操作并响应给主分片,主分片将向客户端确认操作已成功完成。

这些索引阶段(协调阶段、主分片阶段和复制分片阶段)是按顺序执行的。为了实现内部重试机制,每个阶段的生命周期包括后续阶段的生命周期。例如,协调阶段直到每个主分片的主阶段都完成之前不会结束。每个主分片的主阶段直到所有复制分片完成本地索引操作并响应复制请求之后才会完成。通过这样的流程,Elasticsearch 确保了索引操作的正确性和数据的一致性。索引操作先经过协调阶段,然后由主分片执行验证和处理操作,并复制到所有复制分片。这样可以保持数据的冗余和可用性,同时提供高性能的索引功能。

1)失败处理

Elasticsearch 在索引过程中可能发生的各种问题以及主分片如何应对这些问题。

  1. 主分片失败:索引过程中,可能会发生磁盘损坏、节点之间断开连接或配置错误等问题,导致副本上的操作在主分片上失败。尽管这些情况不太常见,但主分片需要对其进行响应。

  2. 主分片失败的处理:如果主分片本身失败,主分片所在的节点会向主节点发送一条消息报告失败。索引操作将等待(默认最多 1 分钟)主节点将其中一个副本提升为新的主分片。然后,操作将转发给新的主分片进行处理。需要注意的是,主节点还监控节点的健康状态,并可能主动降级主分片。通常情况下,这会在持有主分片的节点由于网络问题与集群隔离时发生。

  3. 主分片和副本之间的操作处理:一旦主分片成功执行了操作,它还需要处理在副本分片上执行操作时可能发生的故障。这可能是由于副本的实际故障或网络问题导致操作无法到达副本(或无法从副本响应)。这些故障都会导致在即将确认的“同步副本集”中的副本丢失操作。为了避免违反不变性,主分片向主节点发送消息,请求将存在问题的分片从“同步副本集”中移除。只有在主节点确认移除分片后,主分片才会确认操作。需要注意的是,主节点还会指示其他节点开始构建新的分片副本,以恢复系统到正常状态。

  4. 主分片的状态验证:在将操作转发给副本时,主分片会利用副本来验证自身是否仍然是活跃的主分片。如果由于网络分区(或长时间的垃圾回收)导致主分片被隔离,它可能会在意识到自己已被降级之前继续处理传入的索引操作。来自陈旧主分片的操作将被副本拒绝。当主分片收到来自副本的拒绝请求的响应,因为它不再是主分片时,它将联系主节点并得知自己已被替换。然后,操作将路由到新的主分片。

总结来说,主分片在索引操作中起到关键的作用,负责验证和处理操作,并与副本分片保持同步。它处理可能发生的故障,并确保数据的一致性。通过主分片的协调和转发,Elasticsearch实现了高可用性和数据冗余,确保索引操作的成功和系统的健壮性。

2)主分片没有副本

当所有的分片副本都失败或由于索引配置问题而不可用时,只剩下主分片是唯一可用的副本。在这种情况下,主分片会独立处理操作,而没有其他副本进行验证,这可能会引起一些担忧。然而,主分片无法自行将其他分片标记为失败,而是会向主节点发送请求,由主节点代表其执行此操作。这意味着主节点了解到主分片是唯一可靠的副本。因此,我们可以确保主节点不会将任何其他过时的分片副本提升为新的主分片,并且主分片中的任何操作都不会丢失。然而,需要注意的是,由于此时只有单个数据副本,如果发生物理硬件问题,可能会导致数据丢失。为了应对这种情况,可以采取一些措施来减轻风险。

8.2 基本的读模型

在Elasticsearch中,读取操作可以是非常轻量级的按ID查找,也可以是复杂的搜索请求,包含了复杂的聚合操作,需要较高的CPU计算能力。主备模型的一个优点是它保持所有分片副本的一致性(除了正在处理的操作)。因此,只需要一个处于同步状态的副本就足以提供读取请求的服务。

当一个节点接收到读取请求时,该节点负责将请求转发给持有相关分片的节点,并将响应进行汇总,然后向客户端返回响应。我们称这个节点为协调节点。基本流程如下:

  1. 将读取请求解析为相关的分片。注意,由于大多数搜索请求涉及多个索引,因此通常需要从多个分片中读取,每个分片代表了不同的数据子集。

  2. 从分片的复制组中选择一个活动副本,可以是主分片或副本。默认情况下,Elasticsearch使用自适应副本选择来选择分片副本。

  3. 将分片级别的读取请求发送到所选的副本。

  4. 合并结果并进行响应。需要注意的是,在按ID查找时,只有一个分片是相关的,可以跳过这一步。

通过这样的流程,Elasticsearch能够高效地处理读取请求,将请求转发给适当的副本,并将结果汇总后返回给客户端。

1)分片失败

当一个分片无法响应读取请求时,协调节点会将请求发送给同一复制组中的另一个分片副本。如果重复失败导致没有可用的分片副本,为了保证快速响应,以下API将返回部分结果:

Search(搜索) Multi Search(批量搜索) Multi Get(批量获取)

即使返回了部分结果,响应仍会返回200 OK的HTTP状态码。分片失败可以通过响应头中的timed_out和_shards字段来指示。这样做可以确保在出现部分故障的情况下仍然能够返回可用的数据,并提供尽可能快速的响应。

8.3 一些小的影响

由于读取和写入请求可以并发执行,这两个基本流程会相互交互。这带来了一些固有的影响:

正常情况下,每个读操作只会在每个相关的复制组中执行一次,避免了重复的搜索操作。只有在故障条件下,才会出现多个相同分片的副本执行相同的搜索操作。这样可以保证系统的正常运行和高效性能。

由于主分片首先在本地进行索引,然后再复制请求,因此在被确认之前,同时进行的读取操作可能已经看到了变更。在Elasticsearch的写入流程中,主分片会先在本地执行索引操作,然后将该操作复制到副本分片。在这个过程中,如果有同时进行的读取操作,那么读取操作可能会在索引操作被确认之前就已经读取到了最新的变更。

这种情况可能会发生在以下情况下:

  1. 写入操作的确认比读取操作更慢,导致读取操作在确认之前就访问到了最新的数据变更。
  2. 复制操作存在一定的延迟,导致副本分片还没有完全复制索引操作,但是读取操作已经访问到了主分片上的最新变更。

这种情况下,读取操作可能会看到尚未被确认的最新数据变更,这可能会导致读取的结果在不同的时间点上出现不一致。这是因为并发的写入和读取操作之间存在一定的时间差和复制延迟。

尽管存在这种可能性,Elasticsearch会尽最大努力保证数据的一致性和可靠性。通过复制机制和确认机制,它会尽量减少数据不一致的情况并提供高可用性的读写操作。

这种模型可以在只保留两个数据副本的情况下实现容错性。这与基于法定人数(quorum)的系统相反,后者要求容错性的最小副本数量为3。

在传统的分布式系统中,为了保证数据的容错性和可用性,通常采用法定人数(quorum)的概念。这意味着在进行数据复制和决策时,需要满足一定数量的副本的一致性。在大多数情况下,这个法定人数是集群中副本数量的一半加1。

然而,在Elasticsearch的主副本模型中,只需要两个副本就能实现容错性。这是因为主分片负责接收并处理所有的写入请求,并将写入操作复制到副本分片。只要主分片和至少一个副本分片是可用的,系统就可以继续正常工作。

这种设计有助于简化系统的复杂性,并减少副本之间的同步和通信开销。同时,它也提供了良好的性能和高可用性。尽管只有两个副本,但Elasticsearch仍然通过其他机制来确保数据的一致性和可靠性,如主分片的复制和确认机制。

因此,Elasticsearch的主副本模型提供了一种高效且具有容错性的解决方案,而无需维护额外的副本数量。

8.4 Failures

发生故障的时候,可以会出现以下情况:

  1. 单个分片可能会减慢索引过程。

由于主分片在每个操作期间等待所有处于同步副本集中的副本,因此单个较慢的分片可能会减慢整个复制组的速度。这是我们为了获得上面提到的读取效率所付出的代价。当然,单个较慢的分片也会减慢被路由到它的搜索操作。

  1. 脏读

当一个主分片(primary shard)处于网络隔离的状态时,它可能会接收并处理写操作,但这些操作可能不会被确认(acknowledged)。这是因为主分片在发送请求给它的副本(replicas)或与主节点通信之前,并不会意识到自己处于隔离状态。在此时,操作已经被索引到主分片中,并可以被并发读取。为了减轻这种风险,Elasticsearch会每秒(默认设置)向主节点发送心跳检测,并在没有可知的主节点时拒绝索引操作。这样可以提前发现主节点的隔离情况,从而防止写操作传递到处于网络隔离状态的主分片。