下面介绍文档相关的 CRUD 以及批量操作

Index

Index API 用于向指定索引新增或更新 JSON 文档,使其可以被搜索。如下示例,用于将 JSON 文档插入 id 为 1 的 “twitter” 索引中。注意,在 ES 7.0 版本之后 type 名称统一约定使用 _doc

  1. curl -X PUT "localhost:9200/twitter/_doc/1?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "user" : "kimchy",
  4. "post_date" : "2009-11-15T14:12:12",
  5. "message" : "trying out Elasticsearch"
  6. }'

返回结果如下:

  1. {
  2. "_index" : "twitter",
  3. "_type" : "_doc",
  4. "_id" : "1",
  5. "_version" : 1,
  6. "result" : "created",
  7. "_shards" : {
  8. "total" : 2,
  9. "successful" : 1,
  10. "failed" : 0
  11. },
  12. "_seq_no" : 0,
  13. "_primary_term" : 1
  14. }

响应信息中包含了创建的文档所在的索引、类型、id、版本、分片、是否创建成功等信息。其中版本号会随着文档的更新自动递增,_shards 提供了索引操作的复制过程信息,其含义如下:

  • total 表示索引操作应该在多少个分片(包括主分片和副本分片)上执行。
  • successful 表示索引操作成功的分片数。
  • failed 表示副本分片上索引操作失败时的相关错误信息。

默认当 successful 的数量大于等于 1 时,表示索引操作成功。

如果文档对应的索引不存在,索引操作将自动创建索引,并应用已配置的任何索引模板。如果不存在动态映射,索引操作还会创建动态映射。默认情况下,新的字段和对象将自动添加到映射定义中。自动创建索引是由 action.auto_create_index 属性控制的,默认值为 true,意味着索引总是自动创建的。

1. create

当我们向索引中添加文档时,如果 id 对应的文档不存在则会创建新的索引,否则,会删除现有的文档,再创建新的文档,同时版本号加一。

此外,index 操作还接受可用于强制创建操作的 op 类型,允许 “put-if-absent” 行为。当使用 create 类型时,如果索引中已经存在该 id 的文档,则索引操作将直接失败。

  1. curl -X PUT "localhost:9200/twitter/_doc/1?op_type=create&pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "user" : "kimchy",
  4. "post_date" : "2009-11-15T14:12:12",
  5. "message" : "trying out Elasticsearch"
  6. }'

返回结果如下:

  1. {
  2. "error" : {
  3. "root_cause" : [
  4. {
  5. "type" : "version_conflict_engine_exception",
  6. "reason" : "[1]: version conflict, document already exists (current version [1])",
  7. "index_uuid" : "KpzW0ao2QlCwZeIZJJywzQ",
  8. "shard" : "0",
  9. "index" : "twitter"
  10. }
  11. ],
  12. "type" : "version_conflict_engine_exception",
  13. "reason" : "[1]: version conflict, document already exists (current version [1])",
  14. "index_uuid" : "KpzW0ao2QlCwZeIZJJywzQ",
  15. "shard" : "0",
  16. "index" : "twitter"
  17. },
  18. "status" : 409
  19. }

上述操作还可以简写为:

  1. curl -X PUT "localhost:9200/twitter/_create/1?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "user" : "kimchy",
  4. "post_date" : "2009-11-15T14:12:12",
  5. "message" : "trying out Elasticsearch"
  6. }'

2. ID 自动生成

Elasticsearch 支持可以在不指定文档 id 的情况下执行索引操作。此时 Elasticsearch 将自动生成一个文档 id。同时 op 类型将自动设置为 create。我们把上面的命令去掉 id 参数,然后再次执行索引文档命令。注意,要用 POST 来代替 PUT,否则会出现异常:

  1. curl -X POST "localhost:9200/twitter/_doc/?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "user" : "kimchy",
  4. "post_date" : "2009-11-15T14:12:12",
  5. "message" : "trying out Elasticsearch"
  6. }'

