一起来学ES —— 浅谈Nested结构

1 分钟 读完全文

Nested是什么?

  • 直观的说,Nested实际上就是Object的数组。如下,这个user就是个nested结构
    {
    "user" : [ 
      {
        "first" : "John",
        "last" :  "Smith"
      },
      {
        "first" : "Alice",
        "last" :  "White"
      }
    ]
    }
    

Nested 和 Object 是什么关系?

  • ES原生支持Object类型,也就是任意字段都可以是个对象,而ES又是所有字段都是多值,也就是都可以是list。那么在ES中Nested和Object List又是什么关系呢?
  • 这就要从Object说起了。Object虽然是个对象,但是实际存储时是在当前文档里打平存储的。如上那个例子,如果只有一个user,那么在真实索引中实际上是下面这样的
{
  "user.first" : "John",
  "user.last" : "Smith"
}
  • 而如果是个list,那么就成了
{
  "user.first" : ["John","Alice"],
  "user.last" : ["Smith","White"]
}
  • 因为建索引时打平,因此检索时ES就无法知道到底是John Smith还是John White了。因此引入了Nested结构。
  • Nested将list里的每个doc单独变成子文档进行存储,因此在查询时就可以知道具体的结构信息了。

Nested 查询要注意什么?

  • Nested因为是单独的子文档存储,因此在使用时,直接用 a.b.c 是无法访问的,需要将其套在nested查询里。除此之外,和其他的查询并无差异。
{
  "query": {
    "nested": {
      "path": "user",
      "query": {
        "match": {"user.first" : "John"}
      },
      "inner_hits": {}
    }
  }
}
  • 如上所示,用一个nested套住真实query即可。默认的hit是返回父文档,也就是大的doc。如果加上inner_hits会在父文档的source中多一个inner_hits的字段,返回真实命中的object,其中有个offset表明list数组下标。
  • 需要注意的是,由于单独存储很耗资源,因此默认一个index最多只有50个nested字段。此外,虽然nested是单独存储的,但是其字段数也算入index总字段数,默认最多1000个。

Nested Aggregation是什么?

  • 对于Nested结构,有一点需要谨记的,就是他是个List结构。Nested Agg就是对这个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"
              }
            }
          }
        }
      ]
    }
  }
}

动态kv如何做agg呢?

  • 普通查询的确很简单,但是agg就并不简单了。原来的模式可以直接用真实字段tags.k1做agg,但是在nested里k1已经变成了一个字段的值,因此没法直接做agg了。
  • 这时就需要引入script大法了。其实agg的本质就是从每个doc的正排里取一个值,用这个值做聚合。因此我们只需要用script遍历list,找到对应的key然后返回其value即可。
  • 简单写了个如下所示,如果有更好的方法欢迎留言。
  • 注意!!! 由于nested单独存储,因此doc里并没有nested数据,需要用params从source中拿。性能很差,仅可用于少量数据场景!
{ ...
  "aggs": {
    "test_agg": {
      "terms": {
        "script": {
          "inline": "for(int i=0;i<params['_source']['tags'].length;i++){if(params['_source']['tags'][i]['key']=='k1'){return params['_source']['tags'][i]['value']}}",
          "lang": "painless"
        },
        "size": 5
      }
    }
  }
}
  • 在使用中我们还可以把script存在来,来加速运算,减少缓存。(注意:5.6以后将code改为了source字段,具体写法参阅文档)
POST _scripts/is_tag_key
{
  "script":{
    "lang": "painless",
    "code":"for(int i=0;i<params['_source']['tags'].length;i++){if(params['_source']['tags'][i]['key']==params.key){return params['_source']['tags'][i]['value']}}"
  }
}
  • 这样用起来就简单多了
{ ...
  "aggs": {
    "test_agg": {
      "terms": {
        "script": {
          "stored": "is_tag_key",
          "params": {
            "key": "k1"
          }
        },
        "size": 5
      }
    }
  }
}

怎么在kibana里做agg呢?

  • kibana其实和上面的一样,也是用script.不过只支持inline的,在script field配置。不过注意一定不能太多,因为每一个inline script都是一个单独的script都需要消耗存储资源。

参考资料

版权声明

  • 自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 本文首发于: http://czjxy881.coding.me/

留下评论