通过对前面内容的学习,我相信你的机器上已经成功安装上了 ES,并且对 ES 有个大概的了解了。现在是不是迫不及待想尝试一下 ES 提供的功能呢?其实 ES 官方提供了各种各样的功能接口,而我们今天先来学习操作文档的基本接口。如果需要了解更多的接口使用方式,可以参考官方文档。

文档基本操作接口有:

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

本文除了介绍上述的基本文档操作接口外,还会简单介绍索引的创建、删除操作。今天我们就以在线书店业务为例子来介绍上面的 API 吧。

一、索引管理

现阶段在线书店的业务很简单,我们的模型只需要记录书本的 ID、名字、作者、简介即可。所以我们可以定义如下 Mapping,并且创建索引:

  1. # 创建 books 索引
  2. PUT books
  3. {
  4. "mappings": {
  5. "properties": {
  6. "book_id": {
  7. "type": "keyword"
  8. },
  9. "name": {
  10. "type": "text"
  11. },
  12. "author": {
  13. "type": "keyword"
  14. },
  15. "intro": {
  16. "type": "text"
  17. }
  18. }
  19. },
  20. "settings": {
  21. "number_of_shards": 3,
  22. "number_of_replicas": 1
  23. }
  24. }
  25. # 返回结果
  26. {
  27. "acknowledged" : true,
  28. "shards_acknowledged" : true,
  29. "index" : "books"
  30. }

在 Kibana 中运行上述示例,可以创建 books 索引。books 索引包含 book_id(书本 ID)、name(书本名字)、author(作者)、intro(简介)四个字段,可以满足我们现阶段的需求。其中 books 索引还有 3 个分片和 1 个副本备份。

如果你之前创建过 books 索引的话,这里再次创建会报错,所以你需要先将之前的索引删除,然后再重新创建。在 Kibana 执行以下示例可以删除索引:

  1. # 删除索引 books
  2. DELETE books
  3. # 返回结果
  4. {
  5. "acknowledged" : true
  6. }

如上所示,acknowledged 为 true,则删除成功,如果返回的 http 状态码为 404,则说明这个索引本身不存在。

ok,在准备好索引后,我们开始学习文档基本操作的 API。

二、新建文档

这天书店进了一批电子书,管理员需要把这些书录入到 ES 中。ES 提供了两种创建文档的方式,一种是使用 Index API 索引文档,一种是使用 Create API 创建文档。

1. 使用 Index API 索引文档

  1. # 使用 Index API 索引文档
  2. PUT books/_doc/1
  3. {
  4. "book_id": "4ee82462",
  5. "name": "深入Linux内核架构",
  6. "author": "Wolfgang Mauerer",
  7. "intro": "内容全面深入,领略linux内核的无限风光。"
  8. }
  9. # 结果
  10. {
  11. "_index" : "books",
  12. "_type" : "_doc",
  13. "_id" : "1",
  14. "_version" : 1,
  15. "result" : "created",
  16. "_shards" : {
  17. "total" : 2,
  18. "successful" : 2,
  19. "failed" : 0
  20. },
  21. "_seq_no" : 0,
  22. "_primary_term" : 1
  23. }

如上示例,可以看到索引一个文档是比较简单的,将 Mapping 中对应的字段做成 Json Object 对应的 Key 即可,并且上面的例子中我们指定文档 ID 为 1。需要说明的是,在 ES 7.0 版本后 type 统一为 _doc。

如果我们索引的文档已经存在,会发生什么情况?你可以在不改变文档 ID 的情况下多次执行上面的索引文档的语句,可以发现系统并不会报错,而是将返回结果中 “_version” 字段的值自加。

其实在索引一个文档的时候,如果文档 ID 已经存在,会先删除旧文档,然后再写入新文档的内容,并且增加文档版本号

2. 使用 Create API 创建文档

使用 Create API 创建文档有两种写法:PUT 和 POST 方式,其示例分别如下:

  1. # 使用 PUT 的方式创建文档
  2. PUT books/_create/2
  3. {
  4. "book_id": "4ee82463",
  5. "name": "时间简史",
  6. "author": "史蒂芬霍金",
  7. "intro": "探索时间和空间核心秘密的引人入胜的故事。"
  8. }
  9. # PUT 方式返回的结果
  10. {
  11. "_index" : "books",
  12. "_type" : "_doc",
  13. "_id" : "2",
  14. "_version" : 1
  15. ......
  16. }
  17. # 使用 POST 的方式,不需要指定文档 ID, 系统自动生成
  18. POST books/_doc
  19. {
  20. "book_id": "4ee82464",
  21. "name": "时间简史(插画版)",
  22. "author": "史蒂芬霍金",
  23. "intro": "用精美的插画带你探索时间和空间的核心秘密"
  24. }
  25. # POST 方式返回的结果
  26. {
  27. "_index" : "books",
  28. "_type" : "_doc",
  29. "_id" : "LfwVtH0BxOuNtEd4yM4F",
  30. "_version" : 1
  31. ......
  32. }

