干货 | Elasticsearch Nested类型深入详解

0、概要

在Elasticsearch实战场景中,我们或多或少会遇到嵌套文档的组合形式,反映在ES中称为父子文档。 父子文档的实现,至少包含以下两种方式: 1)父子文档 父子文档在5.X版本中通过parent-child父子type实现,即:1个索引对应多个type; 6.X+版本已经不再支持一个索引多个type,6.X+的父子索引的实现改成Join。 2)Nested嵌套类型

本文通过一个例子将Nested类型适合解决的问题、应用场景、使用方法串起来, 文中所有的DSL都在Elasticsearch6.X+验证通过。

1、Elasticsearch 数据类型全景概览

2、从一个例子说起吧

2.1 问题背景

在elasticsearch中,我们可以将密切相关的实体存储在单个文档中。 例如,我们可以通过传递一系列评论来存储博客文章及其所有评论。

举例:

 1{
 2  "title": "Invest Money",
 3  "body": "Please start investing money as soon...",
 4  "tags": ["money", "invest"],
 5  "published_on": "18 Oct 2017",
 6  "comments": [
 7    {
 8      "name": "William",
 9      "age": 34,
10      "rating": 8,
11      "comment": "Nice article..",
12      "commented_on": "30 Nov 2017"
13    },
14    {
15      "name": "John",
16      "age": 38,
17      "rating": 9,
18      "comment": "I started investing after reading this.",
19      "commented_on": "25 Nov 2017"
20    },
21    {
22      "name": "Smith",
23      "age": 33,
24      "rating": 7,
25      "comment": "Very good post",
26      "commented_on": "20 Nov 2017"
27    }
28  ]
29}

如上所示,所以我们有一个文档描述了一个帖子和一个包含帖子上所有评论的内部对象评论。

但是Elasticsearch搜索中的内部对象并不像我们期望的那样工作。

2.2 问题出现

现在假设我们想查找用户{name:john,age:34}评论过的所有博客帖子。 让我们再看一下上面的示例文档,找到评论过的用户。

name

age

William

34

John

38

Smith

33

从列表中我们可以清楚地看到,没有34岁的用户John。

为简单起见,我们在elasticsearch索引中只有1个文档。

让我们通过查询索引来验证它:

 1GET /blog/_search?pretty
 2{
 3  "query": {
 4    "bool": {
 5      "must": [
 6        {
 7          "match": {
 8            "comments.name": "John"
 9          }
10        },
11        {
12          "match": {
13            "comments.age": 34
14          }
15        }
16      ]
17    }
18  }
19}

我们的示例文档作为回复返回。 很惊讶,这是为什么呢?

2.3 原因分析

这就是为什么我说:elasticsearch中的内部对象无法按预期工作。

这里的问题是elasticsearch(lucene)使用的库没有内部对象的概念,因此内部对象被扁平化为一个简单的字段名称和值列表。

我们的文档内部存储为:

 1{
 2  "title":                    [ invest, money ],
 3  "body":                     [ as, investing, money, please, soon, start ],
 4  "tags":                     [ invest, money ],
 5  "published_on":             [ 18 Oct 2017 ]
 6  "comments.name":            [ smith, john, william ],
 7  "comments.comment":         [ after, article, good, i, investing, nice, post, reading, started, this, very ],
 8  "comments.age":             [ 33, 34, 38 ],
 9  "comments.rating":          [ 7, 8, 9 ],
10  "comments.commented_on":    [ 20 Nov 2017, 25 Nov 2017, 30 Nov 2017 ]
11}

如上,您可以清楚地看到,comments.name和comments.age之间的关系已丢失。

这就是为什么我们的文档匹配john和34的查询。

2.4 如何解决呢?

要解决这个问题,我们只需要对elasticsearch的映射进行一些小改动。

如果您查看索引的映射,您会发现comments字段的类型是object。 我们需要更新它的类型为nested。

