ElasticSearch使用Nested结构如何进行存储KV及聚合查询,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。
下面将讨论如何在ElasticSearch中使用nested结构进行数据的存储、查询和聚合,并结合K-V场景讨论ElasticSearch针对field数量限制的解决方案。
为何要使用Nested结构存储KV(键值对)?
ElasticSearch对于field的数量有限制,默认情况下field的数量如果超过1000个,写入时再创建新的fields就会报错:
java.lang.IllegalArgumentException: Limit of total fields [1000] in index [(index_name)] has been exceeded at org.elasticsearch.index.mapper.MapperService.checkTotalFieldsLimit(MapperService.java:630)
但有些场景的field数量并不是我们能控制的,例如在监控系统中的业务数据所携带的业务标签,其中可能包含了监控系统不能预知的业务字段。
对于这种情景,可能想到的解决方案两个:
-
调整ElasticSearch的配置,增加field的限制数量:这种方案仅仅适用于可以预测出field数量极限的情况,治标不治本,一旦field数量再次抵达限制,又会面临同样的问题。
-
就是使用Pair结构来存储
假设第2种方案的数据结构为:
{ "labels": [{ "key": "ip", "value: "127.0.0.1" }] }, { "labels": [{ "key": "ip", "value: "127.0.0.2" }] }
那么es查询就会存在一个问题,例如下面的查询:
{ "query":{ "bool":{ "must":[ { "match":{ "key":"ip" } }, { "match":{ "value":"127.0.0.1" } } ] } } }
这个查询会把例子中的的数据全部查询出来,并不符合我们的预期。这是因为es在存储索引时,对于普通object类型的field实际上是打平来存储的,比如这样:
{ "labels.key":[ "ip" ], "labels.value":[ "127.0.0.1", "127.0.0.2" ] }
可以看见,索引打平后,对象的关联关系丢失了。对于这种情况,ElasticSearch提供的nested结构可以帮助我们解决类似的问题。Nested结构保留了子文档数据中的关联性,如果labels的数据格式被定义为nested,那么每一个nested object将会作为一个隐藏的单独文本建立索引。如下:
{ "labels.key":"ip", "labels.value":"127.0.0.1" }, { "labels.key":"ip", "labels.value":"127.0.0.2" }
通过分开给每个nested object建索引,object内部的字段间的关系就能保持。当执行查询时,只会匹配’match’同时出现在相同的nested object的结果。
定义mappings
使用nested结构非常简单,指定字段的type为nested即可。下面的例子中定义了一个名为labels的nested结构,其中包含两个字段,分别是key和value。
"mappings": { "demoType": { "labels": { // 字段类型设置为nested "type": "nested", "properties": { "key": { "type": "keyword" }, "value": { "type": "keyword" } } } } }
查询
nested结构的数据查询和普通object略有不同,nested object作为一个独立隐藏文档单独建索引,因此,不能直接查询到它们。取而代之,我们必须使用nested查询或者nested filter。例如:
{ "query": { "bool": { "must": [ { "nested": { "path": "labels", "query": { "bool": { "must": [ { "term": { "labels.key": "ip" } }, { "term": { "labels.value": "127.0.0.1" } } ] } } } } ] } } }
这个查询可以返回我们预期的正确结果:
[{ "labels": { "key": "ip", "value": "127.0.0.1" } }]
分桶聚合
查询的问题解决了,聚合时问题又来了,前面我们说到,nested结构存储在一个隐藏的单独文本索引中,那么普通的聚合查询自然便无法访问到它们。因此,nested结构在聚合时,需要使用特定的nested聚合。
nested聚合
假设es中存储如下数据:
[{ "labels": [{ "key": "ip", "value": "127.0.0.1" },{ "key": "os", "value": "windows" }] }, { "labels": [{ "key": "ip", "value": "127.0.0.2" },{ "key": "os", "value": "linux" }] }]
我们要聚合所有对labels.value
进行聚合,可以使用下面的方式:
{ "size": 0, "aggs": { "labels_nested": { "nested": { "path": "labels" }, "aggs": { "nested_value": { "terms": { "field": "labels.value" } } } } } }
这个查询将会得到下面类似的结果:
{ "aggregations": { "labels_nested": { "doc_count": 2, "nested_value": { "buckets": [ { "doc_count": 1, "key": "127.0.0.1" }, { "doc_count": 1, "key": "127.0.0.2" }, { "doc_count": 1, "key": "windows" }, { "doc_count": 1, "key": "linux" } ] } } } }
过滤属性值
上面的例子可以看到,其只是单纯的将所有的value进行了聚合,并没有针对k-v中的key进行过滤,因此导致labels.key
为ip
和os
的数据均被统计到了其中,这通常不符合我们实际场景中的需求。
现在假设要对所有labels.key
为ip
的labels.value
进行聚合,那么可以使用如下的方式:
{ "size": 0, "aggs": { "labels_nested": { "nested": { "path": "labels" }, "aggs": { "nested_ip": { "filter": { "term": { "labels.key": "ip" } }, "aggs": { "nested_value": { "terms": { "field": "labels.value" } } } } } } } }
通过这样的方式就可以把labels.key
不是ip
的文档过滤掉,经过这个查询将得到类似如下的结果:
{ "aggregations": { "labels_nested": { "doc_count": 2, "nested_ip": { "doc_count": 2, "nested_value": { "buckets": [ { "doc_count": 1, "key": "127.0.0.1" }, { "doc_count": 1, "key": "127.0.0.2" } ] } } } } }
nested多重聚合
如果想在nested聚合下嵌套聚合其它字段,直接嵌套是不行的,这里需要使用到reverse_nested
跳出当前nested聚合后,再进行嵌套聚合。
注意:无论是嵌套其它nested字段还是普通字段,都需要使用reverse_nested跳出当前nested聚合。
例如想对labels.key
为ip
聚合后,再对labels.key
为os
进行聚合:
{ "size": 0, "aggs": { "labels_nested": { "nested": { "path": "labels" }, "aggs": { "nested_ip": { "filter": { "term": { "labels.key": "ip" } }, "aggs": { "nested_ip_value": { "terms": { "field": "labels.value" }, "aggs": { "reverse_labels": { "reverse_nested": {}, //注意这里 "aggs": { "nested_os": { "nested": { "path": "labels" }, "aggs": { "labels_os": { "filter": { "term": { "labels.key": "os" } }, "aggs": { "labels_os_value": { "terms": { "field": "labels.value" } } } } } } } } } } } } } } } }
如此,将得到类似下面的结果:
{ "aggregations": { "labels_nested": { "doc_count": 2, "nested_ip": { "nested_ip_value": { "buckets": [ { "doc_count": 1, "reverse_labels": { "doc_count": 1, "nested_os": { "labels_os": { "doc_count": 1, "labels_os_value": { "buckets": [ { "doc_count": 1, "key": "windows" } ] } }, "doc_count": 1 } }, "key": "127.0.0.1" }, { "doc_count": 1, "reverse_labels": { "doc_count": 1, "nested_os": { "labels_os": { "doc_count": 1, "labels_os_value": { "buckets": [ { "doc_count": 1, "key": "linux" } ] } }, "doc_count": 1 } }, "key": "127.0.0.2" } ] }, "doc_count": 2 } } } }
结语
至此,关于nested结构存储K-V的用法就介绍完啦!使用nested结构可以帮助我们保持object内部的关联性,借此解决elasticsearch对field数量的限制。nested结构不仅可以应用在K-V结构的场景,还可以应用于其它任何需要保持object内部关联性的场景。
注意:使用nested结构也会存在一些问题:
-
增加,改变或者删除一个nested文本,整个文本必须重新建索引。nested文本越多,代价越大。
-
检索请求会返回整个文本,而不仅是匹配的nested文本。尽管有计划正在执行以能够支持返回根文本的同时返回最匹配的nested文本,但目前还未实现。
看完上述内容,你们掌握ElasticSearch使用Nested结构如何进行存储KV及聚合查询的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注亿速云行业资讯频道,感谢各位的阅读!
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/223169.html