ES 添加对象Nested和Object 的区别

Object类型

ES原生支持Object类型,也就是任意字段都可以是个对象,而ES又是所有字段都支持多值,即都可以是list。es的object类型虽然是对象类型,但是数据是打平存储的。

如下,声明一个对象,新增1条数据:

DELETE /test-index
 
PUT /test-index
{
    "settings": {
        "number_of_shards": 8,
        "number_of_replicas": 1,
        "codec": "best_compression"
    },
    "mappings": {
        "test-type": {
            "dynamic": "true",
            "_routing": {
                "required": false
            },
            "_all": {
                "enabled": false
            },
            "properties": {
                "keywordsWithCount": {
                    "dynamic": "false",
                    "properties": {
                        "keyword": {
                            "type": "keyword"
                        },
                        "count": {
                            "type": "keyword"
                        }
                    }
                },
                "companyName": {
                    "type": "keyword"
                }
            }
        }
    }
}
 
POST /test-index/test-type/1
{
  "companyName": "大富翁",
  "keywordsWithCount": [
    {
      "keyword": "NP0001",
      "count": "5"
    },
    {
      "keyword": "NP0002",
      "count": "15"
    }
  ]
}
 
GET /test-index/_search

但实际存储的时候,是打平这样存储的:

{
  "companyName" : "大富翁",
  "keywordsWithCount.keyword": ["NP0001", "NP0002"],
  "keywordsWithCount.count": ["1", "2"]
}

就丢失了keyword和count之间的关联关系,就不知道谁是 1谁是2了。所以,这样查询也能查询出结果:

GET /test-index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "keywordsWithCount.keyword": "NP0001"
          }
        },
        {
          "term": {
            "keywordsWithCount.count": "2"
          }
        }
      ]
    }
  }
}

返参:
"hits" : [
      {
        "_index" : "test-index",
        "_type" : "test-type",
        "_id" : "1",
        "_source" : {
          "companyName" : "大富翁",
          "keywordsWithCount" : [
            {
              "keyword" : "NP0001",
              "count" : "1"
            },
            {
              "keyword" : "NP0002",
              "count" : "2"
            }
          ]
        }
      }
    ]

可是NP0001是1,NP0002才是2 所以,为解决es object类型的数据扁平化存储问题,引入了nested类型。

Nested类型

nested类型:嵌套文档,对象数组的优先选择类型。Nested将数组中的每个对象作为单独的隐藏文档(hidden separate document)进行索引。

解决问题:对象数组的多字段匹配查询。

在独立索引每一个嵌套对象后,对象中每个字段的相关性得以保留。我们查询时,也仅仅返回那些真正符合条件的文档。

不仅如此,由于嵌套文档直接存储在文档内部,查询时嵌套文档和根文档联合成本很低,速度和单独存储几乎一样。

嵌套文档是隐藏存储的,我们不能直接获取。如果要增删改一个嵌套对象,我们必须把整个文档重新索引才可以。值得注意的是,查询的时候返回的是整个文档,而不是嵌套文档本身。

如果需要索引对象数组而不是单个对象,优先考虑使用嵌套数据类型Nested。

如果不需要对 Nested 子文档精确搜索的就选型 object,需要的选型 Nested。

nested类型的定义在声明时指定 "type": "nested" 即可!

DELETE /test-index-2

PUT test-index-2
{
    "settings": {
        "number_of_shards": 8,
        "number_of_replicas": 1,
        "codec": "best_compression"
    },
    "mappings": {
        "test-type": {
            "dynamic": "true",
            "_routing": {
                "required": false
            },
            "_all": {
                "enabled": false
            },
            "properties": {
                "keywordsWithCount": {
                    "type": "nested",
                    "dynamic": "false",
                    "properties": {
                        "keyword": {
                            "type": "keyword"
                        },
                        "count": {
                            "type": "keyword"
                        }
                    }
                },
                "companyName": {
                    "type": "keyword"
                }
            }
        }
    }
}

POST /test-index-2/test-type/1
{
  "companyName": "大富翁",
  "keywordsWithCount": [
    {
      "keyword": "NP0001",
      "count": "5"
    },
    {
      "keyword": "NP0002",
      "count": "15"
    }
  ]
}
 
GET /test-index-2/_search

Nested 因为是单独的子文档存储,因此在使用时,直接用 a.b.c 是无法访问的,需要将其套在nested查询里,且需要指定 "path" 。

GET /test-index-2/_search
{
  "query": {
    "nested": {
      "path": "keywordsWithCount",
      "query": {
        "bool": {
          "must": [
            {
              "term": {
                "keywordsWithCount.keyword": "NP0001"
              }
            },
            {
              "term": {
                "keywordsWithCount.count": "2"
              }
            }
          ]
        }
      }
    }
  }
}

这时返回的数据为空,满足我们的需求。

Nested注意点:

由于单独存储很耗资源,因此默认一个index最多只有50个nested字段。此外,虽然nested是单独存储的,但是其字段数也算入index总字段数,默认最多1000个。

Nested结构是个List结构。Nested Aggregation就是对这个list做agg操作,agg写法和普通的一样,只需要在外面套上nested即可。

能否用Nested做动态kv?

Nested除了存储固定的Object List,还有一种常用的场景就是用来存储动态的KV。虽然ES天然支持dynamic mapping,但是其key都是固化在每一个doc中的,如果存储用户自定义报表数据。每个用户的key差异很大,放在同一张表会出现大量空值。这是很浪费系统资源的行为,并且随着Key的不断增多,最终会超出index的最大key数量。

因此用nested结构来处理这种动态kv就比较合适。 nested的本质就是将

{"tags":{"k1":"v1","k2":"v2"}}
=>
{"tags":[{"key":"key1","value":"v1"},{"key":"key2","value":"v2"}]}

这样一来就可以轻松处理动态kv。并且查询依旧简单,例如k1:v1 AND k2:v2变为

{
  "query": {
    "bool": {
      "must": [
        {
          "nested": {
            "path": "tags",
            "query": {
              "query_string": {
                "query": "tags.key:k1 AND tags.value:v1"
              }
            }
          }
        },
        {
          "nested": {
            "path": "tags",
            "query": {
              "query_string": {
                "query": "tags.key:k2 AND tags.value:v2"
              }
            }
          }
        }
      ]
    }
  }
}

Nested 新增或更新子文档操作,为什么需要更新整个文档?

嵌套 Nested 文档在物理上位于根文档旁边的 Lucene 段中。这是为什么当只想更改单个嵌套文档时必须重建根文档和所有嵌套 Nested 文档的原因。

参考文档:

嵌套对象 | Elasticsearch: 权威指南 | Elastic