暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Elasticsearch学习-嵌套文档

dalaoyang 2020-05-05
322

本文以Elasticsearch 6.8.4版本为例,介绍Elasticsearch嵌套文档的使用。

最近一段时间都在搞Elasticsearch搜索相关的工作,总结一下搜索知识点供大家参考。

在Elasticsearch取消了多个索引内创建多个type的机制,由于场景需要,所以调研了嵌套文档父子文档

以文章和文章留言为例,嵌套文档都在一个文档内,而父子文档则分开存储了父文档与子文档,本文我们来学习嵌套文档的使用。

1、嵌套文档

嵌套文档看似与文档内有一个集合字段类似,但是实则有很大区别,以上面图中嵌套文档为例,留言1,留言2,留言3虽然都在当前文章所在的文档内,但是在内部其实存储为4个独立文档,如下图所示。

同时,嵌套文档的字段类型需要设置为nested,设置成nested后的不能被直接查询,需要使用nested查询,这里不做具体介绍,详细查看1.2。

1.1 创建索引

接下来,介绍一下如何创建嵌套文档索引,比如有这样的数据,如下:

  1. {

  2. "title": "这是一篇文章",

  3. "body": "这是一篇文章,从哪里说起呢?... ...",

  4. "comments": [

  5. {

  6. "name": "张三",

  7. "comment": "写的不错",

  8. "age": 28,

  9. "date": "2020-05-04"

  10. },

  11. {

  12. "name": "李四",

  13. "comment": "写的很好",

  14. "age": 20,

  15. "date": "2020-05-04"

  16. },

  17. {

  18. "name": "王五",

  19. "comment": "这是一篇非常棒的文章",

  20. "age": 31,

  21. "date": "2020-05-01"

  22. }

  23. ]

  24. }

复制

创建索引名和type均为blog的索引,其中comments字段为嵌套文档类型,需要将type设置为nested,其余都是一些正常的字段,创建索引语句如下:

PUT http://localhost:9200/blog/

  1. {

  2. "mappings": {

  3. "blog": {

  4. "properties": {

  5. "comments": {

  6. "type": "nested",

  7. "properties": {

  8. "date": {

  9. "type": "date"

  10. },

  11. "name": {

  12. "type": "text",

  13. "fields": {

  14. "keyword": {

  15. "type": "keyword"

  16. }

  17. }

  18. },

  19. "comment": {

  20. "type": "text",

  21. "fields": {

  22. "keyword": {

  23. "type": "keyword"

  24. }

  25. }

  26. },

  27. "age": {

  28. "type": "long"

  29. }

  30. }

  31. },

  32. "body": {

  33. "type": "text",

  34. "fields": {

  35. "keyword": {

  36. "type": "keyword"

  37. }

  38. }

  39. },

  40. "title": {

  41. "type": "text",

  42. "fields": {

  43. "keyword": {

  44. "type": "keyword"

  45. }

  46. }

  47. }

  48. }

  49. }

  50. }

  51. }

复制

如下图所示

1.2 插入数据

将1.1中示例的数据插入blog索引,对嵌套文档来说,插入没什么特别的,如下:

PUT http://localhost:9200/blog/blog/1/

  1. {

  2. "title":"这是一篇文章",

  3. "body":"这是一篇文章,从哪里说起呢?... ...",

  4. "comments":[

  5. {

  6. "name":"张三",

  7. "comment":"写的不错",

  8. "age":28,

  9. "date":"2020-05-04"

  10. },

  11. {

  12. "name":"李四",

  13. "comment":"写的很好",

  14. "age":20,

  15. "date":"2020-05-04"

  16. },

  17. {

  18. "name":"王五",

  19. "comment":"这是一篇非常棒的文章",

  20. "age":31,

  21. "date":"2020-05-01"

  22. }

  23. ]

  24. }

复制

如图所示:

1.3 查询

在前面说到,使用嵌套文档时,直接查询nested文档时查询不到的,这里试一下,先查询一下根文档的内容(文章内容),查询title包含‘文章’的内容:

POST http://localhost:9200/blog/blog/_search/

  1. {

  2. "query": {

  3. "bool": {

  4. "filter": [

  5. {

  6. "bool": {

  7. "must": [

  8. {

  9. "match_phrase": {

  10. "title": {

  11. "query": "文章"

  12. }

  13. }

  14. }

  15. ]

  16. }

  17. }

  18. ]

  19. }

  20. }

  21. }

复制

Elasticsearch-Head,如下图所示

接下来我们查询一下,留言中name为张三的数据,查询如下:

  1. {

  2. "query": {

  3. "bool": {

  4. "filter": [

  5. {

  6. "bool": {

  7. "must": [

  8. {

  9. "match_phrase": {

  10. "comments.name": {

  11. "query": "张三"

  12. }

  13. }

  14. }

  15. ]

  16. }

  17. }

  18. ]

  19. }

  20. }

  21. }

复制

Elasticsearch-Head 如下图所示

这里举例,我们要查询title中包含‘文章’且留言name中包含‘张三’的数据,使用如下查询:

POST http://localhost:9200/blog/blog/_search/

  1. {

  2. "query": {

  3. "bool": {

  4. "must": [

  5. {

  6. "match": {

  7. "title": "文章"

  8. }

  9. },

  10. {

  11. "nested": {

  12. "path": "comments",

  13. "query": {

  14. "bool": {

  15. "must": [

  16. {

  17. "match": {

  18. "comments.name": "张三"

  19. }

  20. }

  21. ]

  22. }

  23. }

  24. }

  25. }

  26. ]

  27. }

  28. }

  29. }

复制

Elasticsearch-Head 如下图所示

其实从查询语句中可以看出,nested中查询的是嵌套文档的内容,语法与正常查询时一致。