返回结果如下:

  1. {
  2. "_index" : "twitter",
  3. "_type" : "_doc",
  4. "_id" : "pOwcJXoBg2C-dN53ZQUN",
  5. "_version" : 1,
  6. "result" : "created",
  7. "_shards" : {
  8. "total" : 2,
  9. "successful" : 1,
  10. "failed" : 0
  11. },
  12. "_seq_no" : 2,
  13. "_primary_term" : 1
  14. }

从响应结果中可以看到,文档创建成功了,并且 id 是自动生成的字符串

3. 可选参数

version 设置文档版本号,主要用于实现乐观锁
version_type 默认为 internal,请求参数指定的版本号与存储的文档版本号相同则写入。其他可选值有 external 等类型,为 external 类型时,如果当前存储的文档版本号小于请求参数指定的版本号,则写入数据。version_type 主要控制版本号的比较机制,用于对文档进行并发更新操作时同步数据
wait_for_active_shards 用于控制写一致性,当指定数量的分片副本可用时才执行写入,否则重试直至超时。默认为 1,表示主分片可用即执行写入
refresh 写入完毕后执行 refresh,使其对搜索可见
timeout 请求超时时间,默认为 1 分钟
pipeline 指定事先创建好的 pipeline 名称

Get

Elasticsearch 提供了 Get API 用来查看存储在 Elasticsearch 服务器中的文档,使用 GET 命令并指定文档所在的索引、类型和 id 即可返回一个 JSON 格式的文档,命令如下:

  1. curl -X GET "localhost:9200/twitter/_doc/1?pretty"

返回结果如下:

  1. {
  2. "_index" : "twitter",
  3. "_type" : "_doc",
  4. "_id" : "1",
  5. "_version" : 2,
  6. "_seq_no" : 1,
  7. "_primary_term" : 1,
  8. "found" : true,
  9. "_source" : {
  10. "user" : "kimchy",
  11. "post_date" : "2009-11-15T14:12:12",
  12. "message" : "trying out Elasticsearch"
  13. }
  14. }

返回结果中的前 4 个属性表明文档的位置和版本信息,found 属性用于表明是否查询到文档,_source 字段中是原始的 JSON 文档。

默认 Get 操作会返回 _source 字段的全部内容。如果只需要其中部分字段,则可以使用 _source_includes_source_excludes 参数来包含或过滤需要的部分以节省网络传输的开销。两个参数都使用逗号分隔的字段列表或通配符表达式。如下示例:

  1. curl -X GET "localhost:9200/twitter/_doc/1?_source_includes=user,message&_source_excludes=post_date&pretty"

如果只想指定返回的 _source 字段中包含哪些字段,可以使用更短的表示法:

  1. curl -X GET "localhost:9200/twitter/_doc/1?_source=user,message&pretty"

此外,我们可以使用 /{index}/_source/{id} 模式来单独获取文档的 _source 字段信息,如下示例:

  1. curl -X GET "localhost:9200/twitter/_source/1?pretty"

Multi Get

如果想通过一次请求获取多个文档,可以使用 Multi Get API 根据索引名、类型名(可选的)和 id 一次获取多个文档,返回一个文档数组,其中包含所有获取的文档,其顺序与原始的 multi-get 请求相对应。如果某个特定的 get 请求失败,则在响应中包含一个包含此错误的对象。

Multi Get API 可以减少网络连接所产生的开销,提高性能。

  1. curl -X GET "localhost:9200/_mget?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "docs" : [
  4. {
  5. "_index" : "twitter",
  6. "_type" : "_doc",
  7. "_id" : "0"
  8. },
  9. {
  10. "_index" : "twitter",
  11. "_type" : "_doc",
  12. "_id" : "1"
  13. }
  14. ]
  15. }'

如果要查询的文档都在同一索引下,则可以简化写法:

  1. curl -X GET "localhost:9200/twitter/_doc/_mget?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "docs" : [
  4. {
  5. "_id" : "0"
  6. },
  7. {
  8. "_id" : "1"
  9. }
  10. ]
  11. }'

还可以进一步简化:

  1. curl -X GET "localhost:9200/twitter/_doc/_mget?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "ids" : ["0", "1"]
  4. }'

Update

Update API 相比 Index API 不会删除原来的文档,而是实现真正的数据更新。如下示例,我们向文档中新增一个 age 字段,doc 中的文档会被合并到现有文档中:

  1. curl -X POST "localhost:9200/twitter/_update/1?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "doc" : {
  4. "age" : "22"
  5. }
  6. }'

