Bucket aggregations in Elasticsearch create buckets or sets of documents based on certain criteria. Depending on the aggregation type, you can create filtering buckets, that is, buckets representing different value ranges and intervals for numeric values, dates, IP ranges, and more. 

Although bucket aggregations do not calculate metrics, they can hold metrics sub-aggregations that can calculate metrics for each bucket generated by the bucket aggregation. This makes bucket aggregations very useful for the granular representation and analysis of your Elasticsearch indices. In this article, we'll focus on such bucket aggregations as histogram, range, filters, and terms. Let's get started!

Tutorial

Examples in this tutorial were tested in the following environment:

  • Elasticsearch 6.4.0
  • Kibana 6.4.0

Creating a New Index

To illustrate various buckets aggregations mentioned in the intro, we'll first create a new "sports" index storing a collection of "athlete" documents. The index mapping will contain such fields as athlete's location, name, rating, sport, age, number of scored goals, and field position (e.g., defender). Let's create the mapping:

curl -XPUT "http://localhost:9200/sports/" -H "Content-Type: application/json" -d'
{
   "mappings": {
      "athlete": {
         "properties": {
            "birthdate": {
               "type": "date",
               "format": "dateOptionalTime"
            },
            "location": {
               "type": "geo_point"
            },
            "name": {
               "type": "keyword"
            },
            "rating": {
               "type": "integer"
            },
            "sport": {
               "type": "keyword"
            },
             "age": {
                 "type":"integer"
             },
             "goals": {
                 "type": "integer"
             },
             "role": {
                 "type":"keyword"
             },
             "score_weight": {
                 "type": "float"
             }
         }
      }
   }
}'

Once the index mapping is created, let's use Elasticsearch Bulk API to save some data to our index. This API will allow us saving multiple documents to the index in a single call:

curl -XPOST "http://localhost:9200/sports/_bulk" -H "Content-Type: application/json" -d'
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Michael", "birthdate":"1989-10-1", "sport":"Football", "rating": ["5", "4"],  "location":"46.22,-68.45", "age":"23","goals": "43","score_weight":"3","role":"midfielder"}
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Bob", "birthdate":"1989-11-2", "sport":"Football", "rating": ["3", "4"],  "location":"45.21,-68.35", "age":"33", "goals": "54","score_weight":"2", "role":"forward"}
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Jim", "birthdate":"1988-10-3", "sport":"Football", "rating": ["3", "2"],  "location":"45.16,-63.58", "age":"28", "goals": "73", "score_weight":"2", "role":"forward" }
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Joe", "birthdate":"1992-5-20", "sport":"Basketball", "rating": ["4", "3"],  "location":"45.22,-68.53", "age":"18", "goals": "848", "score_weight":"3", "role":"midfielder"}
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Tim", "birthdate":"1992-2-28", "sport":"Basketball", "rating": ["3", "3"],  "location":"46.22,-68.85", "age":"28","goals": "942", "score_weight":"2","role":"forward"}
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Alfred", "birthdate":"1990-9-9", "sport":"Football", "rating": ["2", "2"],  "location":"45.12,-68.35", "age":"25", "goals": "53", "score_weight":"4", "role":"defender"}
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Jeff", "birthdate":"1990-4-1", "sport":"Hockey", "rating": ["2", "3"], "location":"46.12,-68.55", "age":"26","goals": "93","score_weight":"3","role":"midfielder"}
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Will", "birthdate":"1988-3-1", "sport":"Hockey", "rating": ["4", "4"], "location":"46.25,-84.25", "age":"27", "goals": "124", "score_weight":"2", "role":"forward" }
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Mick", "birthdate":"1989-10-1", "sport":"Football", "rating": ["3", "4"],  "location":"46.22,-68.45", "age":"35","goals": "56","score_weight":"3", "role":"midfielder"}
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Pong", "birthdate":"1989-11-2", "sport":"Basketball", "rating": ["1", "3"],  "location":"45.21,-68.35", "age":"34","goals": "1483","score_weight":"2", "role":"forward"}
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Ray", "birthdate":"1988-10-3", "sport":"Football", "rating": ["2", "2"],  "location":"45.16,-63.58", "age":"31","goals": "84", "score_weight":"3", "role":"midfielder" }
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Ping", "birthdate":"1992-5-20", "sport":"Basketball", "rating": ["4", "3"],  "location":"45.22,-68.53", "age":"27","goals": "1328", "score_weight":"2", "role":"forward"}
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Duke", "birthdate":"1992-2-28", "sport":"Hockey", "rating": ["5", "2"],  "location":"46.22,-68.85", "age":"41","goals": "218", "score_weight":"2", "role":"forward"}
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Hal", "birthdate":"1990-9-9", "sport":"Hockey", "rating": ["4", "2"],  "location":"45.12,-68.35", "age":"18","goals": "148", "score_weight":"3", "role":"midfielder"}
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Charge", "birthdate":"1990-4-1", "sport":"Football", "rating": ["3", "2"], "location":"44.19,-82.55", "age":"19","goals": "34", "score_weight":"4", "role":"defender"}
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Barry", "birthdate":"1988-3-1", "sport":"Football", "rating": ["5", "2"], "location":"36.45,-79.15", "age":"20", "goals": "48", "score_weight":"4", "role":"defender" }
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Bank", "birthdate":"1988-3-1", "sport":"Handball", "rating": ["6", "4"], "location":"46.25,-54.53", "age":"25", "goals": "150", "score_weight":"4", "role":"defender" }
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Bingo", "birthdate":"1988-3-1", "sport":"Handball", "rating": ["10", "7"], "location":"46.25,-68.55", "age":"29", "goals": "143", "score_weight":"3", "role":"midfielder" }
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"James", "birthdate":"1988-3-1", "sport":"Basketball", "rating": ["10", "8"], "location":"41.25,-69.55", "age":"36", "goals": "1284", "score_weight":"2", "role":"forward" }
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Wayne", "birthdate":"1988-3-1", "sport":"Hockey", "rating": ["10", "10"], "location":"46.21,-68.55", "age":"25", "goals": "113", "score_weight":"3", "role":"midfielder" }
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Brady", "birthdate":"1988-3-1", "sport":"Handball", "rating": ["10", "10"], "location":"63.24,-84.55", "age":"29", "goals": "443", "score_weight":"2", "role":"forward" }
{"index":{"_index":"sports","_type":"athlete"}}
{"name":"Lewis", "birthdate":"1988-3-1", "sport":"Football", "rating": ["10", "10"], "location":"56.25,-74.55", "age":"24", "goals": "49", "score_weight":"3", "role":"midfielder" }
'

