1.问题引入:
由于在 ES 里新建、删除、更新单个文档都是原子性的,那么将相关实体保存在同一文档里面是有意义的。
PUT /blog/_doc/1
{
"title":"Nest eggs",
"body":"Making your money work...",
"tags":[
"cash",
"shares"
],
"comments":[
{
"name":"John Smith",
"comment":"Great article",
"age":28,
"stars":4,
"date":"2014-09-01"
},
{
"name":"Alice White",
"comment":"More like this please",
"age":31,
"stars":5,
"date":"2014-10-22"
}
]
}
因为所有的内容都在同一文档里,在查询的时候就没有必要拼接多份文档,因此检索性能会更好。但是,上面的文档会匹配这样的一个查询:
POST /blog/_search
{
"query":{
"bool":{
"must":[
{
"match":{
"comments.name":"Alice"
}
},
{
"match":{
"comments.age":28
}
}
]
}
}
}
居然有结果!但是 Alice 是 31,不是 28 啊!
造成这种交叉对象匹配的原因是因为 JSON 文档在 Lucene 底层会被打平成一个简单的键值格式,就像这样:
{
"title": ["eggs","nest"],
"body": ["making","money","work","your"],
"tags": ["cash","shares"],
"comments.name": ["alice","john","smith","white"],
"comments.comment": ["article","great","like","more","please","this"],
"comments.age": [28, 31],
"comments.stars": [4, 5],
"comments.date": ["2014-09-01","2014-10-22"]
}
显然,像这种 'Alice'/'31','john'/'2014-09-01' 间的关联性就不可避免地丢失了。
虽然 object 类型的字段对于保存单一的 object 很有用,但是从检索的角度来说,这对于保存一个 object 数组却是无用的。
2.引入 nested 解决问题:
nested object 就是为了解决上述问题而设计出来的。通过将 comments 字段映射为 nested 类型,而不是 object 类型,每个 nested object 将会作为一个隐藏的单独文档进行保存。如下:
{
"comments.name": ["john", "smith"],
"comments.comment": ["article", "great"],
"comments.age": [28],
"comments.stars": [4],
"comments.date": ["2014-09-01"]
}
{
"comments.name": ["alice","white"],
"comments.comment": ["like","more","please","this"],
"comments.age": [31],
"comments.stars": [5],
"comments.date": ["2014-10-22"]
}
{
"title": ["eggs","nest"],
"body": ["making","money","work","your"],
"tags": ["cash","shares"]
}
通过分开给每个 nested object 进行保存,object 内部字段间的关系就能保持。当执行查询时,只会匹配同时出现在相同的 nested object 里的结果。不仅如此,由于 nested objects 保存数据的方式,在查询的时候将根文档和 nested objects 文档拼接是很快的,就跟把他们当成一个单独的文档一样快。这些额外的 nested objects 文档是隐藏的,我们不能直接接触。为了更新、增加或者移除一个 nested 对象,必须重新插入整个文档。要记住一点:查询请求返回的结果不仅仅包括 nested 对象,而是整个文档。
3.nested 设置 mapping:
1).删除之前的索引:
DELETE /blog
2).创建一个nested 字段很简单,只要在你通常指定 object 类型的地方,改成 nested 类型就行:
PUT /blog
{
"mappings": {
"properties": {
"title": {
"type": "text"
},
"body": {
"type": "text"
},
"tags": {
"type": "keyword"
},
"comments": {
"type": "nested",
"properties": {
"name": {
"type": "text"
},
"comment": {
"type": "text"
},
"age": {
"type": "short"
},
"stars": {
"type": "short"
},
"date": {
"type": "date"
}
}
}
}
}
}
3).插入之前的文档:
PUT /blog/_doc/1
{
"title":"Nest eggs",
"body":"Making your money work...",
"tags":[
"cash",
"shares"
],
"comments":[
{
"name":"John Smith",
"comment":"Great article",
"age":28,
"stars":4,
"date":"2014-09-01"
},
{
"name":"Alice White",
"comment":"More like this please",
"age":31,
"stars":5,
"date":"2014-10-22"
}
]
}
4).再用以前的方法就搜索不到了:
POST /blog/_search
{
"query":{
"bool":{
"must":[
{
"match":{
"comments.name":"Alice"
}
},
{
"match":{
"comments.age":28
}
}
]
}
}
}
4.nested 之搜索:
nested object 作为一个独立隐藏文档单独建索引,因此,我们不能直接查询它们。取而代之,我们必须使用 nested 查询或者 nested filter 来获取它们:
GET /blog/_search
{
"query":{
"bool":{
"must":[
{
"match":{
"title":"eggs"
}
},
{
"nested":{
"path":"comments",
"query":{
"bool":{
"must":[
{
"match":{
"comments.name":"john"
}
},
{
"match":{
"comments.age":28
}
}
]
}
}
}
}
]
}
}
}
一个 nested 字段可以包含其他的 nested 字段。相似地,一个 nested 查询可以包含其他 nested 查询。只要你希望,你就可以使用嵌套层。当然,一个 nested 查询可以匹配多个 nested 文档。每个匹配的 nested 文档都有它自己相关评分,但是这些评分必须归为一个总分应用于根文档上。默认会平均所有匹配的 nested 文档的分数。当然,也可以通过设定 score_mode 参数为 avg,max,sum 或者甚至为none(根文档获得一致评分1.0)。
GET /blog/_search
{
"query":{
"bool":{
"must":[
{
"match":{
"title":"eggs"
}
},
{
"nested":{
"path":"comments",
"score_mode":"max",
"query":{
"bool":{
"must":[
{
"match":{
"comments.name":"john"
}
},
{
"match":{
"comments.age":28
}
}
]
}
}
}
}
]
}
}
}
nested 过滤跟 nested 查询非常像,只是过滤不接受评分。
5.nested 之排序:
1).我们可以基于 nested 字段的值进行排序。再插入一份文档:
PUT /blog/_doc/2
{
"title":"Investment secrets",
"body":"What they don't tell you ...",
"tags":[
"shares",
"equities"
],
"comments":[
{
"name":"Mary Brown",
"comment":"Lies, lies, lies",
"age":42,
"stars":1,
"date":"2014-10-18"
},
{
"name":"John Smith",
"comment":"You're making it up!",
"age":28,
"stars":2,
"date":"2014-10-16"
}
]
}
2).设想我们想要检索在10月份被评论的博客文章,同时按每篇文章收到的最低星级排序。检索请求应该类似如下:
POST /blog/_search
{
"query": {
"nested": {
"path": "comments",
"query": {
"range": {
"comments.date": {
"gte": "2014-10-01",
"lt": "2014-11-01"
}
}
}
}
},
"sort": {
"comments.stars": {
"order": "asc",
"mode": "min"
}
}
}
6.nested 之聚合:
1).与在搜索时需要使用特定的 nested 查询来获取 nested object 一样,特定的 nested 聚合同样能让我们对 nested object 内的字段进行聚合:
POST /blog/_search
{
"aggs":{
"comments":{
"nested":{①
"path":"comments"
},
"aggs":{
"by_month":{
"date_histogram":{②
"field":"comments.date",
"interval":"month",
"format":"yyyy-MM"
},
"aggs":{
"avg_stars":{
"avg":{③
"field":"comments.stars"
}
}
}
}
}
}
}
}
①:nested 聚合进入 nested 评论对象。
②:评论基于 comments.date 字段聚合成月。
③:对于每一个簇,计算 stars 的平均值。
结果表明,聚合的确发生在 nested 文本层:
...
"aggregations": {
"comments": {
"doc_count": 4,
"by_month": {
"buckets": [
{
"key_as_string": "2014-09",
"key": 1409529600000,
"doc_count": 1,
"avg_stars": {
"value": 4
}
},
{
"key_as_string": "2014-10",
"key": 1412121600000,
"doc_count": 3,
"avg_stars": {
"value": 2.6666666666666665
}
}
]
}
}
}
2).反嵌套聚合:
一个 nested 聚合只能接入 nested 文本内的字段,它不能看到根文本或者不同 nested 文本内的字段。但是,我们可以通过一个反嵌套聚合跳出 nested 局域进入父层。比如我们基于评论者的年龄找出哪些标签是评论者感兴趣的。comment.age 是一个 nested 字段,而 tags 位于根文本中:
POST /blog/_search
{
"aggs":{
"comments":{
"nested":{①
"path":"comments"
},
"aggs":{
"age_group":{
"histogram":{②
"field":"comments.age",
"interval":10
},
"aggs":{
"blogposts":{
"reverse_nested":{},③
"aggs":{
"tags":{
"terms":{④
"field":"tags"
}
}
}
}
}
}
}
}
}
}
①:nested 聚合进入 nested 评论对象。
②:histogram(直方图)聚合在 comments.age 字段上分组,每10年一组。
③:reverse_nested 聚合跳转回根文本。
④:terms 聚合计算每个年龄组的流行 terms。
下面是简化结果:
..
"aggregations": {
"comments": {
"doc_count": 4,
"age_group": {
"buckets": [
{
"key": 20,
"doc_count": 2,
"blogposts": {
"doc_count": 2,
"tags": {
"doc_count_error_upper_bound": 0,
"buckets": [
{ "key": "shares", "doc_count": 2 },
{ "key": "cash", "doc_count": 1 },
{ "key": "equities", "doc_count": 1 }
]
}
}
},
...
7.使用 nested 对象的场景:
当有一个主要的实体,就像 blog ,还有有限的一些相关但是没这么重要的其他实体数组,比如 comments(评论),nested 对象就显得非常有用。基于评论的内容找到博客文章是有价值的。
8. nested 模式的缺点:
1).为了增加、改变或者删除一个 nested 文本,整个文本必须重建。nested 文本越多,代价就越大。
2).检索请求返回整个文本,而不仅是匹配的 nested 文本。