再次查询可以看到,新增的字段已经合并到原文档中了,并且版本号也加一。

  1. {
  2. "_index" : "twitter",
  3. "_type" : "_doc",
  4. "_id" : "1",
  5. "_version" : 3,
  6. "_seq_no" : 3,
  7. "_primary_term" : 1,
  8. "found" : true,
  9. "_source" : {
  10. "user" : "kimchy",
  11. "post_date" : "2009-11-15T14:12:12",
  12. "message" : "trying out Elasticsearch",
  13. "age" : "22"
  14. }
  15. }

1. Upsert

当使用 Update API 更新文档时,文档必须存在,否则会返回文档不存在的异常信息。如果开启了 upsert,当文档存在时,正常执行 update 操作;当文档不存在时,则将 upsert 元素的内容作为一个新文档插入。

如下示例,id 为 0 的文档不存在,此时会将 upsert 中的字段插入到 id 为 0 的这个新文档中去

  1. curl -X POST "localhost:9200/twitter/_update/0?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "doc" : {
  4. "age" : "22"
  5. },
  6. "upsert" : {
  7. "name" : "new doc"
  8. }
  9. }'

通过 Get API 查看该文档内容如下:

  1. {
  2. "_index" : "twitter",
  3. "_type" : "_doc",
  4. "_id" : "0",
  5. "_version" : 1,
  6. "_seq_no" : 4,
  7. "_primary_term" : 1,
  8. "found" : true,
  9. "_source" : {
  10. "name" : "new doc"
  11. }
  12. }

如果不想发送一个 doc 和一个 upsert,可以设置 doc_as_upsert 为 true 来将 doc 的内容作为 upsert 的内容,如下示例:

  1. curl -X POST "localhost:9200/twitter/_update/2?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "doc" : {
  4. "age" : "22"
  5. },
  6. "doc_as_upsert" : true
  7. }'

2. Update By Query

在现有索引的基础上进行查询更新,比如我们写入了一些文档后更新了索引的 Mapping 定义,新增了几个字段类型,但这些字段只会在后续新增的文档上生效,先前写入的文档是不会更新的。

此时我们可以使用 _update_by_query 对文档进行更新,使其重新索引(并不是更新)

  1. curl -X POST "localhost:9200/twitter/_update_by_query"

_update_by_query 内部会通过查询获得一个索引的快照,然后使用内部版本号对找到的文档重新进行索引。因此,如果在获取快照和处理索引请求之间文档发生了更改,则会出现版本冲突。当版本号匹配时,会更新文档并且版本号加一。

所有更新和查询的失败都会导致 _update_by_query 中断,并且已执行过的更新不会回滚。当第一个失败导致中止时,返回的所有失败都将在 failures 元素中返回。因此可能存在相当多的失败实体。所以,在生产环境中要考虑这方面的情况,并为文档增加相关的字段进行版本跟踪。

Delete

Delete API 允许根据 JSON 文档的 id 从特定索引中删除 JSON 文档。如下示例:

  1. curl -X DELETE "localhost:9200/twitter/_doc/2?pretty"

返回结果如下:

  1. {
  2. "_index" : "twitter",
  3. "_type" : "_doc",
  4. "_id" : "2",
  5. "_version" : 2,
  6. "result" : "deleted",
  7. "_shards" : {
  8. "total" : 2,
  9. "successful" : 1,
  10. "failed" : 0
  11. },
  12. "_seq_no" : 6,
  13. "_primary_term" : 1
  14. }

索引的每个文档都有版本控制。当删除一个文档时,可以指定版本,以确保我们试图删除的相关文档实际上正在被删除,同时它没有发生改变。对文档执行的每个写操作(包括删除操作)都会导致其版本递增。删除后,已删除文档的版本号在短时间内保持可用,以便控制并发操作。

通过 _delete_by_query 支持按查询删除,将会对匹配查询的每个文档进行删除。如下示例:

  1. curl -X POST "localhost:9200/twitter/_delete_by_query?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "query": {
  4. "match": {
  5. "age": "22"
  6. }
  7. }
  8. }'