Filter(s) Aggregations

Buckets aggregations support single-filter and multi-filter aggregations.

A single-filter aggregation constructs a single bucket from all documents that match a query or field value specified in the filter definition. A single-filter aggregation is useful when you want to identify a set of documents that match certain criteria.

For example, we can use a single-filter aggregation to find all athletes with the role "defender" and calculate the average goals for each filtered bucket. The filter configuration looks as follows:

curl -X POST "localhost:9200/sports/athlete/_search?size=0&pretty" -H 'Content-Type: application/json' -d'
{
    "aggs" : {
        "defender_filter" : {
            "filter" : { "term": { "role": "defender" } },
            "aggs" : {
                "avg_goals" : { "avg" : { "field" : "goals" } }
            }
        }
    }
}
'

As you see, the "filter" aggregation contains a "term" field that specifies the field in your documents to search for specific value ("defender" in our case). Elasticsearch will run through all documents and check to see if the "role" field contains the "defender" in it. The documents matching this value will be then added to a single bucket generated by the aggregation.

The query above should produce the following response:

...
"aggregations" : {
    "defender_filter" : {
      "doc_count" : 4,
      "avg_goals" : {
        "value" : 71.25
      }
    }
  }

This output indicates that the average number of goals scored by all defenders in our collection is 71.25.

This was an example of a single-filter aggregation. However, in Elasticsearch, you have an option of specifying multiple filters using the Filters aggregation. This is a multi-value aggregation where each bucket corresponds to a specific filter. We can modify the example above to filter both defenders and forwards:

curl -X GET "localhost:9200/sports/athlete/_search?size=0&pretty" -H 'Content-Type: application/json' -d'
{
  "aggs" : {
    "athletes" : {
      "filters" : {
        "filters" : {
          "defenders" :   { "term" : { "role" : "defender"   }},
          "forwards" : { "term" : { "role" : "forward" }}
        }
      },
      "aggs" : {
            "avg_goals" : { "avg" : { "field" : "goals" } }
      }
    }
  }
}
'

