我们常会有这样的应用场景:把跟某一个实体相关的实体存储在一个document中。比如我们会存储博客与之相关的评论。
PUT /my_index/blogpost/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" } ] }如果我们依赖动态mapping,comments字段将会作为一个object创建。
因为所有关于一个博客的所有评论都存储在一个document中,所以查询过程中就可以很快速的检索。但是问题来了,以下查询仍然会成立:
GET /_search { "query": { "bool": { "must": [ { "match": { "name": "Alice" }}, { "match": { "age": 28 }} ] } } }这显然不是我们想要的结果。为什么会出现这种情形?
原因在于:我们结构化的json数据在es中被扁平化存储了,如下:
{ "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.
object类型对于存储单一实体是有效的,但是在数组情况下,就无效了,会丧失实体内的关系。
这就是es提供nested objects的目的,来解决以上问题。通过mapping将comments字段设置为nested,每一个nested object就会在索引中作为单一的document存储,如下:
{ "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单独存储(nested 跟 root 处在同一个level上),其fields保持了之间的关系。因此以上查询过程中出现的问题就解决了。不仅如此,由于采用了这样的存储方式,nested object跟root之间的连接查询也变得非常高效,就像是在同一个document中一样。
这种nested document是隐藏存储的,我们不能直接访问。更新、添加或者删除一个nested object,必须reindex整个document。查询结果也不仅仅是返回nested object,而是返回整个document。
1:nested object mapping
设置非常简单:
PUT /my_index { "mappings": { "blogpost": { "properties": { "comments": { "type": "nested", "properties": { "name": { "type": "string" }, "comment": { "type": "string" }, "age": { "type": "short" }, "stars": { "type": "short" }, "date": { "type": "date" } } } } } } }2:query a nested object
因为nested objet 在索引中是作为隐藏文档的形式存在的,所以我们不能直接query,而应该利用nested query 或者 nested filter去访问:
GET /my_index/blogpost/_search { "query": { "bool": { "must": [ { "match": { "title": "eggs" }}, { "nested": { "path": "comments", "query": { "bool": { "must": [ { "match": { "comments.name": "john" }}, { "match": { "comments.age": 28 }} ] }}}} ] }}}当然,一个nested query会去匹配多个nested object,每一个match都会产生一个score值,但是这些score值需要统一成一个score跟root级别的query一起使用。默认是采用average的方式,es也提供了score_mode:avg,max,min,node。
GET /my_index/blogpost/_search { "query": { "bool": { "must": [ { "match": { "title": "eggs" }}, { "nested": { "path": "comments", "score_mode": "max", "query": { "bool": { "must": [ { "match": { "comments.name": "john" }}, { "match": { "comments.age": 28 }} ] }}}} ] }}}3:sorting by nested fields
即使value值存在于一个单独的nested document中,针对某一个nested field进行排序也是可以的。
假设我们想要得到在十月份收到评论的博客,按照这些博客评论中最小的star值进行排序(我们先看下这个查询:十月份收到评论的博客中,每一个博客都有多条评论,因此排序根据这些评论中的最小值进行也就不足为奇了,但是我们的检索结果只是想得到这些博客而已,而不是评论。)
GET /_search { "query": { "nested": { "path": "comments", "filter": { "range": { "comments.date": { "gte": "2014-10-01", "lt": "2014-11-01" } } } } }, "sort": { "comments.stars": { "order": "asc", "mode": "min", "nested_filter": { "range": { "comments.date": { "gte": "2014-10-01", "lt": "2014-11-01" } } } } } }query部分不难理解,sort中为啥还要嵌套一个跟query一样的查询呢???
首先sort是针对query的结果进行排序的,但是query的返回结果是博客信息(query筛选了一部分博客信息,这些博客信息存在10月份的评论,但并不意味着这一篇博客不存在其他月份的评论,而这些信息都是跟随query信息一起返回的,而如果一篇博客有10篇评论而都不在10月份,则这个结果不会返回),如果不执行一次筛选的话,排序的依据就可能不仅仅是10月份的的博客信息了。
4:nested aggregations
如同我们可以用nested query去访问nested object一样,我们也可以用nested aggregation对nested object中的fields进行操作。
GET /my_index/blogpost/_search?search_type=count { "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" } } } } } } } }
上面的aggregation先按照comments.date字段进行bucket,然后每一个bucket根据comment.stars字段进行avg。
nested aggregation只能访问nested document中的field,不能访问root cocument 或者 不同的nested document。然而使用reverse_nested aggregation可以跳出nested scope,去访问parent level的数据.
比如:我们想要得到发表评论的人感兴趣的tags,以评论者的年龄区分。这里comment.age是一个nested field,而tags是一个root document。
GET /my_index/blogpost/_search?search_type=count { "aggs": { "comments": { "nested": { "path": "comments" }, "aggs": { "age_group": { "histogram": { "field": "comments.age", "interval": 10 }, "aggs": { "blogposts": { "reverse_nested": {}, "aggs": { "tags": { "terms": { "field": "tags" } } } } } } } } } }以上:histogtam agg用comment.age进行bucket,然后reverse_nested aggs到了root document,然后term aggs对上边的没一个bucket中的数据根据tags进行buckent统计。
总结:什么时候使用nested objects呢?
Nested objects适用与一个主实体(blog),关联几个有限数目的不太重要的实体(comment)的情况。利于根据次要实体的内容查找符合条件的主实体,并且nested query和filter提供了快速的query-time join性能。
nested model 的不足之处在于:
添加,更新,删除一个nested document,整个document必须reindexed。nested object越多,代价越高。
查询请求返回完整的document,并不仅仅是match到的nested objects(目前es这块有打算去做,但是现在还是不支持)。
有些应用场景下你可能需要在main document和associated entity做完全的分离,这种分离由parent-child relationship提供。