返回结果如下:

  1. {
  2. "took" : 12,
  3. "timed_out" : false,
  4. "total" : 1,
  5. "deleted" : 1,
  6. "batches" : 1,
  7. "version_conflicts" : 0,
  8. "noops" : 0,
  9. "retries" : {
  10. "bulk" : 0,
  11. "search" : 0
  12. },
  13. "throttled_millis" : 0,
  14. "requests_per_second" : -1.0,
  15. "throttled_until_millis" : 0,
  16. "failures" : [ ]
  17. }

Bulk API

如果文档数量非常大,一个一个操作文档显然不太符合实际,为此 Elasticsearch 提供了 Bulk API 用于文档的批量操作。Bulk API 支持在一次 API 调用中,对不同的索引进行操作,从而减少了网络开销,极大提升了文档索引性能。Bulk API 支持四种操作类型,分别为:index、create、update、delete。

Bulk API 的请求体中可以写入多个请求操作,请求的格式如下:

  1. action_and_meta_data\n
  2. optional_source\n
  3. action_and_meta_data\n
  4. optional_source\n
  5. ....
  6. action_and_meta_data\n
  7. optional_source\n

每一行的结尾处都必须有换行字符 “\n”,最后一行也要有,换行符可以有效地分隔每行。另外,这些行里不能包含非转义字符,以免干扰数据解析。

action_and_meta_data 行指定了将要在哪个文档中执行什么操作,其中 action 必须是 index、create、update 或者 delete,而 meta_data 要指明需要被操作文档的 _index 以及 _id 信息。

例如创建文档命令就可以这样填写:

  1. { "index" : { "_index" : "test", "_id" : "1" } }
  2. { "name" : "allen" }

也可以这样写:

  1. { "create" : { "_index" : "test", "_id" : "1" } }
  2. { "name" : "allen" }

这两种写法的区别是如果文档 test/_doc/1 已存在,则 create 会创建失败,而 index 不会,因为 index 会先删除再新增。如果不设置文档 id 的话,Elasticsearch 会自动为其创建,下面这种省略 id 的写法也是可行的:

{ "index" : { "_index" : "test" } }
{ "name" : "allen" }

请求示例:

curl -X POST "localhost:9200/_bulk?pretty" -H 'Content-Type: application/json' -d'
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }'

Bulk API 请求返回的响应结果是一个数组,包含了每一个请求的结果(即便请求失败),结果的顺序与请求的顺序是相同的,如果其中的单条操作失败,是不会影响其他操作的。

使用 Bulk 操作需注意一次提交请求的大小,因为整个批量请求需要被加载到接受请求节点的内存里,所以批量请求的大小越大,留给其他请求可用的内存就越小。有一个最佳的 Bulk 请求大小,超过这个大小,性能不再提升而且可能降低。这个最佳大小完全取决于服务器的硬件、文档的大小和复杂度以及索引和搜索的负载。一个好的批次大小最好应保持在 5~15MB 之间。

Routing

Elasticsearch 是一个分布式系统,当索引一个文档时文档会被存储到 master 节点上的一个主分片上。但具体是存储在分片 1 还是分片 2 上取决于 Elasticsearch 的路由机制。Elasticsearch 的路由机制即是通过哈希算法,将具有相同哈希值的文档放置到同一个主分片中,分片位置计算方法:

shard = hash(routing) % number_of_primary_shards

Elasticsearch 默认将文档的 id 值作为 routing 值,通过哈希函数然后除以主切片的数量得到一个余数,余数的范围永远是 0 到 number_of_primary_shards-1,这个数字就是特定文档所在的分片。这种算法基本上会保持所有数据在所有分片上的一个平均分布,而不会造成数据倾斜。

我们也可以自定义 routing 值。虽然默认的路由模式可以保证数据平均分布,很多时候性能也不是问题。但自定义 routing 值在深入理解数据特征之后,能够带来很多使用上的方便和性能上的提升。 假设存在一个有 50 个分片的索引,在集群上执行一次查询的过程如下:

  • 查询请求首先被集群中的一个节点接收。
  • 接收到这个请求的节点,将这个查询广播到这个索引的每个分片上。
  • 每个分片执行完搜索查询并返回结果。
  • 结果在通道节点上合并、排序并返回给用户。