As you see, now we have two filters labeled "defenders" and "forwards." Each of them checks the "role" field for the corresponding value: "defender" or "forward." The query above should produce the following response:

...
"aggregations" : {
    "athletes" : {
      "buckets" : {
        "defenders" : {
          "doc_count" : 4,
          "avg_goals" : {
            "value" : 71.25
          }
        },
        "forwards" : {
          "doc_count" : 9,
          "avg_goals" : {
            "value" : 661.0
          }
        }
      }
    }
  }

Let's visualize these results in Kibana:

Kibana: Filters Aggregation

As you see, the average sub-aggregation on the "goals" field is defined in the Y-Axis. In the X-Axis, we create two filters and specify "defender" and "forward" values for them. Since the average metrics is a sub-aggregation of the filters aggregation, Elasticsearch will apply the created filters on the "goals" field so we don't need to specify the field explicitly.

Terms Aggregation

A terms aggregation searches for unique values in the specified field of your documents and builds buckets for each unique value found. Unlike the filter(s) aggregation, the task of the terms aggregation is not to limit the results to certain value but to find all unique values for a given field in your documents.

Take a look at the example below where we are trying to create a bucket for every unique value found in the "sport" field. In the result of this operation, we'll end up with four unique buckets for each sport in our index: Football, Handball, Hockey, and Basketball. We'll then use the average sub-aggregation to calculate the average goals for each sport:

curl -X POST "localhost:9200/sports/athlete/_search?size=0&pretty" -H 'Content-Type: application/json' -d'
{
    "aggs": {
        "sports":{
            "terms" : { "field" : "sport" },
            "aggs": {
                "avg_scoring":{
                    "avg": {"field":"goals"}
                }
            }
        }
    }
}
'

And the response should look something like this:

...
"aggregations" : {
    "sports" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "Football",
          "doc_count" : 9,
          "avg_scoring" : {
            "value" : 54.888888888888886
          }
        },
        {
          "key" : "Basketball",
          "doc_count" : 5,
          "avg_scoring" : {
            "value" : 1177.0
          }
        },
        {
          "key" : "Hockey",
          "doc_count" : 5,
          "avg_scoring" : {
            "value" : 139.2
          }
        },
        {
          "key" : "Handball",
          "doc_count" : 3,
          "avg_scoring" : {
            "value" : 245.33333333333334
          }
        }
      ]
    }
  }

As you see, the terms aggregation constructed four buckets for each sports type in our index. Each bucket contains the doc_count (number of documents that fall into the bucket) and the average sub-aggregation for each sport.

Let's visualize these results in Kibana:

Kibana: Terms Aggregation

As you see, in the Y-axis we use the average sub-aggregation on the "goals" field and in the X-Axis we define a terms bucket aggregation on the "sport" field.

Histogram Aggregation

The Histogram aggregation allows us to construct buckets based on the specified intervals. The values that fall into each interval will form an interval bucket. For example, let's assume we want to apply the histogram aggregation on the age field using a 5-years-interval. In this case, the histogram aggregation will find the minimum and maximum age in our document set and associate each document with the specified interval. The "age" field of each document will be rounded down to its closest interval bucket. For example, given our interval value of 5 and the bucket size of 6, the age 32 will be rounded down to 30.

The formula for the histogram aggregation looks as follows:

bucket_key = Math.floor((value - offset) / interval) * interval + offset

Please, note that interval must be a positive decimal, while the offset must be a decimal in [0, interval) range.

Let's use the histogram aggregation to generate buckets of goals/points in basketball with the interval of 200.

curl -X POST "localhost:9200/sports/athlete/_search?size=0&pretty" -H 'Content-Type: application/json' -d'
{
    "aggs" : {
        "basketball_filter":{
            "filter":{"term":{"sport":"Basketball"}},
            "aggs": {
                "goals_histogram": {
                    "histogram": {
                        "field": "goals",
                        "interval": "200"
                    }
                }
            }
        }
    }
}
'

The response should look something like this:

"aggregations" : {
    "basketball_filter" : {
      "doc_count" : 5,
      "goals_histogram" : {
        "buckets" : [
          {
            "key" : 800.0,
            "doc_count" : 2
          },
          {
            "key" : 1000.0,
            "doc_count" : 0
          },
          {
            "key" : 1200.0,
            "doc_count" : 2
          },
          {
            "key" : 1400.0,
            "doc_count" : 1
          }
        ]
      }
    }
  }