使用嵌套文档时,文档的分数计算需要注意,参考官方文档的描述:

nested 查询肯定可以匹配到多个嵌套的文档。每一个匹配的嵌套文档都有自己的相关度得分,但是这众多的分数最终需要汇聚为可供根文档使用的一个分数。

默认情况下,根文档的分数是这些嵌套文档分数的平均值。可以通过设置 score_mode 参数来控制这个得分策略,相关策略有 avg (平均值), max (最大值), sum (加和) 和 none (直接返回 1.0 常数值分数)。

1.4 排序

可能有一些场景需要按照嵌套文档的字段记性排序,举例:

为了符合上述场景,新增两条数据:

PUT http://localhost:9200/blog/blog/2/

  1. {

  2. "title": "这是一篇文章2",

  3. "body": "这是一篇文章2,从哪里说起呢?... ...",

  4. "comments": [

  5. {

  6. "name": "张三",

  7. "comment": "写的不错",

  8. "age": 28,

  9. "date": "2020-05-11"

  10. },

  11. {

  12. "name": "李四",

  13. "comment": "写的很好",

  14. "age": 20,

  15. "date": "2020-05-16"

  16. },

  17. {

  18. "name": "王五",

  19. "comment": "这是一篇非常棒的文章",

  20. "age": 31,

  21. "date": "2020-05-01"

  22. }

  23. ]

  24. }

复制

PUT http://localhost:9200/blog/blog/3/

  1. {

  2. "title": "这是一篇文章3",

  3. "body": "这是一篇文章3,从哪里说起呢?... ...",

  4. "comments": [

  5. {

  6. "name": "张三",

  7. "comment": "写的不错",

  8. "age": 28,

  9. "date": "2020-05-03"

  10. },

  11. {

  12. "name": "李四",

  13. "comment": "写的很好",

  14. "age": 20,

  15. "date": "2020-05-20"

  16. },

  17. {

  18. "name": "王五",

  19. "comment": "这是一篇非常棒的文章",

  20. "age": 31,

  21. "date": "2020-05-01"

  22. }

  23. ]

  24. }

复制

查询title中包含‘文章’且留言name中包含‘张三’,并且按照留言date字段倒序排序,查询语句如下:

  1. {

  2. "query": {

  3. "bool": {

  4. "must": [

  5. {

  6. "match": {

  7. "title": "文章"

  8. }

  9. },

  10. {

  11. "nested": {

  12. "path": "comments",

  13. "query": {

  14. "bool": {

  15. "must": [

  16. {

  17. "match": {

  18. "comments.name": "张三"

  19. }

  20. }

  21. ]

  22. }

  23. }

  24. }

  25. }

  26. ]

  27. }

  28. },

  29. "sort": {

  30. "comments.date": {

  31. "order": "desc",

  32. "mode": "max",

  33. "nested_path": "comments",

  34. "nested_filter": {

  35. "bool": {

  36. "must": [

  37. {

  38. "match": {

  39. "comments.name": "张三"

  40. }

  41. }

  42. ]

  43. }

  44. }

  45. }

  46. }

  47. }

复制

需要注意的是,在sort内,又添加了nested_filter来过滤一遍上面嵌套文档的查询条件,原因是这样的,在嵌套文档查询排序时是先按照条件进行查询,查询后再进行排序,那么可能由于数据的原因,导致排序的字段不是按照匹配上的数据进行排序,比如这是本文正确的结果,如下图所示(为了方便查看,使用图表展示的数据)。

如果我们去掉nestedfilter,在查询,由于文章3中李四评论的日期是20号,导致这条记录排在了最前面,这就是为什么使用nestedfilter的原因,查询结果如下:

1.5 聚合

聚合的场景可能也比较常见,其实熟悉上面嵌套文档的使用的话,对聚合文档使用难度应该也不大,

新增一条数据:

PUT http://localhost:9200/blog/blog/4/

  1. {

  2. "title": "这是一篇文章4",

  3. "body": "这是一篇文章4,从哪里说起呢?... ...",

  4. "comments": [

  5. {

  6. "name": "张三",

  7. "comment": "写的不错",

  8. "age": 28,

  9. "date": "2020-03-03"

  10. },

  11. {

  12. "name": "李四",

  13. "comment": "写的很好",

  14. "age": 20,

  15. "date": "2020-04-20"

  16. },

  17. {

  18. "name": "王五",

  19. "comment": "这是一篇非常棒的文章",

  20. "age": 31,

  21. "date": "2020-06-01"

  22. }

  23. ]

  24. }

复制

举例:需要查询每个月评论人数的平均数,查询语句如下:

POST http://localhost:9200/blog/blog/_search/

  1. {

  2. "size": 0,

  3. "aggs": {

  4. "comments": {

  5. "nested": {

  6. "path": "comments"

  7. },

  8. "aggs": {

  9. "by_month": {

  10. "date_histogram": {

  11. "field": "comments.date",

  12. "interval": "month",

  13. "format": "yyyy-MM"

  14. },

  15. "aggs": {

  16. "avg_stars": {

  17. "avg": {

  18. "field": "comments.age"

  19. }

  20. }

  21. }

  22. }

  23. }

  24. }

  25. }

  26. }

复制

结果如下图所示:

1.6 使用建议

  • 正如本文所说,嵌套文档中,所有内容都在同一个文档内,这就导致嵌套文档进行增加、修改或者删除时,整个文档都要重新被索引。嵌套文档越多,这带来的成本就越大。当时就是由于这个原因,最终没有选择使用嵌套文档。

  • 嵌套文档的分数计算问题需要注意,可以参考本文1.3最后部分。


文章转载自dalaoyang,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论