默认情况下,Elasticsearch 使用文档的 id 将文档平均分布于所有的分片上,这导致了 Elasticsearch 不能确定文档的位置,所以在执行 _search 时它必须将这个请求广播到所有的 50 个分片上去执行。主分片的数量在索引创建时是固定的且永远不能改变。因为如果分片的数量改变了,所有先前的路由值就会变成非法,文档相当于丢失了。而使用自定义的路由模式,可以便查询更具目的性。我们可以直接告诉 Elasticsearch 你的数据分布在哪个分片上,而不必盲目地去广播查询请求。

Elasticsearch 的 index、get、mget、delete、update 等文档 API 都可以接收一个 routing 参数,以索引文档为例,执行 index 操作时可以给文档设置一个 routing 参数,如果用户设置了该参数,Elasticsearch 会使用该参数值来计算分片位置,具有相同 routing 的文档会被分配到同一个分片上。示例如下:

curl -X PUT "localhost:9200/twitter/_doc/1?routing=kimchy&pretty" -H 'Content-Type: application/json' -d'
{
    "user" : "kimchy",
    "post_date" : "2009-11-15T14:12:12",
    "message" : "trying out Elasticsearch"
}'

上述命令索引了一条文档到 Elasticsearch 中,并指定 routing 为 kimchy。此时,如果想要查询 kimchy 关联的文档,可以通过 routing 值进行过滤,这样可以避免 Elasticsearch 向所有分片都发送查询请求,大大减少了系统的资源消耗。带 routing 参数的查询命令如下:

curl -X GET "localhost:9200/twitter/_search?routing=kimchy"

需要注意的是,可以为文档指定多个路由值,路由值之间使用逗号隔开。

使用自定义 routing 值也会造成一些潜在问题,因为这意味着数据无法像默认情况那么均匀的分配到各分片和各节点上,从而导致各节点存储和读写压力分布不均,影响系统的性能和稳定性。比如 user 名称为 kimchy 的文档本身就非常多,而其他大多数的用户只有几个文档,这样的话就会导致 kimchy 所在的分片较大,出现数据偏移的情况。对此可以从以下两个方面进行优化:

使用 index.routing_partition_size 参数
该参数可以使 routing 相同的文档分配到一批分片(集群分片的子集)而不是一个分片上,从而可以从一定程度上减轻数据倾斜的问题。该参数的效果与其值设置的大小有关,当该值等于 number_of_shards 时,routing 将退化为与未指定一样。当然该方法只能减轻数据倾斜,并不能彻底解决。在设置了该参数时,计算公式为:

shard = (hash(routing) + hash(id) % routing_partition_size) % number_of_primary_shards

routing 前使用自定义 hash 函数
很多情况下,用户并不能提前确定数据的分类值,为此可以在分类值和 routing 值之间设置一个 hash 函数,保证分类值散列后的值更均匀,使用该值作为 routing 从而防止数据倾斜。

Reindex API

Reindex API 的作用是将文档从一个索引复制到另一个索引,并且在重建过程中原索引仍支持搜索。一般有以下几种情况需要重建索引:

  • 索引的 Mapping 发生变更:原有字段类型、分词器变更
  • 索引的 Setting 发生变更:索引的主分片数变更
  • 不同集群间做数据迁移
curl -X POST "localhost:9200/_reindex?pretty" -H 'Content-Type: application/json' -d'
{
  "source": {
    "index": "twitter"
  },
  "dest": {
    "index": "new_twitter"
  }
}'

在使用 _reindex 时有两个注意点:

  • _source 元字段不能被禁用,必须是 enable 状态
  • 目标索引必须已经存在

默认情况下,_reindex 只会创建不存在的文档,如果文档已经存在则会导致版本冲突。此时,我们可以指定 op_type 为 create 的方式进行 Reindex:

curl -X POST "localhost:9200/_reindex?pretty" -H 'Content-Type: application/json' -d'
{
  "source": {
    "index": "twitter"
  },
  "dest": {
    "index": "new_twitter",
    "op_type": "create"
  }
}'