前面所说的对象类型虽然可按JSON对象格式保存结构化的对象数据,但由于Lucene并不支持对象类型,所以 Elastiesearch在存储这种类型的字段时会将它们平铺为单个属性。

例如:

PUT colleges/_doc/1
{
  "address": {
    "country": "CN",
    "city": "BJ"
  },
  "age": 10
}

在示例中的colleges文档,address字段会被平铺为address.country和address.city两个字段存储。这种平铺存储的方案在存储单个对象时没有什么问题,但如果在存储数组时会丢失单个对象内部字段的匹配关系。

例如:

PUT colleges/_doc/2
{
  "address": [
    {
      "country": "CN",
      "city": "BJ"
    },
    {
      "country": "US",
      "city": "NY"
    }
  ],
  "age": 10
}

示例中的colleges文档在实际存储时,会被拆解为"dress.country":["CN,"US"]"address.city":["BJ","NY"]两个数组字段。这样一来,单个对象内部,country字段和city字段之间的匹配关系就丢失了。换句话说,使用CN与NY作为共同条件检索的文档时,上述文档也会被检索出来,这在逻辑上就出现了错误:

POST colleges/_search?filter_path=hits
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "address.country": "CN"
          }
        },
        {
          "match": {
            "address.city": "NY"
          }
        }
      ]
    }
  }
}
输出结果:
{
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.77041256,
    "hits" : [
      {
        "_index" : "colleges",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.77041256,
        "_source" : {
          "address" : [
            {
              "country" : "CN",
              "city" : "BJ"
            },
            {
              "country" : "US",
              "city" : "NY"
            }
          ],
          "age" : 10
        }
      }
    ]
  }
}

在示例中使用了bool组合查询,要求country字段为CN而city字段为NY。这样的文档显然并不存在,但由于数组中的对象被平铺为两个独立的数组字段,文档仍然会被检索出来。

nested类型

为了解决对象类型在数组中丢失内部字段之间匹配关系的问题,Elasticsearch提供了一种特殊的对象类型nested。这种类型会为数组中的每一个对象创建一个单独的文档, 以保存对象的字段信息并使它们可检索。由于这类文档并不直接可见,而是藏置在父文档之中,所以这类文档可以称为为隐式文档或嵌入文档。

还是以colleges索引为例,我们把原有的索引删除,将它的address字段设置为nested类型:

PUT colleges
{
  "mappings": {
    "properties": {
      "address": {
        "type": "nested"
      },
      "age": {
        "type": "integer"
      }
    }
  }
}

然后重新存入文档1和2,当字段被设置为nested类型后,再使用原来查询中的bool组合查询就不能检索出来了。这是因为对nested类型字段的检索实际上是对隐式文档的检索,在检索时必须要将检索路由到隐式文档上,所以必须使用专门的检索方法。也就是说,现在即使将原来查询中的查询条件设置为CN和BJ也不会检索出结果。

nested类型字段可使用的检索方法包括DSL的nested查询,还有聚集查询中的nested和reverse_nested两种聚集。

nested查询

nested查询只能针对nested类型字段,需要通过path参数指定nested类型字段的路径,而在query参数中则包含了针对隐式文档的具体查询条件。

例如:

POST /colleges/_search?filter_path=hits
{
  "query": {
    "nested": {
      "path": "address",
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "address.country": "CN"
              }
            },
            {
              "match": {
                "address.city": "NY"
              }
            }
          ]
        }
      }
    }
  }
}
输出结果:
{
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}

在示例中再次使用CN与NY共同作为查询条件,但由于使用nested类型后会将数组中的对象转换成隐式文档,所以在 nested查询中将不会有文档返回了。

将条件更换为CN和BJ,则有文档返回。

POST /colleges/_search?filter_path=hits
{
  "query": {
    "nested": {
      "path": "address",
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "address.country": "CN"
              }
            },
            {
              "match": {
                "address.city": "BJ"
              }
            }
          ]
        }
      }
    }
  }
}
输出结果:
{
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.8836655,
    "hits" : [
      {
        "_index" : "colleges",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.8836655,
        "_source" : {
          "address" : {
            "country" : "CN",
            "city" : "BJ"
          },
          "age" : 10
        }
      },
      {
        "_index" : "colleges",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.8836655,
        "_source" : {
          "address" : [
            {
              "country" : "CN",
              "city" : "BJ"
            },
            {
              "country" : "US",
              "city" : "NY"
            }
          ],
          "age" : 10
        }
      }
    ]
  }
}

nested聚集

nested聚集是一个单桶聚集,也是通过path参数指定nested字段的路径,包含在path指定路径中的隐式文档都将落入桶中。所以nested字段保存数组的长度就是单个文档落入桶中的文档数量,而整个文档落入桶中的数量就是所有文档nested 字段数组长度的总和。

有了nested聚集,就可以针对nested数组中的对象做各种聚集运算,例如:

POST /colleges/_search?filter_path=aggregations
{
  "aggs": {
    "nested_address": {
      "nested": {
        "path": "address"
      },
      "aggs": {
        "city_names": {
          "terms": {
            "field": "address.city.keyword",
            "size": 10
          }
        }
      }
    }
  }
}
输出结果:
{
  "aggregations" : {
    "nested_address" : {
      "doc_count" : 3,
      "city_names" : {
        "doc_count_error_upper_bound" : 0,
        "sum_other_doc_count" : 0,
        "buckets" : [
          {
            "key" : "BJ",
            "doc_count" : 2
          },
          {
            "key" : "NY",
            "doc_count" : 1
          }
        ]
      }
    }
  }
}

在示例中,nested_address是一个nested聚集的名称,它会将address字段的隐式文档归入一个桶中。而嵌套在nested_address聚集中的city_names聚集则会在这个桶中再做terms聚集运算,这样就将对象中city字段所有的词项枚举
出来了。

reverse_nested聚集

reverse_nested聚集用于在隐式文档中对父文档做聚集,所以这种聚集必须作为nested聚集的嵌套聚集使用。

例如:

POST /colleges/_search?filter_path=aggregations
{
  "aggs": {
    "nested address": {
      "nested": {
        "path": "address"
      },
      "aggs": {
        "city names": {
          "terms": {
            "field": "address.city.keyword",
            "size": 10
          },
          "aggs": {
            "avg_age_in_city": {
              "reverse_nested": {},
              "aggs": {
                "avg_age": {
                  "avg": {
                    "field": "age"
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
输出结果:
{
  "aggregations" : {
    "nested address" : {
      "doc_count" : 3,
      "city names" : {
        "doc_count_error_upper_bound" : 0,
        "sum_other_doc_count" : 0,
        "buckets" : [
          {
            "key" : "BJ",
            "doc_count" : 2,
            "avg_age_in_city" : {
              "doc_count" : 2,
              "avg_age" : {
                "value" : 10.0
              }
            }
          },
          {
            "key" : "NY",
            "doc_count" : 1,
            "avg_age_in_city" : {
              "doc_count" : 1,
              "avg_age" : {
                "value" : 10.0
              }
            }
          }
        ]
      }
    }
  }
}

在示例中,city_ names聚集也是将隐式文档中city字段的词项全部聚集出来。不同的是在这个聚集中还嵌套了一个名为avg_age_in_city的聚集,这个聚集就是个reverse_ nested聚集。它会在隐式文档中将city字段具有相同词项的文档归入
一个桶中,而avg_age_in_city聚集嵌套的另外一个名为avg_age的聚集,它会把落入这个桶中文档的age字段的平均值计算出来。所以从总体上来看,这个聚集的作用就是将在同一城市中大学的平均校龄计算出来。