The response above shows that there are no goals that fall within 0-200, 200-400, 400-600, and 600-800 intervals. Therefore, the first bucket starts from the 800-1000 interval. Thus, the documents with the smallest values will determine the min bucket (the bucket with the smallest key). Correspondingly, the documents with the highest values will determine the max bucket (the bucket with the highest key).

Also, the response shows that there are zero documents that fall within the range of [1000, 1200). This means that no athletes scored between 1000 and 1200 goals. By default, Elasticsearch fills gaps like these with empty buckets. You can change this behavior by requesting buckets with a non-zero minimum count using the min_doc_count setting. For example, if we set the value of min_doc_count to 1, the histogram will only construct buckets for intervals which have no less than 1 documents in them. Let's modify our query adding the min_doc_count set to 1.

curl -X POST "localhost:9200/sports/athlete/_search?size=0&pretty" -H 'Content-Type: application/json' -d'
{
    "aggs" : {
        "basketball_filter":{
            "filter":{"term":{"sport":"Basketball"}},
            "aggs": {
                "goals_histogram": {
                    "histogram": {
                        "field": "goals",
                        "interval": "200",
                        "min_doc_count":1 
                    }
                }
            }
        }
    }
}
'

And now the response should not contain any buckets for 1000-1200 interval:

.....
"aggregations" : {
    "basketball_filter" : {
      "doc_count" : 5,
      "goals_histogram" : {
        "buckets" : [
          {
            "key" : 800.0,
            "doc_count" : 2
          },
          {
            "key" : 1200.0,
            "doc_count" : 2
          },
          {
            "key" : 1400.0,
            "doc_count" : 1
          }
        ]
      }
    }
  }

We can also use the extended_bounds setting to "force" the histogram aggregation to start building its buckets on a specific min value and keep on building buckets up to a max value (even if there are no documents anymore). Using extended_bounds only makes sense with the min_doc_count set to 0. (the empty buckets will never be returned if min_doc_count is greater than 0). Take a look at this query:

curl -X POST "localhost:9200/sports/athlete/_search?size=0&pretty" -H 'Content-Type: application/json' -d'
{
    "aggs" : {
        "basketball_filter":{
            "filter":{"term":{"sport":"Basketball"}},
            "aggs": {
                "goals_histogram": {
                    "histogram": {
                        "field": "goals",
                        "interval": "200",
                        "min_doc_count":0,
                        "extended_bounds" : {
                           "min" : 0,
                           "max" : 1600
                         }
                    }
                }
            }
        }
    }
}
'

Here, we specified 0 as the min and 1600 as the max values for our buckets. Therefore, the response should look something like this:

...
"aggregations" : {
    "basketball_filter" : {
      "doc_count" : 5,
      "goals_histogram" : {
        "buckets" : [
          {
            "key" : 0.0,
            "doc_count" : 0
          },
          {
            "key" : 200.0,
            "doc_count" : 0
          },
          {
            "key" : 400.0,
            "doc_count" : 0
          },
          {
            "key" : 600.0,
            "doc_count" : 0
          },
          {
            "key" : 800.0,
            "doc_count" : 2
          },
          {
            "key" : 1000.0,
            "doc_count" : 0
          },
          {
            "key" : 1200.0,
            "doc_count" : 2
          },
          {
            "key" : 1400.0,
            "doc_count" : 1
          },
          {
            "key" : 1600.0,
            "doc_count" : 0
          }
        ]
      }
    }
  }

As you see, all buckets starting from 0 and ending 1600 were generated even as the first bucket and the last bucket do not have any values at all.

Range Aggregation

This bucket aggregation makes it easy to construct buckets based on the user-defined ranges. Elasticsearch will check each value extracted from the numeric field you specified, compare it with the ranges, and put the value into the corresponding range. Please note that this aggregation includes the from value and excludes the to value for each range.

Let's create a range aggregation for the "age" field in our sports index:

curl -X GET "localhost:9200/sports/athlete/_search?size=0&pretty" -H 'Content-Type: application/json' -d'
{
    "aggs" : {
        "goal_ranges" : {
            "range" : {
                "field" : "age",
                "ranges" : [
                    { "to" : 20.0 },
                    { "from" : 20.0, "to" : 30.0 },
                    { "from" : 30.0 }
                ]
            }
        }
    }
}
'

