Skip to content

Commit 39560bf

Browse files
Zyqsempaiolivere
authored andcommitted
Add Adjacency Matrix aggregation
This commit adds the Adjacency Matrix aggregation to Elastic v6 (https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-aggregations-bucket-adjacency-matrix-aggregation.html). See #725
1 parent aa0e2bd commit 39560bf

5 files changed

+298
-1
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ There are also [some recipes](https://github.com/olivere/elastic/tree/release-br
182182
- [x] Top Hits
183183
- [x] Value Count
184184
- Bucket Aggregations
185-
- [ ] Adjacency Matrix
185+
- [x] Adjacency Matrix
186186
- [x] Children
187187
- [x] Date Histogram
188188
- [x] Date Range

search_aggs.go

+42
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,21 @@ func (a Aggregations) Filters(name string) (*AggregationBucketFilters, bool) {
248248
return nil, false
249249
}
250250

251+
// AdjacencyMatrix returning a form of adjacency matrix.
252+
// See: https://www.elastic.co/guide/en/elasticsearch/reference/6.0/search-aggregations-bucket-adjacency-matrix-aggregation.html
253+
func (a Aggregations) AdjacencyMatrix(name string) (*AggregationBucketAdjacencyMatrix, bool) {
254+
if raw, found := a[name]; found {
255+
agg := new(AggregationBucketAdjacencyMatrix)
256+
if raw == nil {
257+
return agg, true
258+
}
259+
if err := json.Unmarshal(*raw, agg); err == nil {
260+
return agg, true
261+
}
262+
}
263+
return nil, false
264+
}
265+
251266
// Missing returns missing results.
252267
// See: https://www.elastic.co/guide/en/elasticsearch/reference/6.0/search-aggregations-bucket-missing-aggregation.html
253268
func (a Aggregations) Missing(name string) (*AggregationSingleBucket, bool) {
@@ -1215,6 +1230,33 @@ func (a *AggregationBucketFilters) UnmarshalJSON(data []byte) error {
12151230
return nil
12161231
}
12171232

1233+
// -- Bucket AdjacencyMatrix --
1234+
1235+
// AggregationBucketAdjacencyMatrix is a multi-bucket aggregation that is returned
1236+
// with a AdjacencyMatrix aggregation.
1237+
type AggregationBucketAdjacencyMatrix struct {
1238+
Aggregations
1239+
1240+
Buckets []*AggregationBucketKeyItem //`json:"buckets"`
1241+
Meta map[string]interface{} // `json:"meta,omitempty"`
1242+
}
1243+
1244+
// UnmarshalJSON decodes JSON data and initializes an AggregationBucketAdjacencyMatrix structure.
1245+
func (a *AggregationBucketAdjacencyMatrix) UnmarshalJSON(data []byte) error {
1246+
var aggs map[string]*json.RawMessage
1247+
if err := json.Unmarshal(data, &aggs); err != nil {
1248+
return err
1249+
}
1250+
if v, ok := aggs["buckets"]; ok && v != nil {
1251+
json.Unmarshal(*v, &a.Buckets)
1252+
}
1253+
if v, ok := aggs["meta"]; ok && v != nil {
1254+
json.Unmarshal(*v, &a.Meta)
1255+
}
1256+
a.Aggregations = aggs
1257+
return nil
1258+
}
1259+
12181260
// -- Bucket histogram items --
12191261

12201262
// AggregationBucketHistogramItems is a bucket aggregation that is returned
+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2012-present Oliver Eilhard. All rights reserved.
2+
// Use of this source code is governed by a MIT-license.
3+
// See http://olivere.mit-license.org/license.txt for details.
4+
5+
package elastic
6+
7+
// AdjacencyMatrixAggregation returning a form of adjacency matrix.
8+
// The request provides a collection of named filter expressions,
9+
// similar to the filters aggregation request. Each bucket in the
10+
// response represents a non-empty cell in the matrix of intersecting filters.
11+
//
12+
// For details, see
13+
// https://www.elastic.co/guide/en/elasticsearch/reference/6.0/search-aggregations-bucket-adjacency-matrix-aggregation.html
14+
type AdjacencyMatrixAggregation struct {
15+
filters map[string]Query
16+
subAggregations map[string]Aggregation
17+
meta map[string]interface{}
18+
}
19+
20+
// NewAdjacencyMatrixAggregation initializes a new AdjacencyMatrixAggregation.
21+
func NewAdjacencyMatrixAggregation() *AdjacencyMatrixAggregation {
22+
return &AdjacencyMatrixAggregation{
23+
filters: make(map[string]Query),
24+
subAggregations: make(map[string]Aggregation),
25+
}
26+
}
27+
28+
// Filters adds the filter
29+
func (a *AdjacencyMatrixAggregation) Filters(name string, filter Query) *AdjacencyMatrixAggregation {
30+
a.filters[name] = filter
31+
return a
32+
}
33+
34+
// SubAggregation adds a sub-aggregation to this aggregation.
35+
func (a *AdjacencyMatrixAggregation) SubAggregation(name string, subAggregation Aggregation) *AdjacencyMatrixAggregation {
36+
a.subAggregations[name] = subAggregation
37+
return a
38+
}
39+
40+
// Meta sets the meta data to be included in the aggregation response.
41+
func (a *AdjacencyMatrixAggregation) Meta(metaData map[string]interface{}) *AdjacencyMatrixAggregation {
42+
a.meta = metaData
43+
return a
44+
}
45+
46+
// Source returns the a JSON-serializable interface.
47+
func (a *AdjacencyMatrixAggregation) Source() (interface{}, error) {
48+
// Example:
49+
// {
50+
// "aggs" : {
51+
// "interactions" : {
52+
// "adjacency_matrix" : {
53+
// "filters" : {
54+
// "grpA" : { "terms" : { "accounts" : ["hillary", "sidney"] }},
55+
// "grpB" : { "terms" : { "accounts" : ["donald", "mitt"] }},
56+
// "grpC" : { "terms" : { "accounts" : ["vladimir", "nigel"] }}
57+
// }
58+
// }
59+
// }
60+
// }
61+
// This method returns only the (outer) { "adjacency_matrix" : {} } part.
62+
63+
source := make(map[string]interface{})
64+
adjacencyMatrix := make(map[string]interface{})
65+
source["adjacency_matrix"] = adjacencyMatrix
66+
67+
dict := make(map[string]interface{})
68+
for key, filter := range a.filters {
69+
src, err := filter.Source()
70+
if err != nil {
71+
return nil, err
72+
}
73+
dict[key] = src
74+
}
75+
adjacencyMatrix["filters"] = dict
76+
77+
// AggregationBuilder (SubAggregations)
78+
if len(a.subAggregations) > 0 {
79+
aggsMap := make(map[string]interface{})
80+
source["aggregations"] = aggsMap
81+
for name, aggregate := range a.subAggregations {
82+
src, err := aggregate.Source()
83+
if err != nil {
84+
return nil, err
85+
}
86+
aggsMap[name] = src
87+
}
88+
}
89+
90+
// Add Meta data if available
91+
if len(a.meta) > 0 {
92+
source["meta"] = a.meta
93+
}
94+
95+
return source, nil
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2012-present Oliver Eilhard. All rights reserved.
2+
// Use of this source code is governed by a MIT-license.
3+
// See http://olivere.mit-license.org/license.txt for details.
4+
5+
package elastic
6+
7+
import (
8+
"encoding/json"
9+
"testing"
10+
)
11+
12+
func TestAdjacencyMatrixAggregationFilters(t *testing.T) {
13+
f1 := NewTermQuery("accounts", "sydney")
14+
f2 := NewTermQuery("accounts", "mitt")
15+
f3 := NewTermQuery("accounts", "nigel")
16+
agg := NewAdjacencyMatrixAggregation().Filters("grpA", f1).Filters("grpB", f2).Filters("grpC", f3)
17+
src, err := agg.Source()
18+
if err != nil {
19+
t.Fatal(err)
20+
}
21+
data, err := json.Marshal(src)
22+
if err != nil {
23+
t.Fatalf("marshaling to JSON failed: %v", err)
24+
}
25+
got := string(data)
26+
expected := `{"adjacency_matrix":{"filters":{"grpA":{"term":{"accounts":"sydney"}},"grpB":{"term":{"accounts":"mitt"}},"grpC":{"term":{"accounts":"nigel"}}}}}`
27+
if got != expected {
28+
t.Errorf("expected\n%s\n,got:\n%s", expected, got)
29+
}
30+
}
31+
32+
func TestAdjacencyMatrixAggregationWithSubAggregation(t *testing.T) {
33+
avgPriceAgg := NewAvgAggregation().Field("price")
34+
f1 := NewTermQuery("accounts", "sydney")
35+
f2 := NewTermQuery("accounts", "mitt")
36+
f3 := NewTermQuery("accounts", "nigel")
37+
agg := NewAdjacencyMatrixAggregation().SubAggregation("avg_price", avgPriceAgg).Filters("grpA", f1).Filters("grpB", f2).Filters("grpC", f3)
38+
src, err := agg.Source()
39+
if err != nil {
40+
t.Fatal(err)
41+
}
42+
data, err := json.Marshal(src)
43+
if err != nil {
44+
t.Fatalf("marshaling to JSON failed: %v", err)
45+
}
46+
got := string(data)
47+
expected := `{"adjacency_matrix":{"filters":{"grpA":{"term":{"accounts":"sydney"}},"grpB":{"term":{"accounts":"mitt"}},"grpC":{"term":{"accounts":"nigel"}}}},"aggregations":{"avg_price":{"avg":{"field":"price"}}}}`
48+
if got != expected {
49+
t.Errorf("expected\n%s\n,got:\n%s", expected, got)
50+
}
51+
}
52+
53+
func TestAdjacencyMatrixAggregationWithMetaData(t *testing.T) {
54+
f1 := NewTermQuery("accounts", "sydney")
55+
f2 := NewTermQuery("accounts", "mitt")
56+
f3 := NewTermQuery("accounts", "nigel")
57+
agg := NewAdjacencyMatrixAggregation().Filters("grpA", f1).Filters("grpB", f2).Filters("grpC", f3).Meta(map[string]interface{}{"name": "Oliver"})
58+
src, err := agg.Source()
59+
if err != nil {
60+
t.Fatal(err)
61+
}
62+
data, err := json.Marshal(src)
63+
if err != nil {
64+
t.Fatalf("marshaling to JSON failed: %v", err)
65+
}
66+
got := string(data)
67+
expected := `{"adjacency_matrix":{"filters":{"grpA":{"term":{"accounts":"sydney"}},"grpB":{"term":{"accounts":"mitt"}},"grpC":{"term":{"accounts":"nigel"}}}},"meta":{"name":"Oliver"}}`
68+
if got != expected {
69+
t.Errorf("expected\n%s\n,got:\n%s", expected, got)
70+
}
71+
}

search_aggs_test.go

+88
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ func TestAggs(t *testing.T) {
148148
FilterWithName("olivere", NewTermQuery("user", "olivere")).
149149
FilterWithName("sandrae", NewTermQuery("user", "sandrae"))
150150
builder = builder.Aggregation("countByUser2", countByUserAgg2)
151+
// AdjacencyMatrix
152+
adjacencyMatrixAgg := NewAdjacencyMatrixAggregation().
153+
Filters("groupA", NewTermQuery("user", "olivere")).
154+
Filters("groupB", NewTermQuery("user", "sandrae"))
155+
builder = builder.Aggregation("interactions", adjacencyMatrixAgg)
151156
// AvgBucket
152157
dateHisto := NewDateHistogramAggregation().Field("created").Interval("year")
153158
dateHisto = dateHisto.SubAggregation("sumOfRetweets", NewSumAggregation().Field("retweets"))
@@ -978,6 +983,24 @@ func TestAggs(t *testing.T) {
978983
t.Errorf("expected %d; got: %d", 1, b.DocCount)
979984
}
980985

986+
// AdjacencyMatrix agg "adjacencyMatrixAgg" (named)
987+
adjacencyMatrixAggRes, found := agg.AdjacencyMatrix("interactions")
988+
if !found {
989+
t.Errorf("expected %v; got: %v", true, found)
990+
}
991+
if adjacencyMatrixAggRes == nil {
992+
t.Fatalf("expected != nil; got: nil")
993+
}
994+
if len(adjacencyMatrixAggRes.Buckets) != 2 {
995+
t.Fatalf("expected %d; got: %d", 2, len(adjacencyMatrixAggRes.Buckets))
996+
}
997+
if adjacencyMatrixAggRes.Buckets[0].DocCount != 2 {
998+
t.Errorf("expected %d; got: %d", 2, adjacencyMatrixAggRes.Buckets[0].DocCount)
999+
}
1000+
if adjacencyMatrixAggRes.Buckets[1].DocCount != 1 {
1001+
t.Errorf("expected %d; got: %d", 1, adjacencyMatrixAggRes.Buckets[1].DocCount)
1002+
}
1003+
9811004
compositeAggRes, found := agg.Composite("composite")
9821005
if !found {
9831006
t.Errorf("expected %v; got: %v", true, found)
@@ -1912,6 +1935,71 @@ func TestAggsBucketFiltersWithNamedBuckets(t *testing.T) {
19121935
}
19131936
}
19141937

1938+
func TestAggsBucketAdjacencyMatrix(t *testing.T) {
1939+
s := `{
1940+
"interactions": {
1941+
"buckets": [
1942+
{
1943+
"key": "grpA",
1944+
"doc_count": 2,
1945+
"monthly": {
1946+
"buckets": []
1947+
}
1948+
},
1949+
{
1950+
"key": "grpA&grpB",
1951+
"doc_count": 1,
1952+
"monthly": {
1953+
"buckets": []
1954+
}
1955+
}
1956+
]
1957+
}
1958+
}`
1959+
1960+
aggs := new(Aggregations)
1961+
err := json.Unmarshal([]byte(s), &aggs)
1962+
if err != nil {
1963+
t.Fatalf("expected no error decoding; got: %v", err)
1964+
}
1965+
1966+
agg, found := aggs.AdjacencyMatrix("interactions")
1967+
if !found {
1968+
t.Fatalf("expected aggregation to be found; got: %v", found)
1969+
}
1970+
if agg == nil {
1971+
t.Fatalf("expected aggregation != nil; got: %v", agg)
1972+
}
1973+
if agg.Buckets == nil {
1974+
t.Fatalf("expected aggregation buckets != %v; got: %v", nil, agg.Buckets)
1975+
}
1976+
if len(agg.Buckets) != 2 {
1977+
t.Fatalf("expected %d buckets; got: %d", 2, len(agg.Buckets))
1978+
}
1979+
1980+
if agg.Buckets[0].DocCount != 2 {
1981+
t.Fatalf("expected DocCount = %d; got: %d", 2, agg.Buckets[0].DocCount)
1982+
}
1983+
subAgg, found := agg.Buckets[0].Histogram("monthly")
1984+
if !found {
1985+
t.Fatalf("expected sub aggregation to be found; got: %v", found)
1986+
}
1987+
if subAgg == nil {
1988+
t.Fatalf("expected sub aggregation != %v; got: %v", nil, subAgg)
1989+
}
1990+
1991+
if agg.Buckets[1].DocCount != 1 {
1992+
t.Fatalf("expected DocCount = %d; got: %d", 1, agg.Buckets[1].DocCount)
1993+
}
1994+
subAgg, found = agg.Buckets[1].Histogram("monthly")
1995+
if !found {
1996+
t.Fatalf("expected sub aggregation to be found; got: %v", found)
1997+
}
1998+
if subAgg == nil {
1999+
t.Fatalf("expected sub aggregation != %v; got: %v", nil, subAgg)
2000+
}
2001+
}
2002+
19152003
func TestAggsBucketMissing(t *testing.T) {
19162004
s := `{
19172005
"products_without_a_price" : {

0 commit comments

Comments
 (0)