如上示例,使用 PUT 的方式创建文档需要指定文档的 ID,如果文档 ID 已经存在,则返回 http 状态码为 409 的错误。而使用 POST 的方式创建文档时候,则不需要指定文档 ID,系统会自动创建,在返回结果中可以看到,新创建的文档的 ID 为 ‘LfwVtH0BxOuNtEd4yM4F’。

序号 语句 特性描述
1 PUT books/_doc/1 使用 Index API 索引文档,如果文档存在,会先删除然后再写入,即有覆盖原内容的功能。
2 PUT books/_create/2 Create API 中使用 PUT 的方式创建文档,需要指定文档 ID。如果文档已经存在,则返回 http 409 错误。
3 POST books/_doc Create API 中使用 POST 的方式,不需要指定文档 ID, 系统自动生成。

上表是新建文档时 3 种写法的总结,如果你有更新文档内容的需求,应该使用第一种方式。如果写入文档时有唯一性校验需求的话,应该使用第二种方式。如果需要系统为你创建文档 ID,应该使用第三种方式。相对于第一种方式来说,第三种方式写入的效率会更高,因为不需要在库里查询文档是否已经存在,并且进行后续的删除工作

三、获取文档

当用户需要通过书本 ID 获取书本信息的时候,可以使用 ES 提供的 GET API 来获取文档内容。获取文档有 2 种情况,一种是只获取一个文档内容,另一种是同时获取多个文档的内容。

1. 使用 GET API 获取单个文档

通过书本的 ID 获取书本的信息时可以使用 GET API 来实现,其示例如下:

  1. # 使用 GET API 获取单个文档的内容
  2. GET books/_doc/1
  3. # 结果
  4. {
  5. "_index" : "books",
  6. "_type" : "_doc",
  7. "_id" : "1",
  8. "_version" : 1,
  9. "_seq_no" : 0,
  10. "_primary_term" : 1,
  11. "found" : true,
  12. "_source" : {
  13. "book_id" : "4ee82462",
  14. "name" : "深入Linux内核架构",
  15. "author" : "Wolfgang Mauerer",
  16. "intro" : "内容全面深入,领略linux内核的无限风光。"
  17. }
  18. }

如上示例,GET API 是非常简单的,使用时只需要指定文档 ID 即可。文档的原生内容保存在 “_source” 字段中,其他字段是这个文档的元数据。如果成功,返回的是 http 状态码为 200,如果文档不存在则 http 状态码为 404,并且 found 字段为 false。

GET API 提供了多个参数,更多的信息可以参考官方文档,下面是几个比较常用的:

参数 简介
preference 默认的情况下,GET API 会从多个副本中随机挑选一个,设置 preference 参数可以控制 GET 请求被路由到哪个分片上执行。
realtime 控制 GET 请求是实时的还是准实时的,默认为 true。
refresh 是否在执行 GET 操作前执行 refresh(后续我们有介绍),默认为 false。
routing 自定义 routing key。
stored_fields 返回在 Mapping 中 store 设置为 true 的字段,而不是 _source。默认为 false。
_source 指定是否返回 _source 的字段,或者设置某些需要返回的字段。
_source_excludes 不返回哪些字段,逗号分割的字符串列表。如果 _source 设置为 false,此参数会被忽略。
_source_includes 返回哪些字段,逗号分割的字符串列表。如果 _source 设置为 false,此参数会被忽略。
version 指定版本号,如果获取的文档的版本号与指定的不一样,返回 http 409。

2. 使用 MGET API 获取多个文档

