For AI agents: this page is also available as Markdown at /docs/geo-aggregations.md. Every docs page is: add .md to its URL. llms.txt lists them all.

Geo aggregations

Geo aggregations answer "how many, and where": how many cafes are within 1, 5 and 10 km, how matches spread across a map, or where to center and zoom a map so that it shows them all. They run over every match, in the same request as the hits, like any other facet or metric. The examples use the places collection from the Geo search quickstart.

Aggregation /v1 Elasticsearch
Grid cells facet type: geohash_grid, geotile_grid, geohex_grid the aggregations of the same names
Distance rings facet type: geo_distance geo_distance
Bounds and centroid metrics geo_bounds, geo_centroid geo_bounds, geo_centroid
Query buckets facet type: query with geo queries filters with geo queries

The geo facets and metrics work with any query language. They also work on geo_shape fields, except geohex_grid and geo_distance.

Grid aggregations

A grid aggregation counts the matches in each cell of a grid, the building block of heat maps and clustered map markers. Each bucket can run metrics and nested facets:

{
  "language": "lucene",
  "query": "*:*",
  "limit": 0,
  "facets": {
    "cells": {
      "type": "geohash_grid", "field": "location", "precision": 5,
      "metrics": {"avg_rating": "avg(rating)"}
    }
  }
}
"facets": {
  "cells": {
    "type": "geohash_grid",
    "field": "location",
    "buckets": [
      {"value": "dr5ru", "count": 2, "metrics": {"avg_rating": 4.35}},
      {"value": "dr5rg", "count": 1, "metrics": {"avg_rating": 4.5}},
      {"value": "dr5rs", "count": 1, "metrics": {"avg_rating": 4.4}},
      {"value": "dr5rv", "count": 1, "metrics": {"avg_rating": 4.7}}
    ]
  }
}
{
  "size": 0,
  "aggs": {
    "cells": {
      "geohash_grid": {"field": "location", "precision": 5},
      "aggs": {"avg_rating": {"avg": {"field": "rating"}}}
    }
  }
}
"aggregations": {
  "cells": {
    "buckets": [
      {"key": "dr5ru", "doc_count": 2, "avg_rating": {"value": 4.35}},
      {"key": "dr5rg", "doc_count": 1, "avg_rating": {"value": 4.5}},
      {"key": "dr5rs", "doc_count": 1, "avg_rating": {"value": 4.4}},
      {"key": "dr5rv", "doc_count": 1, "avg_rating": {"value": 4.7}}
    ]
  }
}

Harbor Coffee and Midtown Market share the Midtown cell dr5ru; the other places each have a cell of their own.

Grid precision Default Bucket key
geohash_grid Geohash length, 1 to 12 5 The geohash, such as "dr5ru"
geotile_grid Map zoom level, 0 to 29 7 The tile, "zoom/x/y"
geohex_grid H3 resolution, 0 to 15 6 The H3 index in hex
Parameter Meaning
field The geo_point (or, except for geohex_grid, geo_shape) field.
precision The grid level, as above. A distance such as "1km" is not supported.
bounds Optional. Keep only the cells that meet a box: top_left and bottom_right, top_right and bottom_left, top, left, bottom and right, or {"wkt": "BBOX (...)"}. A left edge east of the right one crosses the date line. Each kept cell still counts all of its points.
size How many cells to return, the largest counts first, then by key. Default 10000, or your plan's maximum facet size if lower; more answers 422 limit_exceeded. Elasticsearch's shard_size is accepted.

Distance rings

A geo_distance aggregation counts the matches in distance bands from an origin, like Elasticsearch's geo_distance aggregation:

{
  "language": "lucene",
  "query": "*:*",
  "limit": 0,
  "facets": {
    "rings": {
      "type": "geo_distance", "field": "location", "origin": "40.7580,-73.9855", "unit": "km",
      "ranges": [{"to": 1}, {"from": 1, "to": 5}, {"from": 5}],
      "facets": {"by_category": {"type": "terms", "field": "category"}}
    }
  }
}

