
背景介紹
需要對索引中的做聚合,但是聚合的條件會比較復雜,并非從單一字段進行聚合。參考如下數(shù)據(jù)結構
{
//空格分詞字段
"feild_a": "1 2 3 4 5",
//keyword字段
"feild_b": "2"
}
要做的事:我們有 1 2 3 …… 等id,需要統(tǒng)計索引里面,a和b字段分別含有1 2 3對應的數(shù)量。文字難以表述的需求我們轉化成數(shù)據(jù)結構來看
{
"data": [{
"id": "1",
"count": 1
}, {
"id": "2",
"count": 2
}, {
"id": "3",
"count": 1
}]
}
如果只有上面那一條數(shù)據(jù)的話,那么應該得出下面這個統(tǒng)計結果。
問題分析
從表面剖析這個需求的話,似乎是個多字段的聚合。相對于單字段聚合來說問題還是比較棘手的。并不能簡單的通過單字段的聚合來解決問題,我們先從最簡單的情況開始處理問題。以統(tǒng)計1 2 3這3個id來舉例子。
1.單字段情況下聚合
假設只需要對一個字段聚合,比如b字段,b字段是keyword類型,需要考慮的情況最為簡單,當要對b字段聚合時語句很好寫,如下即可
{
"from": 0,
"size": 0,
"query": {
"bool": {
"must": [{
"bool": {
"should": [{
"terms": {
"field_a": ["1", "2", "3"],
"boost": 1.0
}
}, {
"terms": {
"field_b": ["1", "2", "3"],
"boost": 1.0
}
}],
"adjust_pure_negative": true,
"minimum_should_match": "1",
"boost": 1.0
}
}],
"adjust_pure_negative": true,
"boost": 1.0
}
},
"aggregations": {
"my_agg": {
"terms": {
"field": "field_b"
}
}
}
}
這是完整的query,后面的查詢會省略掉query部分。query部分的用處也很明顯:只把需要做聚合的部分過濾出來做聚合,我們需要統(tǒng)計的數(shù)據(jù)就在這部分中,而不是整個索引庫。這樣有兩個好處:
1.提高效率,減少需要聚合的數(shù)據(jù)的數(shù)量
2.剔除需要考慮的意外情況,降低語句的復雜度
而聚合部分就非常簡單了,僅僅對field_b聚合即可,但是很遺憾,離我們最終目標很遠,這樣只能統(tǒng)計出b字段的數(shù)據(jù)分布情況。
2.多字段情況的聚合
相對于上面的那種,接下來把另外一個字段也考慮進來看看。所以我們寫下了這樣的請求語句:
"aggregations": {
"my_agg1": {
"terms": {
"field": "tag_brand_id"
}
},
"my_agg2": {
"terms": {
"field": "brand_cid_array"
}
}
}
勉強的可以看到確實也是“統(tǒng)計了兩個字段的情況”,但是是分開的,意味著要自己去解析返回結果并做計算來得到最終的返回結果。這確實是很令人惡心的事,那還有沒有其他辦法呢。但是觀察語句的結構發(fā)現(xiàn),似乎并沒有過多可以更改的余地,所以需要尋求其他靈活的解決辦法。
3.script agg的聚合
簡單的單聚合無法表達出多字段聚合的需求,在谷歌過后我尋找到了這樣一種解決方案:使用script,即腳本來描述我的需求。下面這段agg就是為了表達我想要根據(jù)我的需求靈活處理的一個方式:
"aggregations": {
"my_agg1": {
"terms": {
"script": " if (doc['field_a'].values.contains('1') || doc['field_b'].values.contains('1')){1};if (doc['field_a'].values.contains('2') || doc['field_b'].values.contains('2')){2};
if (doc['field_a'].values.contains('3') || doc['field_b'].values.contains('3')){3};"
}
}
}
這一段腳本的作用很明顯,就是告訴es:當a字段或者b字段包括1的時候,扔到桶1;當a字段或者b字段包括2的時候,扔到桶2;……以此類推。看上去確實似乎完全解決了開頭提出來的問題,驗證后效率還能接受,不是特別慢。但是正當我沾沾自喜以為解決了問題的時候,隨手驗證了另外一個case,就直接冷水潑頭了:
a字段和b字段是可能包含同一個id比如2,但是對于統(tǒng)計結果來說要求算作一條。
用上面這個腳本并無法體現(xiàn)出這個區(qū)別,而且還會有一個問題:
請求123和請求321時會返回不同統(tǒng)計結果
因為ifelse語句的關系,和||的性質(zhì),在滿足條件1后便會扔到桶1,而無法在去后續(xù)條件中判斷。這個腳本有很明顯的bug存在。但是painless畢竟是腳本,可以使用的API和關鍵字都非常有限,寫的復雜了還會很嚴重影響效率,無奈這個方案也只能pass,即使它看上去差點解決了我的問題。
4.filter agg的聚合
在重新看了官方文檔后,我發(fā)現(xiàn)了agg中的一個用法,filter agg。
filter agg的用法其實很簡單,但是全意外的和我的需求很契合。之前忽視掉這個用法的主要原因是看到的示例都是對單字段做聚合。那如何同時聚合多個字段呢?從API入手驗證是否可以使用比較靈活的寫法
public KeyedFilter(String key, QueryBuilder filter) {
if (key == null) {
throw new IllegalArgumentException("[key] must not be null");
}
if (filter == null) {
throw new IllegalArgumentException("[filter] must not be null");
}
this.key = key;
this.filter = filter;
}
這是es提供的javaapi中filter agg的構造函數(shù),key就是過濾名稱,filter就是過濾條件。而且很友好的是,filter類型為QueryBuilder,也就是說,可以做成比較復雜的過濾方式。
"aggregations": {
"batch_count": {
"filters": {
"filters": {
"1": {
"bool": {
"should": [{
"term": {
"field_a": {
"value": "1",
"boost": 1.0
}
}
}, {
"term": {
"field_b": {
"value": "1",
"boost": 1.0
}
}
}],
"adjust_pure_negative": true,
"boost": 1.0
}
},
"2": {
"bool": {
"should": [{
"term": {
"field_a": {
"value": "2",
"boost": 1.0
}
}
}, {
"term": {
"field_b": {
"value": "2",
"boost": 1.0
}
}
}],
"adjust_pure_negative": true,
"boost": 1.0
}
},
"3": {
"bool": {
"should": [{
"term": {
"field_a": {
"value": "3",
"boost": 1.0
}
}
}, {
"term": {
"field_b": {
"value": "3",
"boost": 1.0
}
}
}],
"adjust_pure_negative": true,
"boost": 1.0
}
}
},
"other_bucket": false,
"other_bucket_key": "-1"
}
}
}
這就是最后成型的agg塊
問題總結
agg模塊的開發(fā)是比較麻煩的,首先性能問題比較困擾,其次語句編寫遠沒有query模塊的靈活。這次順利解決需求,記錄。