Search & Retrieval

Geospatial Search

Querying and aggregating geospatial data using geo_point and geo_shape types, distance queries, bounding boxes, geo grid aggregations, PostGIS, and H3 hierarchical indexing.

⏱ 10 min read

What it is

Geospatial search is the capability to query, filter, and aggregate documents based on geographic coordinates or shapes. The two fundamental operations are proximity search ("find all restaurants within 5km of my location") and containment search ("find all orders shipped to addresses within the New York metro boundary polygon"). Elasticsearch supports both through the geo_point type (a single latitude/longitude coordinate) and the geo_shape type (GeoJSON geometries: points, linestrings, polygons, multi-polygons). The query types are geo_distance (circle/radius filter), geo_bounding_box (rectangular filter), geo_polygon (arbitrary polygon filter), and geo_shape (spatial relationship queries: intersects, contains, disjoint).

At the aggregation layer, geo grid aggregations bucket documents into geographic grid cells: geohash_grid (variable-precision string encoding), geotile_grid (map tile coordinates at zoom level), and hex_grid (H3 hexagonal cells). These aggregations are used to build heatmaps and density visualisations on maps without transferring millions of individual coordinates to the client. For complex spatial operations (topology, routing, network analysis), PostGIS — the PostgreSQL spatial extension — provides a full SQL-based geospatial engine with support for hundreds of ST_ functions.

Why it exists

Geographic queries cannot be efficiently answered by standard B-tree indexes because geographic space is two-dimensional: a range query on latitude alone is meaningless without also constraining longitude. Geospatial indexes use space-partitioning data structures (R-trees in PostGIS, geohash Trie in Elasticsearch, BKD tree/Lucene PointValues in Elasticsearch 5+) that partition 2D space into indexed regions, enabling fast range lookups in both dimensions simultaneously. Without specialised geo indexing, a query like "find 1,000 nearest restaurants" would require computing the Haversine distance from the user's location to every restaurant in the database — O(n) rather than O(log n).

The proliferation of mobile devices created enormous demand for location-aware features: ride-hailing, food delivery, real estate, and local search all depend on sub-second geospatial queries over millions of points. Hierarchical spatial indexes like Google's S2 (spherical geometry cells) and Uber's H3 (hexagonal hierarchical index) were developed to serve this need at internet scale, providing cells at configurable resolutions from country-level (level 0) to meter-level (level 15) that can be used as pre-computed index keys in any standard key-value or columnar store.

When to use

  • Proximity search: "find the 10 nearest pharmacies to my current location."
  • Containment search: "find all users within a service coverage area polygon."
  • Heatmap visualisation: density of events, users, or assets on a geographic map.
  • Geofencing and event triggering: detect when a tracked entity enters or exits a defined polygon.
  • Delivery route optimisation: spatial joins between order locations and delivery zones.

When not to use

  • Very small datasets: For fewer than 10,000 points, an in-memory Haversine loop or a simple database computed column is simpler than a geospatial index.
  • Non-geographic similarity: If your coordinate space is not geographic (e.g., 2D game coordinates), use standard 2D range queries rather than Haversine-based geo indexes.
  • Complex routing and graph problems: Shortest-path routing requires a graph database or routing engine (OSRM, Valhalla); geospatial indexes only answer spatial proximity and containment queries.
  • High-precision indoor positioning: GPS-based geo_point fields have metre-level precision outdoors; indoor positioning systems require different infrastructure.

Typical architecture

ELASTICSEARCH GEO QUERIES:

Mapping:
  location: { type: geo_point }

Proximity query (nearby restaurants):
  POST /restaurants/_search {
    "query": {
      "bool": {
        "filter": {
          "geo_distance": {
            "distance": "5km",
            "location": { "lat": 51.5, "lon": -0.1 }
          }
        }
      }
    },
    "sort": [{ "_geo_distance": {
        "location": { "lat": 51.5, "lon": -0.1 },
        "order": "asc", "unit": "km"
    }}]
  }

Heatmap aggregation (geotile_grid):
  "aggs": {
    "map_grid": {
      "geotile_grid": { "field": "location", "precision": 12 }
    }
  }
  → returns tile coordinates + doc count per tile

