普通内部对象

"kibana_sample_data_ecommerce" : {
    "mappings" : {
      "properties" : {
        "category" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword"
            }
          }
        },
        "currency" : {
          "type" : "keyword"
        },
        "customer_full_name" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        //省略部分
        "products" : {!!!!!!!!!!!!!!!!!
          "properties" : {
            "_id" : {
              "type" : "text",
              "fields" : {
                "keyword" : {
                  "type" : "keyword",
                  "ignore_above" : 256
                }
              }
            },
            "base_price" : {
              "type" : "half_float"
            },
            "base_unit_price" : {
              "type" : "half_float"
            },

不是期望值

GET kibana_sample_data_ecommerce/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "products.base_price": 24.99 }},
        { "match": { "products.sku":"ZO0549605496"}},
        {"match": { "order_id": "584677"}}
      ]
    }
  }
}

我这里搜索有三个条件,order_id,商品的价格和sku,事实上同时满足这三个条件的文档并不存在(sku=ZO0549605496的商品价格是11.99)。但是结果却返回了一个文档,这是为什么呢?

因为在ES中对于json对象数组的处理是压扁了处理的,比如上面的例子在ES存储的结构是这样的:

{
  "order_id":            [ 584677 ],
  "products.base_price":    [ 11.99, 24.99... ],
  "products.sku": [ ZO0549605496, ZO0299602996 ],
  ...
}

很明显,这样的结构丢失了商品金额和sku的关联关系。

如果你的业务场景对这个问题不敏感,就可以选择这种方式,因为它足够简单并且效率也比下面两种方案高。

嵌套文档

// mapping
PUT test_index
{
  "mappings": {
    "properties": {
      "user": {
        "type": "nested",
          "properties": {
            "name":    { "type": "string"  },
            "age":     { "type": "short"   }
          }
      }
    }
  }
}
// index data
PUT test_index/_doc/1
{
  "group" : "root",
  "user" : [
    {
      "name" : "John",
      "age" :  30
    },
    {
      "name" : "Alice",
      "age" :  28
    }
  ]
}
PUT test_index/_doc/2
{
  "group" : "wheel",
  "user" : [
    {
      "name" : "Tom",
      "age" :  33
    },
    {
      "name" : "Jack",
      "age" :  25
    }
  ]
}
// search
GET test_index/_search
{
  "query": {
    "nested": {
      "path": "user",
      "query": {
        "bool": {
          "must": [
            { "match": { "user.name": "Tom" }},
            { "match": { "user.age":  33 }} 
          ]
        }
      }
    }
  }
}
// result
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 2.2039728,
    "hits" : [
      {
        "_index" : "test_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 2.2039728,
        "_source" : {
          "group" : "wheel",
          "user" : [
            {
              "name" : "Tom",
              "age" : 33
            },
            {
              "name" : "Jack",
              "age" : 25
            }
          ]
        }
      }
    ]
  }
}

缺点

GET _cat/indices?v

       

es dev tool查看所有索引名称 es 查询所有索引_es dev tool查看所有索引名称

     

是不是很奇怪问啥文档的数量是6而不是2呢?这是因为nested子文档在ES内部其实也是独立的lucene文档,只是我们在查询的时候,ES内部帮我们做了join处理。最终看起来好像是一个独立的文档一样。

那可想而知同样的条件下,这个性能肯定不如普通内部对象的方案。在实际的业务应用中要根据实际情况决定是否选择这种方案。

嵌套文档聚合

在查询的时候,我们使用 nested 查询 就可以获取嵌套对象的信息。同理, nested 聚合允许我们对嵌套对象里的字段进行聚合操作。

GET /my_index/blogpost/_search
{
  "size" : 0,
  "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"
              }
            }
          }
        }
      }
    }
  }
}
// result
...
"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
              }
           }
        ]
     }
  }
}
...
总共有4个 comments 对象 :1个对象在9月的桶里,3个对象在10月的桶里。

TODO

例如,我们要基于评论者的年龄找出评论者感兴趣 tags 的分布。 comment.age 是一个嵌套字段,但 tags 在根文档中:

reverse_nested

父子文档

假如我需要更新文档的group属性的值,需要重新索引这个文档。尽管嵌套的user对象我不需要更新,他也随着主文档一起被重新索引了。

使用场景:一个子文档可以属于多个主文档的场景,用nested无法实现。

// my_join_field是给我们的父子文档关系的名字,这个可以自定义。join关键字表示这是一个父子文档关系,接下来relations里面表示question是父,answer是子。
PUT my_index
{
  "mappings": {
    "properties": {
      "my_id": {
        "type": "keyword"
      },
      "my_join_field": { 
        "type": "join",
        "relations": {
          "question": "answer" 
        }
      }
    }
  }
}
// 索引父文档,"name": "question"表示插入的是父文档。
PUT my_index/_doc/1
{
  "my_id": "1",
  "text": "This is a question",
  "my_join_field": {
    "name": "question" 
  }
}
PUT my_index/_doc/2
{
  "my_id": "2",
  "text": "This is another question",
  "my_join_field": {
    "name": "question"
  }
}
// 索引子文档,routing和parent要一致
PUT my_index/_doc/3?routing=1
{
  "my_id": "3",
  "text": "This is an answer",
  "my_join_field": {
    "name": "answer", 
    "parent": "1" 
  }
}
PUT my_index/_doc/4?routing=2
{
  "my_id": "4",
  "text": "This is another1 answer",
  "my_join_field": {
    "name": "answer",
    "parent": "2"
  }
}
// search
GET my_index/_search
{
  "query": {
    "match_all": {}
  },
  "sort": ["my_id"]
}
// 查询父文档 根据子文档条件
POST my_index/_search
{
  "query": {
    "has_child": {
      "type": "answer",
      "query": {
        "match": {
          "text": "answer"
        }
      }
    }
  }
}
// 查询子文档 根据父文档条件
POST my_index/_search
{
  "query": {
    "has_parent": {
      "parent_type": "question",
      "query": {
        "match": {
          "text": "question"
        }
      }
    }
  }
}
// 查询子文档 根据父id条件
POST my_index/_search
{
  "query": {
    "parent_id": { 
      "type": "answer",
      "id": "1"
    }

总的来说,

嵌套对象通过冗余数据来提高查询性能,适用于读多写少的场景

父子文档类似关系型数据库中的关联关系,适用于写多的场景,减少了文档修改的范围。

参考

https://www.elastic.co/guide/en/elasticsearch/reference/7.9/query-dsl-parent-id-query.html

es dev tool查看所有索引名称 es 查询所有索引_字段_02