本文将讨论如何在 ElasticSearch 中使用 nested 结构进行数据的存储、查询和聚合,并结合 K-V 场景讨论 ElasticSearch 针对 field 数量限制的解决方案。
为何要使用 Nested 结构存储 KV(键值对)? ElasticSearch 对于 field 的数量有限制,默认情况下 field 的数量如果超过 1000 个,写入时再创建新的 fields 就会报错:
1 2 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 种方案的数据结构为:
1 2 3 4 5 6 7 8 9 10 11 12 { "labels" : [{ "key" : "ip" , "value" : "127.0.0.1" }] }, { "labels" : [{ "key" : "ip" , "value" : "127.0.0.2" }] }
那么 es 查询就会存在一个问题,例如下面的查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { "query" :{ "bool" :{ "must" :[ { "match" :{ "key" :"ip" } }, { "match" :{ "value" :"127.0.0.1" } } ] } } }
这个查询会把例子中的的数据全部查询出来,并不符合我们的预期。这是因为 es 在存储索引时,对于普通 object 类型的 field 实际上是打平来存储的,比如这样:
1 2 3 4 5 6 7 8 9 { "labels.key" :[ "ip" ], "labels.value" :[ "127.0.0.1" , "127.0.0.2" ] }
可以看见,索引打平后,对象的关联关系丢失了。对于这种情况,ElasticSearch 提供的 nested 结构可以帮助我们解决类似的问题。Nested 结构保留了子文档数据中的关联性,如果 labels 的数据格式被定义为 nested,那么每一个 nested object 将会作为一个隐藏的单独文本建立索引。如下:
1 2 3 4 5 6 7 8 { "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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "mappings" : { "demoType" : { "labels" : { "type" : "nested" , "properties" : { "key" : { "type" : "keyword" }, "value" : { "type" : "keyword" } } } } } }
查询 nested 结构的数据查询和普通 object 略有不同,nested object 作为一个独立隐藏文档单独建索引,因此,不能直接查询到它们。取而代之,我们必须使用 nested 查询或者 nested filter。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 { "query" : { "bool" : { "must" : [ { "nested" : { "path" : "labels" , "query" : { "bool" : { "must" : [ { "term" : { "labels.key" : "ip" } }, { "term" : { "labels.value" : "127.0.0.1" } } ] } } } } ] } } }
这个查询可以返回我们预期的正确结果:
1 2 3 4 5 6 [{ "labels" : { "key" : "ip" , "value" : "127.0.0.1" } }]
分桶聚合 查询的问题解决了,聚合时问题又来了,前面我们说到,nested 结构存储在一个隐藏的单独文本索引中,那么普通的聚合查询自然便无法访问到它们。因此,nested 结构在聚合时,需要使用特定的 nested 聚合。
nested 聚合 假设 es 中存储如下数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [{ "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
进行聚合,可以使用下面的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "size" : 0 , "aggs" : { "labels_nested" : { "nested" : { "path" : "labels" }, "aggs" : { "nested_value" : { "terms" : { "field" : "labels.value" } } } } } }
这个查询将会得到下面类似的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 { "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
进行聚合,那么可以使用如下的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 { "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
的文档过滤掉,经过这个查询将得到类似如下的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "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
进行聚合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 { "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" } } } } } } } } } } } } } } } }
如此,将得到类似下面的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 { "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 文本,但目前还未实现。