The buckets are *-1.0 (count 2: Harbor Coffee and Midtown Market), 1.0-5.0 (count 1: Hudson Espresso) and 5.0-* (count 2: Astoria Bakery and Bridge Roasters). Each /v1 bucket has the key as value, its count, its from and to, and its nested facets.

{
  "size": 0,
  "aggs": {
    "rings": {
      "geo_distance": {
        "field": "location", "origin": "40.7580,-73.9855", "unit": "km",
        "ranges": [{"to": 1}, {"from": 1, "to": 5}, {"from": 5}]
      },
      "aggs": {"by_category": {"terms": {"field": "category"}}}
    }
  }
}

Buckets come back as {"key": "*-1.0", "to": 1.0, "doc_count": 2, ...}, in request order.

Parameter Meaning
field The geo_point field.
origin The point, in any query point form.
unit m (default), km, mi, yd, ft, in, cm, mm or nmi, for from and to.
ranges A list of {"from", "to", "key"}. from is included and to is not; either can be left open. At most your plan's maximum facet size.
keyed true returns the buckets as an object by key instead of a list.

Bounds and centroid

geo_bounds returns the box around the matching points, and geo_centroid their average location and how many there are. Together they fit a map to the results:

{
  "language": "lucene",
  "query": "*:*",
  "limit": 0,
  "metrics": {
    "box": {"func": "geo_bounds", "field": "location"},
    "middle": "geo_centroid(location)"
  }
}
"metrics": {
  "box": {"bounds": {"top_left": {"lat": 40.7644, "lon": -74.0324},
                     "bottom_right": {"lat": 40.7033, "lon": -73.9235}}},
  "middle": {"location": {"lat": 40.7447, "lon": -73.9816}, "count": 5}
}
{
  "size": 0,
  "aggs": {
    "box": {"geo_bounds": {"field": "location"}},
    "middle": {"geo_centroid": {"field": "location"}}
  }
}

The results have the same shapes as the /v1 metrics, under aggregations.

(Coordinates rounded here.)

Query buckets

A query facet with one geo query per bucket counts the matches in each circle or box you name. It is available today, and suits a handful of fixed areas or bands:

{
  "language": "es",
  "query": {"match": {"description": "coffee"}},
  "limit": 0,
  "facets": {
    "distance": {
      "type": "query",
      "buckets": {
        "within_1km":  {"geo_distance": {"distance": "1km",  "location": "40.7580,-73.9855"}},
        "within_5km":  {"geo_distance": {"distance": "5km",  "location": "40.7580,-73.9855"}},
        "within_10km": {"geo_distance": {"distance": "10km", "location": "40.7580,-73.9855"}}
      },
      "metrics": {"avg_rating": "avg(rating)"}
    }
  }
}

With "language": "es", the bucket queries are Query DSL objects. Each bucket in the response has the bucket name as its value, a count, and its metrics.

A filters aggregation with a geo filter per bucket:

{
  "query": {"match": {"description": "coffee"}},
  "size": 0,
  "aggs": {
    "distance": {
      "filters": {"filters": {
        "within_1km":  {"geo_distance": {"distance": "1km",  "location": "40.7580,-73.9855"}},
        "within_5km":  {"geo_distance": {"distance": "5km",  "location": "40.7580,-73.9855"}},
        "within_10km": {"geo_distance": {"distance": "10km", "location": "40.7580,-73.9855"}}
      }},
      "aggs": {"avg_rating": {"avg": {"field": "rating"}}}
    }
  }
}

These circles overlap: a place 0.5 km away counts in all three. For rings that do not overlap, subtract the inner circle with must_not, or use distance rings once they are live:

"from_1_to_5km": {"bool": {
  "filter":   [{"geo_distance": {"distance": "5km", "location": "40.7580,-73.9855"}}],
  "must_not": [{"geo_distance": {"distance": "1km", "location": "40.7580,-73.9855"}}]
}}

The same pattern counts named areas, with one geo_bounding_box (or, once live, geo_polygon or geo_shape) per bucket.

View this page as Markdown

Cookie settings

Optional tracking stays off until you choose it. Your choice applies to this browser for 180 days. Read the details.

Essential storage keeps you signed in, protects forms, and remembers this choice. It is always available.