Skip to content

Commit fbc0c0a

Browse files
oliveredungnx-teko
authored andcommitted
Add MultiTerms bucket aggregation
This commit adds the Multi Terms bucket aggregation as described here: https://www.elastic.co/guide/en/elasticsearch/reference/7.13/search-aggregations-bucket-multi-terms-aggregation.html
1 parent d771c7c commit fbc0c0a

File tree

4 files changed

+175
-35
lines changed

4 files changed

+175
-35
lines changed

search_aggs.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,21 @@ func (a Aggregations) Terms(name string) (*AggregationBucketKeyItems, bool) {
369369
return nil, false
370370
}
371371

372+
// MultiTerms returns multi terms aggregation results.
373+
// See: https://www.elastic.co/guide/en/elasticsearch/reference/7.13/search-aggregations-bucket-multi-terms-aggregation.html
374+
func (a Aggregations) MultiTerms(name string) (*AggregationBucketMultiKeyItems, bool) {
375+
if raw, found := a[name]; found {
376+
agg := new(AggregationBucketMultiKeyItems)
377+
if raw == nil {
378+
return agg, true
379+
}
380+
if err := json.Unmarshal(raw, agg); err == nil {
381+
return agg, true
382+
}
383+
}
384+
return nil, false
385+
}
386+
372387
// SignificantTerms returns significant terms aggregation results.
373388
// See: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/search-aggregations-bucket-significantterms-aggregation.html
374389
func (a Aggregations) SignificantTerms(name string) (*AggregationBucketSignificantTerms, bool) {
@@ -1342,6 +1357,71 @@ func (a *AggregationBucketKeyItem) UnmarshalJSON(data []byte) error {
13421357
return nil
13431358
}
13441359

1360+
// AggregationBucketMultiKeyItems is a bucket aggregation that is returned
1361+
// with a multi terms aggregation.
1362+
type AggregationBucketMultiKeyItems struct {
1363+
Aggregations
1364+
1365+
DocCountErrorUpperBound int64 //`json:"doc_count_error_upper_bound"`
1366+
SumOfOtherDocCount int64 //`json:"sum_other_doc_count"`
1367+
Buckets []*AggregationBucketMultiKeyItem //`json:"buckets"`
1368+
Meta map[string]interface{} // `json:"meta,omitempty"`
1369+
}
1370+
1371+
// UnmarshalJSON decodes JSON data and initializes an AggregationBucketMultiKeyItems structure.
1372+
func (a *AggregationBucketMultiKeyItems) UnmarshalJSON(data []byte) error {
1373+
var aggs map[string]json.RawMessage
1374+
if err := json.Unmarshal(data, &aggs); err != nil {
1375+
return err
1376+
}
1377+
if v, ok := aggs["doc_count_error_upper_bound"]; ok && v != nil {
1378+
json.Unmarshal(v, &a.DocCountErrorUpperBound)
1379+
}
1380+
if v, ok := aggs["sum_other_doc_count"]; ok && v != nil {
1381+
json.Unmarshal(v, &a.SumOfOtherDocCount)
1382+
}
1383+
if v, ok := aggs["buckets"]; ok && v != nil {
1384+
json.Unmarshal(v, &a.Buckets)
1385+
}
1386+
if v, ok := aggs["meta"]; ok && v != nil {
1387+
json.Unmarshal(v, &a.Meta)
1388+
}
1389+
a.Aggregations = aggs
1390+
return nil
1391+
}
1392+
1393+
// AggregationBucketMultiKeyItem is a single bucket of an AggregationBucketMultiKeyItems structure.
1394+
type AggregationBucketMultiKeyItem struct {
1395+
Aggregations
1396+
1397+
Key []interface{} //`json:"key"`
1398+
KeyAsString *string //`json:"key_as_string"`
1399+
KeyNumber []json.Number
1400+
DocCount int64 //`json:"doc_count"`
1401+
}
1402+
1403+
// UnmarshalJSON decodes JSON data and initializes an AggregationBucketMultiKeyItem structure.
1404+
func (a *AggregationBucketMultiKeyItem) UnmarshalJSON(data []byte) error {
1405+
var aggs map[string]json.RawMessage
1406+
dec := json.NewDecoder(bytes.NewReader(data))
1407+
dec.UseNumber()
1408+
if err := dec.Decode(&aggs); err != nil {
1409+
return err
1410+
}
1411+
if v, ok := aggs["key"]; ok && v != nil {
1412+
json.Unmarshal(v, &a.Key)
1413+
json.Unmarshal(v, &a.KeyNumber)
1414+
}
1415+
if v, ok := aggs["key_as_string"]; ok && v != nil {
1416+
json.Unmarshal(v, &a.KeyAsString)
1417+
}
1418+
if v, ok := aggs["doc_count"]; ok && v != nil {
1419+
json.Unmarshal(v, &a.DocCount)
1420+
}
1421+
a.Aggregations = aggs
1422+
return nil
1423+
}
1424+
13451425
// -- Bucket types for significant terms --
13461426

13471427
// AggregationBucketSignificantTerms is a bucket aggregation returned

search_aggs_bucket_multi_terms.go

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44

55
package elastic
66

7-
import "fmt"
8-
9-
// A multi-bucket value source based aggregation where buckets are dynamically built - one per
10-
// unique set of values. The multi terms aggregation is very similar to the terms aggregation,
11-
// however in most cases it will be slower than the terms aggregation and will consume more
12-
// memory. Therefore, if the same set of fields is constantly used, it would be more efficient to
13-
// index a combined key for this fields as a separate field and use the terms aggregation on this field.
7+
// MultiTermsAggregation is a multi-bucket value source based aggregation
8+
// where buckets are dynamically built - one per unique set of values.
9+
// The multi terms aggregation is very similar to the terms aggregation,
10+
// however in most cases it will be slower than the terms aggregation and will
11+
// consume more memory. Therefore, if the same set of fields is constantly
12+
// used, it would be more efficient to index a combined key for this fields
13+
// as a separate field and use the terms aggregation on this field.
1414
//
15-
// See: https://www.elastic.co/guide/en/elasticsearch/reference/7.12/search-aggregations-bucket-multi-terms-aggregation.html
15+
// See https://www.elastic.co/guide/en/elasticsearch/reference/7.13/search-aggregations-bucket-multi-terms-aggregation.html
1616
type MultiTermsAggregation struct {
17-
multiTerms []*MultiTerm
17+
multiTerms []MultiTerm
1818
subAggregations map[string]Aggregation
1919
meta map[string]interface{}
2020

@@ -27,14 +27,30 @@ type MultiTermsAggregation struct {
2727
order []MultiTermsOrder
2828
}
2929

30+
// NewMultiTermsAggregation initializes a new MultiTermsAggregation.
3031
func NewMultiTermsAggregation() *MultiTermsAggregation {
3132
return &MultiTermsAggregation{
3233
subAggregations: make(map[string]Aggregation),
3334
}
3435
}
3536

36-
func (a *MultiTermsAggregation) Terms(multiTerm ...*MultiTerm) *MultiTermsAggregation {
37-
a.multiTerms = multiTerm
37+
// Terms adds a slice of field names to return in the aggregation.
38+
//
39+
// Notice that it appends to existing terms, so you can use Terms more than
40+
// once, and mix with MultiTerms method.
41+
func (a *MultiTermsAggregation) Terms(fields ...string) *MultiTermsAggregation {
42+
for _, field := range fields {
43+
a.multiTerms = append(a.multiTerms, MultiTerm{Field: field})
44+
}
45+
return a
46+
}
47+
48+
// MultiTerms adds a slice of MultiTerm instances to return in the aggregation.
49+
//
50+
// Notice that it appends to existing terms, so you can use MultiTerms more
51+
// than once, and mix with Terms method.
52+
func (a *MultiTermsAggregation) MultiTerms(multiTerms ...MultiTerm) *MultiTermsAggregation {
53+
a.multiTerms = append(a.multiTerms, multiTerms...)
3854
return a
3955
}
4056

@@ -206,9 +222,6 @@ func (a *MultiTermsAggregation) Source() (interface{}, error) {
206222
// ValuesSourceAggregationBuilder
207223
terms := make([]interface{}, len(a.multiTerms))
208224
for i := range a.multiTerms {
209-
if a.multiTerms[i] == nil {
210-
return nil, fmt.Errorf("expected a multiterm but found a nil multiterm")
211-
}
212225
s, err := a.multiTerms[i].Source()
213226
if err != nil {
214227
return nil, err
@@ -288,26 +301,16 @@ func (order *MultiTermsOrder) Source() (interface{}, error) {
288301

289302
// MultiTerm specifies a single term field for a multi terms aggregation.
290303
type MultiTerm struct {
291-
field string
292-
missing interface{}
304+
Field string
305+
Missing interface{}
293306
}
294307

295308
// Source returns serializable JSON of the MultiTerm.
296309
func (term *MultiTerm) Source() (interface{}, error) {
297310
source := make(map[string]interface{})
298-
source["field"] = term.field
299-
if term.missing != nil {
300-
source["missing"] = term.missing
311+
source["field"] = term.Field
312+
if term.Missing != nil {
313+
source["missing"] = term.Missing
301314
}
302315
return source, nil
303316
}
304-
305-
// Missing configures the value to use when document miss a value
306-
func (term *MultiTerm) Missing(missing interface{}) *MultiTerm {
307-
term.missing = missing
308-
return term
309-
}
310-
311-
func NewMultiTerm(field string) *MultiTerm {
312-
return &MultiTerm{field: field}
313-
}

search_aggs_bucket_multi_terms_test.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
)
1111

1212
func TestMultiTermsAggregation(t *testing.T) {
13-
agg := NewMultiTermsAggregation().Terms(NewMultiTerm("genre"), NewMultiTerm("product"))
13+
agg := NewMultiTermsAggregation().Terms("genre", "product")
1414
src, err := agg.Source()
1515
if err != nil {
1616
t.Fatal(err)
@@ -26,9 +26,29 @@ func TestMultiTermsAggregation(t *testing.T) {
2626
}
2727
}
2828

29+
func TestMultiTermsAggregationWithMultiTerms(t *testing.T) {
30+
agg := NewMultiTermsAggregation().MultiTerms(
31+
MultiTerm{Field: "genre", Missing: "n/a"},
32+
MultiTerm{Field: "product", Missing: "n/a"},
33+
)
34+
src, err := agg.Source()
35+
if err != nil {
36+
t.Fatal(err)
37+
}
38+
data, err := json.Marshal(src)
39+
if err != nil {
40+
t.Fatalf("marshaling to JSON failed: %v", err)
41+
}
42+
got := string(data)
43+
expected := `{"multi_terms":{"terms":[{"field":"genre","missing":"n/a"},{"field":"product","missing":"n/a"}]}}`
44+
if got != expected {
45+
t.Errorf("expected\n%s\n,got:\n%s", expected, got)
46+
}
47+
}
48+
2949
func TestMultiTermsAggregationWithSubAggregation(t *testing.T) {
3050
subAgg := NewAvgAggregation().Field("height")
31-
agg := NewMultiTermsAggregation().Terms(NewMultiTerm("genre"), NewMultiTerm("product")).Size(10).
51+
agg := NewMultiTermsAggregation().Terms("genre", "product").Size(10).
3252
OrderByAggregation("avg_height", false)
3353
agg = agg.SubAggregation("avg_height", subAgg)
3454
src, err := agg.Source()
@@ -49,7 +69,7 @@ func TestMultiTermsAggregationWithSubAggregation(t *testing.T) {
4969
func TestMultiTermsAggregationWithMultipleSubAggregation(t *testing.T) {
5070
subAgg1 := NewAvgAggregation().Field("height")
5171
subAgg2 := NewAvgAggregation().Field("width")
52-
agg := NewMultiTermsAggregation().Terms(NewMultiTerm("genre"), NewMultiTerm("product")).Size(10).
72+
agg := NewMultiTermsAggregation().Terms("genre", "product").Size(10).
5373
OrderByAggregation("avg_height", false)
5474
agg = agg.SubAggregation("avg_height", subAgg1)
5575
agg = agg.SubAggregation("avg_width", subAgg2)
@@ -69,7 +89,7 @@ func TestMultiTermsAggregationWithMultipleSubAggregation(t *testing.T) {
6989
}
7090

7191
func TestMultiTermsAggregationWithMetaData(t *testing.T) {
72-
agg := NewMultiTermsAggregation().Terms(NewMultiTerm("genre"), NewMultiTerm("product")).Size(10).OrderByKeyDesc()
92+
agg := NewMultiTermsAggregation().Terms("genre", "product").Size(10).OrderByKeyDesc()
7393
agg = agg.Meta(map[string]interface{}{"name": "Oliver"})
7494
src, err := agg.Source()
7595
if err != nil {
@@ -87,7 +107,10 @@ func TestMultiTermsAggregationWithMetaData(t *testing.T) {
87107
}
88108

89109
func TestMultiTermsAggregationWithMissing(t *testing.T) {
90-
agg := NewMultiTermsAggregation().Terms(NewMultiTerm("genre"), NewMultiTerm("product").Missing("n/a")).Size(10)
110+
agg := NewMultiTermsAggregation().MultiTerms(
111+
MultiTerm{Field: "genre"},
112+
MultiTerm{Field: "product", Missing: "n/a"},
113+
).Size(10)
91114
src, err := agg.Source()
92115
if err != nil {
93116
t.Fatal(err)

search_aggs_test.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
)
1515

1616
// TestAggs is an integration test for most aggregation types.
17-
func TestAggs(t *testing.T) {
17+
func TestAggs123(t *testing.T) {
1818
client := setupTestClientAndCreateIndex(t) //, SetTraceLog(log.New(os.Stdout, "", log.LstdFlags)))
1919

2020
tweet1 := tweet{
@@ -79,6 +79,7 @@ func TestAggs(t *testing.T) {
7979
globalAgg := NewGlobalAggregation()
8080
usersAgg := NewTermsAggregation().Field("user").Size(10).OrderByCountDesc()
8181
retweetsAgg := NewTermsAggregation().Field("retweets").Size(10)
82+
multiTermsAgg := NewMultiTermsAggregation().Terms("user").MultiTerms(MultiTerm{Field: "tags", Missing: "unclassified"}).Size(10)
8283
avgRetweetsAgg := NewAvgAggregation().Field("retweets")
8384
avgRetweetsWithMetaAgg := NewAvgAggregation().Field("retweetsMeta").Meta(map[string]interface{}{"meta": true})
8485
weightedAvgRetweetsAgg := NewWeightedAvgAggregation().
@@ -127,6 +128,7 @@ func TestAggs(t *testing.T) {
127128
builder = builder.Aggregation("global", globalAgg)
128129
builder = builder.Aggregation("users", usersAgg)
129130
builder = builder.Aggregation("retweets", retweetsAgg)
131+
builder = builder.Aggregation("multiterms", multiTermsAgg)
130132
builder = builder.Aggregation("avgRetweets", avgRetweetsAgg)
131133
builder = builder.Aggregation("avgRetweetsWithMeta", avgRetweetsWithMetaAgg)
132134
builder = builder.Aggregation("weightedAvgRetweets", weightedAvgRetweetsAgg)
@@ -339,6 +341,38 @@ func TestAggs(t *testing.T) {
339341
}
340342
}
341343

344+
// A multi terms aggregate
345+
{
346+
agg, found := agg.MultiTerms("multiterms")
347+
if !found {
348+
t.Errorf("expected %v; got: %v", true, found)
349+
}
350+
if agg == nil {
351+
t.Fatalf("expected != nil; got: nil")
352+
}
353+
if len(agg.Buckets) != 4 {
354+
t.Fatalf("expected %d; got: %d", 4, len(agg.Buckets))
355+
}
356+
if want, have := 2, len(agg.Buckets[0].Key); want != have {
357+
t.Errorf("expected %v; got: %v", want, have)
358+
}
359+
if want, have := "olivere", agg.Buckets[0].Key[0]; want != have {
360+
t.Errorf("expected %v; got: %v", want, have)
361+
}
362+
if want, have := "golang", agg.Buckets[0].Key[1]; want != have {
363+
t.Errorf("expected %v; got: %v", want, have)
364+
}
365+
if agg.Buckets[0].KeyAsString == nil {
366+
t.Fatal("expected string not nil")
367+
}
368+
if want, have := "olivere|golang", *agg.Buckets[0].KeyAsString; want != have {
369+
t.Errorf("expected %v; got: %v", want, have)
370+
}
371+
if agg.Buckets[0].DocCount != 2 {
372+
t.Errorf("expected %d; got: %d", 2, agg.Buckets[0].DocCount)
373+
}
374+
}
375+
342376
// avgRetweets
343377
{
344378
agg, found := agg.Avg("avgRetweets")

0 commit comments

Comments
 (0)