我们可以通过运行以下查询来简单地更新索引的映射:

 1PUT /blog_new
 2{
 3  "mappings": {
 4    "blog": {
 5      "properties": {
 6        "title": {
 7          "type": "text"
 8        },
 9        "body": {
10          "type": "text"
11        },
12        "tags": {
13          "type": "keyword"
14        },
15        "published_on": {
16          "type": "keyword"
17        },
18        "comments": {
19          "type": "nested",
20          "properties": {
21            "name": {
22              "type": "text"
23            },
24            "comment": {
25              "type": "text"
26            },
27            "age": {
28              "type": "short"
29            },
30            "rating": {
31              "type": "short"
32            },
33            "commented_on": {
34              "type": "text"
35            }
36          }
37        }
38      }
39    }
40  }
41}

将映射更改为Nested类型后,我们可以查询索引的方式略有变化。 我们需要使用Nested查询。

下面给出了Nested查询示例:

 1GET /blog_new/_search?pretty
 2{
 3  "query": {
 4    "bool": {
 5      "must": [
 6        {
 7          "nested": {
 8            "path": "comments",
 9            "query": {
10              "bool": {
11                "must": [
12                  {
13                    "match": {
14                      "comments.name": "john"
15                    }
16                  },
17                  {
18                    "match": {
19                      "comments.age": 34
20                    }
21                  }
22                ]
23              }
24            }
25          }
26        }
27      ]
28    }
29  }
30}

由于用户{name:john,age:34}没有匹配,上面的查询将不返回任何文档。

再次感到惊讶? 只需一个小小的改变即可解决问题。

这可能是我们理解的一个较小的变化,但是在elasticsearch存储我们的文档的方式上有很多变化。

在内部,嵌套对象将数组中的每个对象索引为单独的隐藏文档,这意味着可以独立于其他对象查询每个嵌套对象。

下面给出了更改映射后样本文档的内部表示:

 1{
 2  {
 3    "comments.name":    [ john ],
 4    "comments.comment": [ after i investing started reading this ],
 5    "comments.age":     [ 38 ],
 6    "comments.rating":  [ 9 ],
 7    "comments.date":    [ 25 Nov 2017 ]
 8  },
 9  {
10    "comments.name":    [ william ],
11    "comments.comment": [ article, nice ],
12    "comments.age":     [ 34 ],
13    "comments.rating":   [ 8 ],
14    "comments.date":    [ 30 Nov 2017 ]
15  },
16  {
17    "comments.name":    [ smith ],
18    "comments.comment": [ good, post, very],
19    "comments.age":     [ 33 ],
20    "comments.rating":   [ 7 ],
21    "comments.date":    [ 20 Nov 2017 ]
22  },
23  {
24    "title":            [ invest, money ],
25    "body":             [ as, investing, money, please, soon, start ],
26    "tags":             [ invest, money ],
27    "published_on":     [ 18 Oct 2017 ]
28  }
29}

如您所见,每个内部对象都在内部存储为单独的隐藏文档。 这保持了他们的领域之间的关系。

3、Nested类型的作用?

从上一小节,可以清晰的看出nested类型的特别之处。

nested类型是对象数据类型的专用版本,它允许对象数组以可以彼此独立查询的方式进行索引。

4、Nested类型的适用场景

图片来自:rockybean教程

5、Nested类型的增、删、改、查、聚合操作详解

还是以第2节的blog_new索引示例,Nested类型的增、删、改、查操作。

5.1 Nested类型——增

新增blog和评论

 1POST blog_new/blog/2
 2{
 3  "title": "Hero",
 4  "body": "Hero test body...",
 5  "tags": ["Heros", "happy"],
 6  "published_on": "6 Oct 2018",
 7  "comments": [
 8    {
 9      "name": "steve",
10      "age": 24,
11      "rating": 18,
12      "comment": "Nice article..",
13      "commented_on": "3 Nov 2018"
14    }
15  ]
16}

5.2 Nested类型——删

序号为1的评论原来有三条,现在删除John的评论数据,删除后评论数为2条。

1POST  blog_new/blog/1/_update
2{
3 "script": {
4    "lang": "painless",
5    "source": "ctx._source.comments.removeIf(it -> it.name == 'John');"
6 }
7}

5.3 Nested类型——改

将steve评论内容中的age值调整为25,同时调整了评论内容。

