自建博客地址:https://www.bytelife.net,歡迎訪問! 本文為博客同步發表文章,為了更好的閱讀體驗,建議您移步至我的博客👇
本文作者: Jeffrey
本文鏈接: https://www.bytelife.net/articles/51440.html
版權聲明: 本博客所有文章除特別聲明外,均采用 BY-NC-SA 許可協議。轉載請注明出處!
本文將討論如何在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文本,但目前還未實現。