As you see, we have specified three ranges for the query. This means that Elasticsearch will create three buckets corresponding to each range. The above query should produce the following output:

.......
"aggregations" : {
    "goal_ranges" : {
      "buckets" : [
        {
          "key" : "*-20.0",
          "to" : 20.0,
          "doc_count" : 3
        },
        {
          "key" : "20.0-30.0",
          "from" : 20.0,
          "to" : 30.0,
          "doc_count" : 13
        },
        {
          "key" : "30.0-*",
          "from" : 30.0,
          "doc_count" : 6
        }
      ]
    }
  }

As the output shows, the largest number of athletes in our index are between 20 and 30 years old.

To make the ranges more human-readable, we can customize the key name for each range like this:

curl -X GET "localhost:9200/sports/athlete/_search?size=0&pretty" -H 'Content-Type: application/json' -d'
{
    "aggs" : {
        "goal_ranges" : {
            "range" : {
                "field" : "age",
                "ranges" : [
                    { "key":"start-of-career","to" : 20.0 },
                    { "key":"mid-of-career", "from": 20.0, "to" : 30.0 },
                    { "key":"end-of-cereer","from" : 30.0 }
                ]
            }
        }
    }
}
'

This will produce the following response:

"aggregations" : {
    "goal_ranges" : {
      "buckets" : [
        {
          "key" : "start-of-career",
          "to" : 20.0,
          "doc_count" : 3
        },
        {
          "key" : "mid-of-career",
          "from" : 20.0,
          "to" : 30.0,
          "doc_count" : 13
        },
        {
          "key" : "end-of-cereer",
          "from" : 30.0,
          "doc_count" : 6
        }
      ]
    }
  }

We can add more information to ranges using stats sub-aggregation. This aggregation will provide min, max, avg, and sum values for each range. Let's take a look:

curl -X GET "localhost:9200/sports/athlete/_search?size=0&pretty" -H 'Content-Type: application/json' -d'
{
    "aggs" : {
        "goal_ranges" : {
            "range" : {
                "field" : "age",
                "ranges" : [
                    { "key":"start-of-career","to" : 20.0 },
                    { "key":"mid-of-career", "from": 20.0, "to" : 30.0 },
                    { "key":"end-of-cereer","from" : 30.0 }
                ]
            },
            "aggs": {
                "age_stats": {
                    "stats": {"field":"age"}
                }
            }
        }
    }
}
'

And the response:

"aggregations" : {
    "goal_ranges" : {
      "buckets" : [
        {
          "key" : "start-of-career",
          "to" : 20.0,
          "doc_count" : 3,
          "age_stats" : {
            "count" : 3,
            "min" : 18.0,
            "max" : 19.0,
            "avg" : 18.333333333333332,
            "sum" : 55.0
          }
        },
        {
          "key" : "mid-of-career",
          "from" : 20.0,
          "to" : 30.0,
          "doc_count" : 13,
          "age_stats" : {
            "count" : 13,
            "min" : 20.0,
            "max" : 29.0,
            "avg" : 25.846153846153847,
            "sum" : 336.0
          }
        },
        {
          "key" : "end-of-cereer",
          "from" : 30.0,
          "doc_count" : 6,
          "age_stats" : {
            "count" : 6,
            "min" : 31.0,
            "max" : 41.0,
            "avg" : 35.0,
            "sum" : 210.0
          }
        }
      ]
    }
  }

Visualizing ranges in Kibana is quite simple. We'll use a pie chart for this. As you see in the image below, slice size is defined by the Count aggregation. In the Buckets section, we need to create three ranges for our data. These ranges will be the split slices of our pie chart.

 

Geo Distance Aggregation

With the geo distance aggregation, you can define a point of origin and a set of distance ranges from that point. The aggregation will then evaluate the distance of each geo_point value from the origin point and determine which range bucket the document falls into. A document is considered to belong to a given bucket if the distance between the document's geo_point value and the origin point falls within the distance range of that bucket.

