# Pre-filter points in geo(tile) aggregations

**URL:** <https://discuss.elastic.co/t/pre-filter-points-in-geo-tile-aggregations/377011>\
**Category:** Elasticsearch\
**Created:** [April 10, 2025, 5:00pm UTC](https://discuss.elastic.co/t/pre-filter-points-in-geo-tile-aggregations/377011 "2025-04-10T17:00:46Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![frens](https://sea2.discourse-cdn.com/elastic/user_avatar/discuss.elastic.co/frens/32/114800_2.png) [@frens](https://discuss.elastic.co/u/frens)\
**Post date:** [April 10, 2025, 5:00pm UTC](https://discuss.elastic.co/t/pre-filter-points-in-geo-tile-aggregations/377011/1 "2025-04-10T17:00:46Z")

</div>

Elasticsearch [supports bounds](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geotilegrid-aggregation.html#geotilegrid-addtl-bounding-box-filtering) for geo-tile aggregations. Is there any way to filter the points _before_ the transformation to tiles?

We're considering scripted fields, plugins, and other things. But it would be ideal if we could do this with native ES features.

Thanks!

---

<div class="post-metadata">

**Author:** ![Ignacio\_Vera](https://sea2.discourse-cdn.com/elastic/user_avatar/discuss.elastic.co/ignacio_vera/32/36674_2.png) [@Ignacio\_Vera](https://discuss.elastic.co/u/Ignacio_Vera)\
**Post date:** [April 11, 2025, 5:30am UTC](https://discuss.elastic.co/t/pre-filter-points-in-geo-tile-aggregations/377011/2 "2025-04-11T05:30:14Z")

</div>

I am not sure if I fully understand your question but in order to filter points you might want to use any of the provided geo queries: [Geo queries | Elasticsearch Guide [8.17] | Elastic](https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html)

Not the the bounds you are referring are not filtering points but the generated tiles. Those bounds are not applied to the points but to the tiles, e.g it filter out any tile that is disjoint with the bounds.

---

<div class="post-metadata">

**Author:** ![frens](https://sea2.discourse-cdn.com/elastic/user_avatar/discuss.elastic.co/frens/32/114800_2.png) [@frens](https://discuss.elastic.co/u/frens)\
**Post date:** [April 11, 2025, 7:48am UTC](https://discuss.elastic.co/t/pre-filter-points-in-geo-tile-aggregations/377011/3 "2025-04-11T07:48:23Z")

</div>

Hi Ignacio,

Sorry for not being more clear earlier. That's exactly my point though. We're already using a [geoshape query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-shape-query.html) to filter _documents_. But we're working with data where documents have multiple locations. So a document might match the geoshape query, but also contain points outside of that query. These points are still fed through the agregation.

We can filter the resulting tiles through the bounds of the geo-tile aggregation. But I was wondering whether the possibility exists to filter the points before transformation to tiles.

I.e., effectively, in GeoTileCellIdSource I would like to make the following change:

```java
@Override
protected NumericDocValues boundedCellSingleValue(GeoPointValues values, GeoBoundingBox boundingBox) {
    final GeoTileBoundedPredicate predicate = new GeoTileBoundedPredicate(precision(), boundingBox);
    final int tiles = 1 << precision();
    return new CellSingleValue(values, precision()) {
        @Override
        protected boolean advance(org.elasticsearch.common.geo.GeoPoint target) {

            // ---- added filtering -----------------------------------------
            if (boundingBox.pointInBounds(target.getLon(), target.getLat()) == false) {
                return false;
            }
            // ---- end of added filtering ----------------------------------

            final int x = GeoTileUtils.getXTile(target.getLon(), tiles);
            final int y = GeoTileUtils.getYTile(target.getLat(), tiles);
            if (predicate.validTile(x, y, precision)) {
                value = GeoTileUtils.longEncodeTiles(precision, x, y);
                return true;
            }
            return false;
        }
    };
}

```

(and something similar for `boundedCellMultiValues`)

I understand this gives some potentially odd results if the bounds of the query filter don't match the bounds for the aggregation. Also, when the bounds don't align with the geo-tile grid you'll get some 'interesting' results at the edges.

If there is no other option, we'll probably filter the points with a runtime mapping with a painless script.

---

<div class="post-metadata">

**Author:** ![frens](https://sea2.discourse-cdn.com/elastic/user_avatar/discuss.elastic.co/frens/32/114800_2.png) [@frens](https://discuss.elastic.co/u/frens)\
**Post date:** [April 11, 2025, 7:56am UTC](https://discuss.elastic.co/t/pre-filter-points-in-geo-tile-aggregations/377011/4 "2025-04-11T07:56:51Z")

</div>

I just noticed that there seems to be a difference between geo-tile aggregations and geohash/-hex aggregations. The latter two seem to also filter points before aggregation in contrast to geo-tile. E.g., `GeoHashCellIdSource#boundedCellSingleValue` contains `pointInBounds(target.getLon(), target.getLat())`:

```java
final String hash = Geohash.stringEncode(target.getLon(), target.getLat(), precision);
if (pointInBounds(target.getLon(), target.getLat()) || predicate.validHash(hash)) {
    value = Geohash.longEncode(hash);
    return true;
}
return false;

```

What I essentially would like to do is be able to change the `||` to `&&`.

---

<div class="post-metadata">

**Author:** ![Ignacio\_Vera](https://sea2.discourse-cdn.com/elastic/user_avatar/discuss.elastic.co/ignacio_vera/32/36674_2.png) [@Ignacio\_Vera](https://discuss.elastic.co/u/Ignacio_Vera)\
**Post date:** [April 11, 2025, 9:46am UTC](https://discuss.elastic.co/t/pre-filter-points-in-geo-tile-aggregations/377011/5 "2025-04-11T09:46:01Z")

</div>

I understand. Unfortunately the changes you are proposing will break the implemented functionality in the [vector tile search API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-vector-tile-api.html). The current behaviour is intentional and what you are proposing is a new functionality that currently does not exist.

The way it currently works is that we search for all geometries intersecting the query geometry, then we tile the full geometries.

What you want is to search for all geometries intersecting the query , then tile the intersection of those geometries with the query.

At the moment your only option is to filter the points (build the intersection) with a runtime mapping.

---

<div class="post-metadata">

**Author:** ![frens](https://sea2.discourse-cdn.com/elastic/user_avatar/discuss.elastic.co/frens/32/114800_2.png) [@frens](https://discuss.elastic.co/u/frens)\
**Post date:** [April 11, 2025, 10:28am UTC](https://discuss.elastic.co/t/pre-filter-points-in-geo-tile-aggregations/377011/6 "2025-04-11T10:28:41Z")

</div>

I understand this would break existing queries, which obviously is a no-go.

We'll give runtime fields a try then. Are there any performance considerations to bear in mind?

---

<div class="post-metadata">

**Author:** ![Ignacio\_Vera](https://sea2.discourse-cdn.com/elastic/user_avatar/discuss.elastic.co/ignacio_vera/32/36674_2.png) [@Ignacio\_Vera](https://discuss.elastic.co/u/Ignacio_Vera)\
**Post date:** [April 11, 2025, 10:46am UTC](https://discuss.elastic.co/t/pre-filter-points-in-geo-tile-aggregations/377011/7 "2025-04-11T10:46:34Z")

</div>

Just make sure you are reading the points using doc values, e.g via doc["field"] construct.

---

<div class="post-metadata">

**Author:** ![frens](https://sea2.discourse-cdn.com/elastic/user_avatar/discuss.elastic.co/frens/32/114800_2.png) [@frens](https://discuss.elastic.co/u/frens)\
**Post date:** [April 11, 2025, 12:18pm UTC](https://discuss.elastic.co/t/pre-filter-points-in-geo-tile-aggregations/377011/8 "2025-04-11T12:18:37Z")

</div>

Thanks Ignacio!

---

<div class="post-metadata">

**Author:** ![frens](https://sea2.discourse-cdn.com/elastic/user_avatar/discuss.elastic.co/frens/32/114800_2.png) [@frens](https://discuss.elastic.co/u/frens)\
**Post date:** [April 14, 2025, 2:41pm UTC](https://discuss.elastic.co/t/pre-filter-points-in-geo-tile-aggregations/377011/9 "2025-04-14T14:41:02Z")

</div>

Looking a bit deeper into runtime fields, we're hitting the `AbstractFieldScript.MAX_VALUES` limit. In our solution, the use of such fields would be ideal, but we have a lot of documents with many more than 100 values for a field.

I understand that we're approaching the limits of ES here. I would be grateful for suggestions.

---

<div class="post-metadata">

**Author:** ![Ignacio\_Vera](https://sea2.discourse-cdn.com/elastic/user_avatar/discuss.elastic.co/ignacio_vera/32/36674_2.png) [@Ignacio\_Vera](https://discuss.elastic.co/u/Ignacio_Vera)\
**Post date:** [April 14, 2025, 3:29pm UTC](https://discuss.elastic.co/t/pre-filter-points-in-geo-tile-aggregations/377011/10 "2025-04-14T15:29:05Z")

</div>

I can only think in one solution but requires the cluster to be in a license higher than basic:

The idea is to create a geo\_shape runtime field instead of a geo\_point runtime field. You can then apply the geotile aggregation on the geo\_shape field, although this requires a license.

Something like:

```auto
GET points/_search
{
  "fields": [
    "multipoint"
  ],
  "runtime_mappings": {
    "multipoint": {
      "type": "geo_shape",
      "script": """
        StringBuilder sb = new StringBuilder("multipoint(");
        def v = doc["location"];
        for (int i = 0; i < v.length; i++) {
          sb.append(v[i].lon).append(" ").append(v[i].lat);
          if (i != v.length - 1) sb.append(",");
        }
        sb.append(")");
        emit(sb.toString());
        """
    }
  }
}

```