1POST blog_new/blog/2/_update
2{
3  "script": {
4    "source": "for(e in ctx._source.comments){if (e.name == 'steve') {e.age = 25; e.comment= 'very very good article...';}}" 
5  }
6}

5.4 Nested类型——查

如前所述,查询评论字段中评论姓名=William并且评论age=34的blog信息。

 1GET /blog_new/_search?pretty
 2{
 3  "query": {
 4    "bool": {
 5      "must": [
 6        {
 7          "nested": {
 8            "path": "comments",
 9            "query": {
10              "bool": {
11                "must": [
12                  {
13                    "match": {
14                      "comments.name": "William"
15                    }
16                  },
17                  {
18                    "match": {
19                      "comments.age": 34
20                    }
21                  }
22                ]
23              }
24            }
25          }
26        }
27      ]
28    }
29  }
30}

5.5 Nested类型——聚合

认知前提:nested聚合隶属于聚合分类中的Bucket聚合分类。

聚合blog_new 中评论者年龄最小的值。

 1GET blog_new/_search
 2{
 3  "size": 0,
 4  "aggs": {
 5    "comm_aggs": {
 6      "nested": {
 7        "path": "comments"
 8      },
 9      "aggs": {
10        "min_age": {
11          "min": {
12            "field": "comments.age"
13          }
14        }
15      }
16    }
17  }
18}

6、小结

  1. 如果您在索引中使用内部对象并做查询操作,请验证内部对象的类型是否为nested类型。 否则查询可能会返回无效的结果文档。
  2. 更新认知是非常痛苦的,不确定的问题只有亲手实践才能检验真知。

参考: [1]http://t.cn/Evwh0uW [2]官网6.x+:http://t.cn/Ehltakr

原文发布于微信公众号 - 铭毅天下(gh_0475cf887cf7)

原文发表时间:2018-10-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员阿凯

JDK10 揭秘

1495
来自专栏Java架构

Java 异常进阶Java异常简介天使变恶魔无处不在的throws手足无措的API使用者疲于应付的API用户正确地使用Checked Exception

1496
来自专栏为数不多的Android技巧

我为Dexposed续一秒——论ART上运行时 Method AOP实现

两年前阿里开源了Dexposed 项目,它能够在Dalvik上无侵入地实现运行时方法拦截,正如其介绍「enable ‘god’ mode for single ...

2342
来自专栏程序你好

.NET面试基础知识

不同公司的职位和工作职责不同。在面试中,工作职责和经验对这个职位很重要。程序员职位有一年的经验他们会关注oops概念、并行编程、算法和解决问题的能力等等。另一方...

1152
来自专栏喔家ArchiSelf

全栈Python 编程必备

Python作为一种编程语言,被称为“胶水语言”,更被拥趸们誉为“最美丽”的编程语言,从云端到客户端,再到物联网终端,无所不在,同时还是人工智能优选的编程语言。

3145
来自专栏精讲JAVA

用 Maven 实现一个 protobuf 的 Java 例子

Protocal Buffers(简称protobuf)是谷歌的一项技术,用于结构化的数据序列化、反序列化,常用于RPC 系统(Remote Procedure...

1222
来自专栏Golang语言社区

一起用golang之Go程序的套路

系统性地介绍golang基础的资料实在太多了,这里不再一一赘述。本文的思路是从另一个角度来由浅入深地探究下Go程序的套路。毕竟纸上得来终觉浅,所以,能动手就不要...

2812
来自专栏walterlv - 吕毅的博客

迫不及待地体验了一把 C#8.0 中的可空引用类型(Nullable Reference)

发布于 2017-12-18 13:41 更新于 2017-12...

992
来自专栏java架构师

设计模式学习笔记之组合模式模式

我们常常会遇到一类具有“容器”特征的对象,他们既是容器,本身也是对象。比如,公司人员管理中的人,他们是处于不同层级,每个层的人下边,又有下属。也就是数的结构。 ...

2936
来自专栏贾老师の博客

Lock-Free 学习总结

在通常使用 Mutex 互斥锁的场景, 有的线程抢占了锁, 其他线程则会被阻塞, 当获得锁的进程挂掉之后, 整个程序就 block 住了. 但在 Lock-Fr...

5353

扫码关注云+社区