一、嵌套对象
1.1、问题背景
在elasticsearch中,我们可以将密切相关的实体存储在单个文档中。 例如,我们可以通过传递一系列评论来存储博客文章及其所有评论:
{
"title": "Invest Money",
"body": "Please start investing money as soon...",
"tags": ["money", "invest"],
"published_on": "18 Oct 2017",
"comments": [
{
"name": "William",
"age": 34,
"rating": 8,
"comment": "Nice article..",
"commented_on": "30 Nov 2017"
},
{
"name": "John",
"age": 38,
"rating": 9,
"comment": "I started investing after reading this.",
"commented_on": "25 Nov 2017"
},
{
"name": "Smith",
"age": 33,
"rating": 7,
"comment": "Very good post",
"commented_on": "20 Nov 2017"
}
]
}
如上所示,所以我们有一个文档描述了一个帖子和一个包含帖子上所有评论的内部对象评论。
但是Elasticsearch搜索中的内部对象并不像我们期望的那样工作。
现在假设我们想查找用户{name:john,age:34}评论过的所有博客帖子。 让我们再看一下上面的示例文档,找到评论过的用户:
GET /blog/_search?pretty
{
"query": {
"bool": {
"must": [
{
"match": {
"comments.name": "John"
}
},
{
"match": {
"comments.age": 34
}
}
]
}
}
}
我们想应该没有返回,但是实际结果是我们的示例文档作为回复返回。 很惊讶,这是为什么呢?
1.2、原因分析
原因是:elasticsearch中的内部对象无法按预期工作,这里的问题是elasticsearch(lucene)使用的库没有内部对象的概念,因此内部对象被扁平化为一个简单的字段名称和值列表。
我们的文档内部存储为:
{
"title": [ invest, money ],
"body": [ as, investing, money, please, soon, start ],
"tags": [ invest, money ],
"published_on": [ 18 Oct 2017 ]
"comments.name": [ smith, john, william ],
"comments.comment": [ after, article, good, i, investing, nice, post, reading, started, this, very ],
"comments.age": [ 33, 34, 38 ],
"comments.rating": [ 7, 8, 9 ],
"comments.commented_on": [ 20 Nov 2017, 25 Nov 2017, 30 Nov 2017 ]
}
如上,您可以清楚地看到,comments.name和comments.age之间的关系已丢失。
这就是为什么我们的文档匹配john和34的查询。
1.3、如何解决
要解决这个问题,我们只需要对elasticsearch的映射进行一些小改动。
如果您查看索引的映射,您会发现comments
字段的类型是object
。
我们需要更新它的类型为nested
。
我们可以通过运行以下查询来简单地更新索引的映射:
PUT /blog_new
{
"mappings": {
"blog": {
"properties": {
"title": {
"type": "text"
},
"body": {
"type": "text"
},
"tags": {
"type": "keyword"
},
"published_on": {
"type": "keyword"
},
"comments": {
"type": "nested",
"properties": {
"name": {
"type": "text"
},
"comment": {
"type": "text"
},
"age": {
"type": "short"
},
"rating": {
"type": "short"
},
"commented_on": {
"type": "text"
}
}
}
}
}
}
}
将映射更改为Nested
类型后,我们可以查询索引的方式略有变化。 我们需要使用Nested
查询。
下面给出了Nested
查询示例:
GET /blog_new/_search?pretty
{
"query": {
"bool": {
"must": [
{
"nested": {
"path": "comments",
"query": {
"bool": {
"must": [
{
"match": {
"comments.name": "john"
}
},
{
"match": {
"comments.age": 34
}
}
]
}
}
}
}
]
}
}
}
由于用户{name:john,age:34}
没有匹配,上面的查询将不返回任何文档。
再次感到惊讶? 只需一个小小的改变即可解决问题。
这可能是我们理解的一个较小的变化,但是在elasticsearch存储我们的文档的方式上有很多变化。
在内部,嵌套对象将数组中的每个对象索引为单独的隐藏文档,这意味着可以独立于其他对象查询每个嵌套对象。
下面给出了更改映射后样本文档的内部表示:
{
{
"comments.name": [ john ],
"comments.comment": [ after i investing started reading this ],
"comments.age": [ 38 ],
"comments.rating": [ 9 ],
"comments.date": [ 25 Nov 2017 ]
},
{
"comments.name": [ william ],
"comments.comment": [ article, nice ],
"comments.age": [ 34 ],
"comments.rating": [ 8 ],
"comments.date": [ 30 Nov 2017 ]
},
{
"comments.name": [ smith ],
"comments.comment": [ good, post, very],
"comments.age": [ 33 ],
"comments.rating": [ 7 ],
"comments.date": [ 20 Nov 2017 ]
},
{
"title": [ invest, money ],
"body": [ as, investing, money, please, soon, start ],
"tags": [ invest, money ],
"published_on": [ 18 Oct 2017 ]
}
}
如您所见,每个内部对象都在内部存储为单独的隐藏文档。 这保持了他们的领域之间的关系。
通过上文,可以清晰的看出nested类型的特别之处。
nested
类型是对象数据类型的专用版本,它允许对象数组以可以彼此独立查询的方式进行索引。
限制nested字段的数量:
索引一个包含 100 个 nested字段的文档实际上就是索引 101 个文档,每个嵌套文档都作为一个独立文档来索引。为了防止过度定义嵌套字段的数量,每个索引可以定义的嵌套字段被限制在 50 个。
1.4、Nested类型的适用场景
尽量选择nested来解决问题。
1.5、Nested类型的增、删、改、查、聚合操作详解
1.5.1、Nested类型——增
新增blog和评论:
POST blog_new/blog/2
{
"title": "Hero",
"body": "Hero test body...",
"tags": ["Heros", "happy"],
"published_on": "6 Oct 2018",
"comments": [
{
"name": "steve",
"age": 24,
"rating": 18,
"comment": "Nice article..",
"commented_on": "3 Nov 2018"
}
]
}
新增评论:
POST test/_update/P4vUL3IB8Q1wCMXpE3WC/
{
"script" : {
"source": "ctx._source.comments.add(params.new_comment)",
"params" : {
"new_comment" : {
"name" : "xiang",
"age" : 25,
"rating" : 18,
"comment" : "very very good article...",
"commented_on" : "3 Nov 2018"
}
}
}
}
1.5.2、Nested类型——删
序号为1的评论原来有三条,现在删除John的评论数据,删除后评论数为2条。
POST blog_new/blog/1/_update
{
"script": {
"lang": "painless",
"source": "ctx._source.comments.removeIf(it -> it.name == 'John');"
}
}
1.5.3、Nested类型——改
将steve评论内容中的age值调整为25,同时调整了评论内容。
POST blog_new/blog/2/_update
{
"script": {
"source": "for(e in ctx._source.comments){if (e.name == 'steve') {e.age = 25; e.comment= 'very very good article...';}}"
}
}
1.5.4、Nested类型——查
如前所述,查询评论字段中评论姓名=William并且评论age=34的blog信息。
GET /blog_new/_search?pretty
{
"query": {
"bool": {
"must": [
{
"nested": {
"path": "comments",
"query": {
"bool": {
"must": [
{
"match": {
"comments.name": "William"
}
},
{
"match": {
"comments.age": 34
}
}
]
}
}
}
}
]
}
}
}
1.5.5、Nested类型——聚合
认知前提:nested聚合隶属于聚合分类中的Bucket聚合分类。
聚合blog_new 中评论者年龄最小的值。
GET blog_new/_search
{
"size": 0,
"aggs": {
"comm_aggs": {
"nested": {
"path": "comments"
},
"aggs": {
"min_age": {
"min": {
"field": "comments.age"
}
}
}
}
}
}
1.6、小结
如果您在索引中使用内部对象并做查询操作,请验证内部对象的类型是否为nested
类型。 否则查询可能会返回无效的结果文档。
更新认知是非常痛苦的,不确定的问题只有亲手实践才能检验真知。
二、父子文档
2.1、join 简介
在 ES 中有一种特殊的数据类型『join
』,被形象地称为父子文档。它是一种可以在同一索引中存放两种有关系数据的数据类型,类似于关系数据库中让两张表发生关系的外键 FOREIGN KEY
。
在官方文档中这样介绍:join
数据类型的字段是一个特殊字段,它可以在同一个索引的文档中创建 父子关系 。通过参数 relations
定义可能存在关系的一组文档,这个关系的参数由 父名 和 子名 构成。
2.2、定义
我们需要在设置 mapping
时将其关系定义好,如下示例:
PUT 索引名称
{
"mappings": {
"properties": {
"join类型的字段名称": {
"type": "join",
"relations": {
"父文档标示字段名": "子文档标示字段名"
}
}
}
}
}
2.3、父文档
构建父文档时可以通过如下方法:
PUT 索引名称/类型/文档id?refresh
{
"text": "EthanYan",
... // 父文档中其他的字段与值
"join类型的字段名称": {
"name": "父文档标示字段名"
}
}
这种方式是为了便于理解,与下方子文档中构建方式对应。当你运用熟练后,有一种简便的构建方法:
PUT 索引名称/类型/文档id?refresh
{
"text": "EthanYan",
... // 父文档中其他的字段与值
"join类型的字段名称": "父文档标示字段名"
}
2.4、子文档
构建子文档时可以通过如下方法:
PUT 索引名称/类型/文档id?routing=父文档id&refresh
{
"text": "xiaoyan",
... // 子文档中其他的字段与值
"join类型的字段名称": {
"name": "子文档标示字段名",
"parent": "父文档id"
}
}
注意:
构建子文档时与父文档有些许不同,以下几点需要特别注意:
url
中可以看到有一个参数routing
,此参数必须设置,因为我们需要保证父文档与子文档在同一分片中。- 我们可以看到子文档在
join
类型字段中除了参数name
外,还多了一个参数parent
,故名思义,此字段为了指明父文档的所在,其值填写为父文档的 id
2.5、查询
此字段类型当然是为了查询而存在,要不然没有灵魂。下面举例进行说明。索引名为 sales_org
有一个父文档为下方示例:
{
"node_name_cn": "川渝",
"node_code": "LP.IIB.RW.CTU",
"node_type": "办事处",
"node_id_fqdn": "SI/LP/LP.IIB.RW/LP.IIB.RW.CTU",
"node_name_fqdn": "SI/LP//川渝",
"node_name_en": "",
"mgmt_territory": "",
"node_tree_level": 3,
"node_name_short": "LP.IIB.RW.CTU",
"node_info": "node_parent"
}
一个子文档示例如下:
{
"empl_id": "*******",
"email_addr": "*****@fafa.com",
"dept_id": "LP.IIB.RW.CTU",
"name_cn": "Nie Cong",
"node_info": {
"parent": "LP.IIB.RW.CTU",
"name": "node_child"
}
}
可以看到 join
类型字段名为 node_info
,父文档标示字段名为 node_parent
,子文档标示字段名为 node_child
.
2.6、 基于父文档查询全部子文档
POST sales_org/_search
{
"query": {
"has_parent": {
"parent_type": "node_parent", // 填写父文档标示字段名
"query": { // 填写查询条件,注意填写的查询条件是查询父文档,该查询条件是为定位到要基于的父文档
"match": {
"_id": "LP.IIB.RW.CTU"
}
}
}
}
}
2.7、基于子文档查询其父文档
{
"query": {
"has_child": {
"type": "node_child", // 填写子文档标示字段名
"query": { // 填写查询条件,注意填写的查询条件是查询子文档,该查询条件是为定位到要基于的子文档
"match": {
"dept_id": "LP.IIB.RW.CTU"
}
}
}
}
}