目录
1.简介
由于在 Elasticsearch 中单个文档的增删改都是原子性操作,那么将相关实体数据都存储在同一文档中也就理所当然。
如说,我们可以将订单及其明细数据存储在一个文档中。又比如,我们可以将一篇博客文章的评论以一个 comments
数组的形式和博客文章放在一起
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"
}
]
}
由于所有的信息都在一个文档中,当我们查询时就没有必要去联合文章和评论文档,查询效率就很高
但是当我们使用如下查询时,上面的文档也会被当做是符合条件的结果
GET /_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "Alice" }},
{ "match": { "age": 28 }}
]
}
}
}
出现上面这种问题的原因是 JSON 格式的文档被处理成如下的扁平式键值对的结构
{
"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 ]
}
嵌套对象 就是来解决这个问题的。将 comments
字段类型设置为 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 ]
}
在独立索引每一个嵌套对象后,对象中每个字段的相关性得以保留。我们查询时,也仅仅返回那些真正符合条件的文档
不仅如此,由于嵌套文档直接存储在文档内部,查询时嵌套文档和根文档联合成本很低,速度和单独存储几乎一样
嵌套文档是隐藏存储的,我们不能直接获取。如果要增删改一个嵌套对象,我们必须把整个文档重新索引才可以。值得注意的是,查询的时候返回的是整个文档,而不是嵌套文档本身。
1.1.嵌套对象映射
设置一个字段为 nested
很简单 — 你只需要将字段类型 object
替换为 nested
即可
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" }
}
}
}
}
1.2.嵌套对象查询
由于嵌套对象 被索引在独立隐藏的文档中,我们无法直接查询它们。 相应地,必须使用 [ nested
查询] 去获取它们
GET /my_index/blogpost/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "eggs" // title
子句是查询根文档的
}
},
{
"nested": {
"path": "comments", // nested
子句作用于嵌套字段 comments
。在此查询中,既不能查询根文档字段,也不能查询其他嵌套文档。
"query": {
"bool": {
"must": [ // comments.name
和 comments.age
子句操作在同一个嵌套文档中
{
"match": {
"comments.name": "john"
}
},
{
"match": {
"comments.age": 28
}
}
]
}
}
}
}
]
}}}
nested 字段可以包含其他的 nested 字段。同样地,nested 查询也可以包含其他的 nested 查询。而嵌套的层次会按照你所期待的被应用。
nested
查询肯定可以匹配到多个嵌套的文档。每一个匹配的嵌套文档都有自己的相关度得分,但是这众多的分数最终需要汇聚为可供根文档使用的一个分数。
默认情况下,根文档的分数是这些嵌套文档分数的平均值。可以通过设置 score_mode 参数来控制这个得分策略,相关策略有 avg
(平均值), max
(最大值), sum
(加和) 和 none
(直接返回 1.0
常数值分数)。
GET /my_index/blogpost/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "eggs"
}
},
{
"nested": {
"path": "comments",
"score_mode": "max", // 返回最优匹配嵌套文档的 _score
给根文档使用
"query": {
"bool": {
"must": [
{
"match": {
"comments.name": "john"
}
},
{
"match": {
"comments.age": 28
}
}
]
}
}
}
}
]
}
}
}}}
如果 nested 查询放在一个布尔查询的 filter 子句中,其表现就像一个 nested 查询,只是 score_mode 参数不再生效。
因为它被用于不打分的查询中 — 只是符合或不符合条件,不必打分 — 那么 score_mode 就没有任何意义,因为根本就没有要打分的地方。
1.3.使用嵌套字段排序
尽管嵌套字段的值存储于独立的嵌套文档中,但依然有方法按照嵌套字段的值排序。
PUT /my_index/blogpost/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"
}
]
}
假如我们想要查询在10月份收到评论的博客文章,并且按照 stars
数的最小值来由小到大排序,那么查询语句如下
GET /_search
{
"query": { // 此处的 nested
查询将结果限定为在10月份收到过评论的博客文章
"nested": {
"path": "comments",
"filter": {
"range": {
"comments.date": {
"gte": "2014-10-01",
"lt": "2014-11-01"
}
}
}
}
},
"sort": {
"comments.stars": { // 结果按照匹配的评论中 comment.stars
字段的最小值 (min
) 来由小到大 (asc
) 排序
"order": "asc",
"mode": "min",
"nested_path": "comments", // 排序子句中的 nested_path
和 nested_filter
和 query
子句中的 nested
查询相同
"nested_filter": {
"range": {
"comments.date": {
"gte": "2014-10-01",
"lt": "2014-11-01"
}
}
}
}
}
}
为什么要用 nested_path 和 nested_filter 重复查询条件呢?原因在于,排序发生在查询执行之后。 查询条件限定了在10月份收到评论的博客文档,但返回的是博客文档。如果我们不在排序子句中加入 nested_filter
, 那么我们对博客文档的排序将基于博客文档的所有评论,而不是仅仅在10月份接收到的评论。
1.4.嵌套聚合
在查询的时候,使用 nested
查询 就可以获取嵌套对象的信息。同理, nested
聚合允许对嵌套对象里的字段进行聚合操作。
GET /my_index/blogpost/_search
{
"size" : 0,
"aggs": {
"comments": { // nested
聚合 `进入'' 嵌套的 `comments
对象
"nested": {
"path": "comments"
},
"aggs": {
"by_month": {
"date_histogram": { // comment对象根据 comments.date 字段的月份值被分到不同的桶
"field": "comments.date",
"interval": "month",
"format": "yyyy-MM"
},
"aggs": {
"avg_stars": {
"avg": { // 计算每个桶内star的平均数量
"field": "comments.stars"
}
}
}
}
}
}
}
}
[结果]
总共有4个 comments
对象 :1个对象在9月的桶里,3个对象在10月的桶里
...
"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
}
}
]
}
}
}
...
1.5.逆向嵌套聚合
nested
聚合 只能对嵌套文档的字段进行操作。 根文档或者其他嵌套文档的字段对它是不可见的。 然而,通过 reverse_nested
聚合,可以 走出 嵌套层级,回到父级文档进行操作。
[例如] 要基于评论者的年龄找出评论者感兴趣 tags
的分布。 comment.age
是一个嵌套字段,但 tags
在根文档中
GET /my_index/blogpost/_search
{
"size" : 0,
"aggs": {
"comments": {
"nested": { // nested
聚合进入 comments
对象
"path": "comments"
},
"aggs": {
"age_group": {
"histogram": { // histogram
聚合基于 comments.age
做分组,每10年一个分组
"field": "comments.age",
"interval": 10
},
"aggs": {
"blogposts": {
"reverse_nested": {}, // reverse_nested
聚合退回根文档
"aggs": {
"tags": {
"terms": { // terms
聚合计算每个分组年龄段的评论者最常用的标签词
"field": "tags"
}
}
}
}
}
}
}
}
}
}
[结果]
..
"aggregations": {
"comments": {
"doc_count": 4, // 一共有4条评论
"age_group": {
"buckets": [
{
"key": 20, // 在20岁到30岁之间总共有两条评论
"doc_count": 2,
"blogposts": {
"doc_count": 2, // 这些评论包含在两篇博客文章中
"tags": {
"doc_count_error_upper_bound": 0,
"buckets": [ // 在这些博客文章中最热门的标签是 shares
、 cash
、equities
{ "key": "shares", "doc_count": 2 },
{ "key": "cash", "doc_count": 1 },
{ "key": "equities", "doc_count": 1 }
]
}
}
},
...
1.6.嵌套对象的使用时机
嵌套对象 在只有一个主要实体时非常有用,这个主要实体包含有限个紧密关联但又不是很重要的实体,例如 blogpost
对象包含评论对象。 在基于评论的内容查找博客文章时, nested
查询有很大的用处,并且可以提供更快的查询效率。
嵌套模型的缺点如下:
- 当对嵌套文档做增加、修改或者删除时,整个文档都要重新被索引。嵌套文档越多,这带来的成本就越大。
- 查询结果返回的是整个文档,而不仅仅是匹配的嵌套文档。尽管目前有计划支持只返回根文档中最佳匹配的嵌套文档,但目前还不支持。
有时需要在主文档和其关联实体之间做一个完整的隔离设计。这个隔离是由 父子关联 提供的。