In the example below, our point of origin has the latitude value of 46.22 and the longitude value of -68.85. We used the string format of origin 46.22,-68.85 where the first value defines the latitude and the second one defines the longitude. Alternatively, you can use the object format -- { "lat" : 46.22, "lon" : -68.85 } or array format: [-68.85 , 46.22] which is based on the GeoJson standard and where the first number is the lon and the second one is the lat

Also, we create three ranges in km values. The default distance unit is m (meters), so we need to explicitly set km in the "unit" field. Other supported distance units are mi (miles), in (inches), yd (yards), cm (centimeters), and mm (millimeters).

curl -X POST "localhost:9200/sports/athlete/_search?size=0&pretty" -H 'Content-Type: application/json' -d'
{
    "aggs" : {
        "athlete_location" : {
            "geo_distance" : {
                "field" : "location",
                "origin" : "46.22,-68.85",
                "unit" : "km", 
                "ranges" : [
                    { "to" : 200 },
                    { "from" : 200, "to" : 400 },
                    { "from" : 400 }
                ]
            }
        }
    }
}
'

The response should be the following:

.....
"aggregations" : {
    "athlete_location" : {
      "buckets" : [
        {
          "key" : "*-200.0",
          "from" : 0.0,
          "to" : 200.0,
          "doc_count" : 13
        },
        {
          "key" : "200.0-400.0",
          "from" : 200.0,
          "to" : 400.0,
          "doc_count" : 0
        },
        {
          "key" : "400.0-*",
          "from" : 400.0,
          "doc_count" : 9
        }
      ]
    }
  }

As the results indicate, there are 13 athletes who live not farther than 200 km from the origin point and 9 athletes who live farther than 400 km from the origin point.

IP Range Aggregation

Elasticsearch has built-in support for IP ranges as well. The IP aggregation works similarly to other range aggregations. Let's create an index mapping for IP addresses to illustrate how this aggregation works:

curl -X PUT "localhost:9200/ips" -H 'Content-Type: application/json' -d'
{
  "mappings": {
    "ip": {
      "properties": {
        "ip_addr": {
          "type": "ip"
        }
      }
    }
  }
}
'

Let's put some private network IPs to the index.

curl -XPOST "localhost:9200/ips/_bulk" -H 'Content-Type: application/json' -d'
{"index":{"_index":"ips","_type":"ip"}}
{ "ip_addr": "172.16.0.0" }
{"index":{"_index":"ips","_type":"ip"}}
{ "ip_addr": "172.16.0.1" }
{"index":{"_index":"ips","_type":"ip"}}
{ "ip_addr": "172.16.0.2" }
{"index":{"_index":"ips","_type":"ip"}}
{ "ip_addr": "172.16.0.3" }
{"index":{"_index":"ips","_type":"ip"}}
{ "ip_addr": "172.16.0.4" }
{"index":{"_index":"ips","_type":"ip"}}
{ "ip_addr": "172.16.0.5" }
{"index":{"_index":"ips","_type":"ip"}}
{ "ip_addr": "172.16.0.6" }
{"index":{"_index":"ips","_type":"ip"}}
{ "ip_addr": "172.16.0.7" }
{"index":{"_index":"ips","_type":"ip"}}
{ "ip_addr": "172.16.0.8" }
{"index":{"_index":"ips","_type":"ip"}}
{ "ip_addr": "172.16.0.9" }
'

Now, as we have some data in our index, let's create an IP range aggregation:

curl -X GET "localhost:9200/ips/_search?size=0&pretty" -H 'Content-Type: application/json' -d'
{
    "aggs" : {
        "ip_ranges" : {
            "ip_range" : {
                "field" : "ip_addr",
                "ranges" : [
                    { "to" : "172.16.0.4" },
                    { "from" : "172.16.0.4" }
                ]
            }
        }
    }
}
'

We defined two ranges for our IP addresses. You can define as many as you need as per your needs. The query above should return the following response:

"aggregations" : {
    "ip_ranges" : {
      "buckets" : [
        {
          "to" : "172.16.0.4",
          "doc_count" : 4
        },
        {
          "from" : "172.16.0.4",
          "doc_count" : 6
        }
      ]
    }
  }

Conclusion

That's it! In this article, we discussed buckets aggregations in Elasticsearch. In the next part of the Buckets Aggregation series, we'll continue our overview of the buckets aggregations and focus on composite, children, date histogram, date range, diversified sampler, and other common buckets aggregations in Elasticsearch. Stay tuned to our blog content to learn more!