当我们需要通过多个文档 ID 同时获取它们的信息时,如果使用 GET API 的话,需要发起多个请求,这样做效率比较低下。可以使用 ES 提供的 MGET API 来解决这个需求,MGET API 的请求格式有 3 种,其示例如下:

  1. # 1:在 body 中指定 index
  2. GET /_mget
  3. {
  4. "docs": [
  5. { "_index": "books", "_id": "1" },
  6. { "_index": "books", "_id": "2" }
  7. ]
  8. }
  9. # 2:直接指定 index
  10. GET /books/_doc/_mget
  11. {
  12. "docs": [
  13. { "_id": "1" },
  14. { "_id": "2" }
  15. ]
  16. }
  17. # 3:也可以简写为一下例子
  18. GET /books/_mget
  19. {
  20. "ids" : ["1", "2"]
  21. }
  22. # 结果
  23. {
  24. "docs" : [
  25. {
  26. "found" : true,
  27. "_source" : {
  28. "book_id" : "4ee82462",
  29. "name" : "深入Linux内核架构"
  30. ......
  31. }
  32. },
  33. {
  34. "found" : true,
  35. "_source" : {
  36. "book_id" : "4ee82463",
  37. "name" : "时间简史",
  38. ......
  39. }
  40. }
  41. ]
  42. }

如上示例,如果在 body 中指定 index,可以获取多个索引中的文档数据,比较灵活。而使用直接指定 index 的方式只能获取指定索引中的文档。更多的 MGET API 使用例子可以参考官方文档。同样,如果对应的文档找不到,found 字段为 false。

四、更新文档

有时候发现一些书籍的信息有误,需要进行修改,ES 提供了 Update API 来更新文档信息,我们可以通过这个接口来更新书本的信息。

更新一个文档,需要指定文档的 ID 和需要更新的字段与其对应的值。Update API 使用如下:

  1. # 更新文档
  2. POST books/_update/2
  3. {
  4. "doc": {
  5. "name":"时间简史(视频版)",
  6. "intro": "探索时间和空间核心秘密的引人入胜的视频故事。"
  7. }
  8. }
  9. #结果
  10. {
  11. "_index" : "books",
  12. "_type" : "_doc",
  13. "_id" : "2",
  14. "_version" : 3,
  15. "result" : "updated",
  16. ......
  17. }

上述示例更新了文档 2 的 name 和 intro 字段。如返回结果所示,版本号会增加,”result” 字段为 updated。

上面提到过,索引文档的方式也有更新数据的效果,那它跟文档更新接口有啥区别呢?其实索引文档的更新效果是先删除数据,然后再写入新数据。所以索引文档的方式会覆盖旧的数据,使其无法实现只更新某些字段的需求。

除了使用指定 ID 的方式来更新数据外,还可以用 update_by_query 的方式:

  1. POST books/_update_by_query
  2. {
  3. "query": {
  4. "term": {
  5. "book_id": {
  6. "value": "4ee82462"
  7. }
  8. }
  9. },
  10. "script": {
  11. "source": "ctx._source.name='深入Linux内核架构1'",
  12. "lang": "painless"
  13. }
  14. }

如上示例,我们将 book_id 为 ‘4ee82462’ 的文档的 name 字段进行了更新。我们可以使用 DSL 中 ‘script’ 字段来指定一段脚本。脚本中 ‘ctx._source’ 可以拿到匹配的文档的数据,所以 ctx._source.name=’深入Linux内核架构1’ 的意思是将匹配文档的 ‘name’ 字段的数据设置为 ‘深入Linux内核架构1’。

需要注意的是,如果 query 中匹配的文档数量巨大,那么这个接口在 kibana 中执行的时候可能会发生超时的错误,当然你可以在请求中使用 timeout 参数,但是这个不是解决问题的有效方案。我建议你使用异步的方式进行处理,并且控制需要更新的数据量,例如按日期分批来进行更新。使用异步方式的实例如下:

  1. POST books/_update_by_query?wait_for_completion=false
  2. {
  3. "query": {
  4. "term": {
  5. "book_id": {
  6. "value": "4ee82462"
  7. }
  8. }
  9. },
  10. "script": {
  11. "source": "ctx._source.name='深入Linux内核架构1'",
  12. "lang": "painless"
  13. }
  14. }
  15. 结果:
  16. {
  17. "task" : "R8Zd-p4GRXmBovdOQaOyqA:29677918"
  18. }

如上示例,异步的方式返回了一个 task id,然后可以用这个 task id 查询任务的执行情况:

  1. GET /_tasks/R8Zd-p4GRXmBovdOQaOyqA:29677918

_update_by_query 的接口参数有很多,更详细的使用示例请参考官方文档

五、删除文档

当书店不再拥有某本书的版权时,我们需要对其进行删除(现实业务一般是用下架,这里只是举例子啦)。ES 提供了 Delete API 来删除一个文档,删除一个文档是非常简单的,只需要指定索引和文档 ID 即可。Delete API 使用如下:

  1. # 删除文档 2
  2. DELETE books/_doc/2
  3. # 结果
  4. {
  5. "_index" : "books",
  6. "_type" : "_doc",
  7. "_id" : "2",
  8. "_version" : 4,
  9. "result" : "deleted",
  10. ......
  11. }