H3 INDEX PATTERN (pre-indexed cells):
  At write time: compute H3 cell indexes for
  location at resolutions 7, 8, 9, 10
  Store as array: h3_cells: ["87283472fffffff","8728347...]

  At query time: compute H3 cell covering for
  query radius, execute:
  terms: { h3_cells: [] }
  → No Haversine computation at query time
  → Scales to billions of points

POSTGIS PATTERN:
  CREATE TABLE locations (
    id SERIAL, geom GEOMETRY(POINT, 4326)
  );
  CREATE INDEX ON locations USING GIST(geom);

  -- Find within 5km using ST_DWithin
  SELECT * FROM locations
  WHERE ST_DWithin(
    geom::geography,
    ST_MakePoint(-0.1, 51.5)::geography,
    5000  -- metres
  );

Pros and cons

Pros

  • BKD-tree backed geo_point queries in Elasticsearch deliver sub-10ms proximity search over tens of millions of points.
  • Geo grid aggregations enable real-time map density visualisation without transferring raw coordinates to the client.
  • H3 indexing transforms geospatial proximity queries into standard equality/membership lookups, enabling use with any key-value store.
  • PostGIS provides a full suite of spatial operations (topology, projections, routing adjacency) beyond proximity search.
  • Geo_shape queries support complex polygon intersections needed for geofencing and coverage area analysis.

Cons

  • Geo_shape indexes are significantly larger than geo_point indexes due to shape tessellation overhead.
  • PostGIS spatial index performance degrades with very high-cardinality polygon sets; requires careful VACUUM and ANALYZE scheduling.
  • H3 cell approximation introduces boundary errors: proximity based on cell membership is not exactly equal to true radius-based proximity.
  • Aggregation precision vs. performance trade-off: high-resolution geo grid aggregations over large datasets can be slow.
  • Antimeridian and polar edge cases trip up naive implementations; always test with coordinates near ±180° longitude.

Implementation notes

For proximity search with ranking by distance, use the Elasticsearch geo_distance filter combined with a _geo_distance sort. This returns results sorted by ascending distance from the query point. For "find nearest K" queries, use size: k — Elasticsearch collects the K nearest documents using a priority queue without scanning all documents outside the filter radius. Always set a maximum radius to bound the filter cardinality; an unbounded distance filter degenerates to a full scan. For filtering by arbitrary polygons (service areas, delivery zones), store polygon boundaries as geo_shape in a separate index and use a geo_shape query with relation: intersects.

When building applications that need to serve both proximity queries and map visualisations at scale, consider pre-indexing H3 cell IDs at multiple resolutions alongside the raw coordinates. At resolution 8 (≈460m diameter hexagons), a 5km radius covering approximately 70 H3 cells can be queried with a simple terms filter, which is faster than a geo_distance computation at very high QPS. Uber, Airbnb, and other location-heavy platforms adopted H3 precisely for this reason: it turns a continuous 2D problem into a discrete set membership problem that scales horizontally. Libraries exist for all major languages to compute H3 cell IDs from lat/lon.

Common failure modes

  • WGS84 coordinate order confusion: GeoJSON specifies [longitude, latitude] while Elasticsearch geo_point defaults to {lat, lon} object form; mixing them silently indexes inverted coordinates that produce wrong distance results.
  • Large geo_shape polygons: Indexing a highly detailed coastline polygon with thousands of vertices is extremely expensive; simplify polygons to the precision you actually need before indexing.
  • Unbounded distance filter: A geo_distance query without a maximum radius will scan the entire index if no other filter is applied, causing timeout under load.
  • Grid aggregation over-precision: Requesting geotile_grid at precision 18 over 100M documents returns billions of cells, exhausting memory before returning results.
  • Antimeridian polygon bugs: Polygons that cross the ±180° longitude line require special handling (splitting into two polygons); naive polygon construction produces a shape that wraps the wrong way around the globe.

Decision checklist

  • Is coordinate field type correct (geo_point for points, geo_shape for polygons/lines)?
  • Are coordinate order conventions consistently [lon, lat] for GeoJSON or {lat, lon} for Elasticsearch?
  • Have distance queries been given a maximum radius bound to prevent full-index scans?
  • Have geo_shape polygons been simplified to a reasonable vertex count?
  • Has the antimeridian edge case been tested with coordinates near ±180°?
  • For very high QPS, have H3-based pre-indexed cells been considered?

Example use cases

  • Local business search: "restaurants near me" — geo_distance filter at 2km radius combined with full-text search on restaurant name and cuisine type, sorted by distance then rating.
  • Delivery zone management: Geofencing — when a driver's GPS position crosses a GeoJSON polygon boundary stored in Elasticsearch, trigger an event to update order status.
  • Ride-hailing demand heatmap: Real-time H3 hex grid aggregation of open ride requests displayed on a dispatcher dashboard; cells update every 10 seconds as requests are created and fulfilled.
  • Full-Text Search — Often combined with geo filters for local search (text + proximity).
  • Faceted Search — Geo grid aggregations are a spatial variant of faceted aggregation.
  • Search Infrastructure — Cluster capacity planning with geo_shape indexes requires extra storage headroom.

Further reading