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.问题出现
现在假设我们想查找用户{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}
我们的示例文档作为回复返回。 很惊讶,这是为什么呢?
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的查询。
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}
如您所见,每个内部对象都在内部存储为单独的隐藏文档。 这保持了他们的领域之间的关系。
Parent child
这种建模方式,采取的是类似于关系型数据库的三范式建模,多个实体都分割开来,每个实体之间都通过一些关联方式,进行了父子关系的关联,各种数据不需要都放在一起,父doc 和 子doc 分别在进行更新的时候,都不会影响对方
一对多关系的建模,维护起来比较翻遍,而且我们之前说过,类似关系型数据库的建模的方式,应用层join的方式,会导致比较差,因为做多次搜索,父子关系的数据模型不会性能很好,因为虽然数据实体之间分割开来,但是我们在搜索的时候,由ES自动为我们处理底层的关联关系,并且通过一些收单保证搜索性能,但父子数据必须存在于同一个shard中。
案例背景 ,研发中心原攻管理案例,一个IT公司有多个研发中心,每个研发中心有多个员工
put /company
{
"mappings":{
"rd_center":{},
"employee":{
"_parent": { "type":"rd_center" }
}
}
}
POST /company/rd_center/_bulk
{ "index":{"_id":"1"}}
{"name":"北京研发部","city":"北京","country":"中国"}
{ "index":{"_id":"2"}}
{"name":"上海研发部","city":"上海","country":"中国"}
{ "index":{"_id":"3"}}
{"name":"硅谷人工智能实验室","city":"硅谷","country":"美国"}
PUT /company/employee/1?parent=1
{
"name":"张三",
"birthday": "",
"hobby": "爬山"
}
此时,parent-child 关系,就确保了说,父doc 和 子doc 都是保存在一个shard上的,内部原理还是doc routing,employee 和 rd_center的数据,都会用parent id 作为routing,这样就会到一个shard
POST /company/employee/_bulk
{ "index":{"_id":"2","parent": "1"}}
{"name":"李四","birthday":"1970-10-24","hobby":"游泳"}
{ "index":{"_id":"3","parent": "2"}}
{"name":"王二","birthday":"1979-04-01","hobby":"爬山"}
{ "index":{"_id":"4","parent": "3"}}
{"name":"赵五","birthday":"1987-05-11","hobby":"骑马"}
Nested类型的作用
nested类型是对象数据类型的专用版本,它允许对象数组以可以彼此独立查询的方式进行索引。