如上示例,如果文档存在则删除成功,”result” 字段为 “deleted”。如果文档本身不存在,则返回 http 的状态码为 404。

除了指定文档 ID 进行文档删除外,我们还可以使用 Delete By Query API 进行查询删除。

  1. # 使用 Delete By Query API 删除文档
  2. POST /books/_delete_by_query
  3. {
  4. "query": {
  5. "term": {
  6. "book_id": "4ee82462"
  7. }
  8. }
  9. }
  10. # 结果
  11. {
  12. "total" : 1,
  13. "deleted" : 1,
  14. ......
  15. "failures" : [ ]
  16. }

如上示例的返回结果中,”deleted” 字段为 1,即删除了 1 个文档。至于查询的内容是什么意思,我们在后续的内容中将会有详细介绍。现在你只需要知道,”term” 字段下面填写文档的对应的字段和需要查询的值即可,本示例中我们查询了书本 ID 为 “4ee82462” 的文档。

六、批量操作文档

当我们需要写入多个文档的时候,如果每写一个文档就发起一个请求的话,多少有点浪费。这个时候我们可以使用 Bulk API 来批量处理文档

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

  • Index
  • Create
  • Update
  • Delete

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

  1. POST _bulk
  2. # 第一行指定操作类型和元数据(索引、文档id等)
  3. { "index" : { "_index" : "books", "_id" : "1" } }
  4. # 紧接着的一行是这个操作的内容(文档数据,如果有的话。像简单的删除就没有)
  5. { "book_id": "4ee82462","name": "深入Linux内核架构", ......}

下面示例是在 Bulk API 中同时使用多种操作类型的例子:

  1. # 在 Bulk API 中同时使用多种操作类型的实例
  2. POST _bulk
  3. { "index" : { "_index" : "books", "_id" : "1" } }
  4. { "book_id": "4ee82462","name": "深入Linux内核架构","author": "Wolfgang Mauerer","intro": "内容全面深入,领略linux内核的无限风光。" }
  5. { "delete" : { "_index" : "books", "_id" : "2" } }
  6. { "create" : { "_index" : "books", "_id" : "3" } }
  7. { "book_id": "4ee82464","name": "深入Linux内核架构第三版","author": "Wolfgang Mauerer","intro": "内容全面深入,再次领略linux内核的无限风光。" }
  8. { "update" : {"_index" : "books", "_id" : "4"} } # 指定操作类型、索引、文档 id
  9. { "doc" : {"intro" : "书本的内容非常好,值得一看"} } # 指定文档内容
  10. # 结果
  11. {
  12. "items" : [
  13. {
  14. "index" : {
  15. "_id" : "1",
  16. "result" : "created",
  17. ......
  18. }
  19. },
  20. {
  21. "delete" : {
  22. "_id" : "2",
  23. "result" : "deleted",
  24. ......
  25. }
  26. },
  27. {
  28. "create" : {
  29. "_id" : "3",
  30. "result" : "created",
  31. ......
  32. }
  33. },
  34. {
  35. "update" : {
  36. "_id" : "4",
  37. "status" : 404,
  38. ......
  39. }
  40. }
  41. ]
  42. }

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

更详细 Bulk API 使用方式,可以参考官方文档

七、总结

今天为你介绍了文档的基础操作,包括新建文档、获取文档、更新文档、删除文档、批量操作文档,这些内容都比较简单,相信你都已经掌握了。下面就来总结一下今天的内容吧。

索引的创建比较简单,我们要先定义好业务需要的 JSON,然后构建 Mapping,最后创建索引。

新建文档可以有多种方式,使用 Index API 可以索引一个文档,如果文档已经存在,则先删除文档,然后再写入新的数据。另外还可以使用 Create API 来创建文档,使用 PUT 的方式创建文档时需要指定文档 ID,如果文档已经存在则写入失败。而使用 POST 的方式则不需要指定文档 ID, 系统自动生成。

已经写入的文档可以通过 GET API 来获取,如果需要同时获取多个文档内容,可以使用 MGET API 来实现。

当我们需要更新、删除文档的时候,可以使用 Update API 和 Delete API。最后我们介绍了 Bulk API 是如何批量操作文档的,Bulk API 支持 4 种类型的操作在一个请求中完成,可以提高了执行效率。

最后,如果大家对这些 API 的使用还有点生疏的话,可以参考官方文档多实操几次。

好了,今天的内容到此为止。欢迎你在留言区与我分享你的想法,我们一起交流、一起进步。