Introduction
Geo search is one of the most practical features in Elasticsearch that engineers encounter in production. When your team is building ride-hailing apps, local marketplaces, or location-based SaaS platforms, you need to answer one core question fast: what's near the user? Geo search: everything engineers need to know in practice starts with understanding how to store location data efficiently, then filter by radius, sort by proximity, and finally aggregate spatial data for insights. In this guide, we'll walk through the four pillars that make geo queries work reliably at scale.
geo_point — Store latitude and longitude
The foundation of any geo search system is the geo_point field type in Elasticsearch. A geo_point stores a single latitude and longitude coordinate pair, allowing Elasticsearch to understand and reason about physical location on Earth.
When you define a mapping, you declare which fields are geo_points. This is critical because Elasticsearch uses a specialized indexing strategy (quadtree decomposition) under the hood to make spatial queries fast. Without the correct field type, your geo queries will fail or perform poorly.
Here's how you'd map a document for a ride-hailing or delivery platform:
{
"mappings": {
"properties": {
"id": {"type": "keyword"},
"driver_name": {"type": "text"},
"location": {
"type": "geo_point"
},
"vehicle_type": {"type": "keyword"},
"timestamp": {"type": "date"}
}
}
}You can index location data in multiple formats. The most common in production is the object format:
{
"id": "driver-123",
"driver_name": "Alice",
"location": {
"lat": 40.7128,
"lon": -74.0060
},
"vehicle_type": "economy",
"timestamp": "2025-01-15T14:30:00Z"
}Alternatively, you can use the string format (WGS-84 convention: latitude,longitude):
{
"location": "40.7128,-74.0060"
}We recommend the object format in team codebases because it's self-documenting and catches serialization errors early. One common pitfall: always store longitude first in WGS-84 standard when using strings, or lat/lon in objects. Swapping them silently produces queries that return wrong results and are nightmarish to debug.
geo_distance — Filter by radius
Once your location data is indexed as geo_points, filtering by distance is straightforward. The geo_distance query lets you find all documents within a specified radius of a center point. This is the workhorse query for "find restaurants near me" or "show available drivers in my area."
Here's a practical example: a user is at coordinates (40.7128, -74.0060) and wants to find all available drivers within 5 kilometers:
{
"query": {
"bool": {
"must": [
{
"geo_distance": {
"distance": "5km",
"location": {
"lat": 40.7128,
"lon": -74.0060
}
}
},
{
"term": {
"status": "available"
}
}
]
}
}
}The distance parameter accepts units: km, mi, m, cm, etc. In production systems, we typically store distance as kilometers or miles depending on your target region. The query returns all drivers whose location field falls within the circle defined by center and radius.
You can also use geo_distance_range to specify both minimum and maximum distances, useful for excluding drivers too close (who may be attending another ride) or too far:
{
"query": {
"geo_distance": {
"distance": "1km",
"distance_type": "arc",
"location": {
"lat": 40.7128,
"lon": -74.0060
}
}
}
}A performance note: Elasticsearch supports two distance calculation methods via distance_type: arc (default, more accurate for Earth's curve, slightly slower) and plane (faster but less accurate for large radii). For most ride-hailing and delivery use cases, arc is precise enough and the performance difference is negligible on modern hardware.
Sort by distance — Nearest results first
Finding documents within a radius is only half the story. Users expect results ranked by proximity, with the closest options appearing first. The geo_distance sort lets you order query results by distance from a reference point.
Let's extend our driver search to show the nearest available drivers first:
{
"query": {
"bool": {
"must": [
{
"geo_distance": {
"distance": "10km",
"location": {"lat": 40.7128, "lon": -74.0060}
}
},
{"term": {"status": "available"}}
]
}
},
"sort": [
{
"_geo_distance": {
"location": {
"lat": 40.7128,
"lon": -74.0060
},
"order": "asc",
"unit": "km"
}
}
],
"size": 10
}This query returns up to 10 drivers sorted by distance in ascending order (closest first). The unit parameter specifies what distance unit appears in the _geo_distance field in your response. In the results, you'll see each driver with a computed distance field:
{
"hits": {
"hits": [
{
"_id": "driver-456",
"_source": {
"driver_name": "Bob",
"location": {"lat": 40.7145, "lon": -74.0070}
},
"sort": [0.7]
},
{
"_id": "driver-789",
"_source": {
"driver_name": "Carol",
"location": {"lat": 40.7200, "lon": -74.0100}
},
"sort": [2.1]
}
]
}
}The sort array contains the distance in kilometers. Combining filtering and sorting is the pattern you'll use in almost every production geo application. Teams often add pagination here to implement "show 10 nearest drivers, then load more," avoiding loading thousands of results upfront.
Geo aggregations — Heatmaps and grids
Beyond individual point queries, Elasticsearch provides aggregations that group documents by geographic areas. The geohash_grid and geo_grid aggregations are invaluable for building heatmaps, demand density visualizations, or analytics dashboards.
Imagine you want to see which areas of a city have the most active drivers. A geohash_grid aggregation divides your map into a grid of buckets and counts documents in each cell:
{
"size": 0,
"aggs": {
"driver_density": {
"geohash_grid": {
"field": "location",
"precision": 6
},
"aggs": {
"avg_rating": {
"avg": {"field": "rating"}
}
}
}
}
}The precision parameter controls grid granularity. Lower values (1–3) give coarse buckets spanning entire regions; higher values (6–8) give fine-grained street-level detail. Precision 6 typically covers a few city blocks. Each bucket in the response includes a geohash string, document count, and any nested aggregations (like average driver rating).
For modern Elasticsearch (7.10+), the geo_hex_grid aggregation is a good alternative using hexagonal grids instead of geohash cells. Hexagons provide more uniform spatial coverage and are easier to visualize on maps:
{
"size": 0,
"aggs": {
"hex_grid": {
"geo_hex_grid": {
"field": "location",
"precision": 8
}
}
}
}Common use cases: product teams use these aggregations to render heatmaps in dashboards showing supply concentration. Operations teams use them to identify under-served areas. Both need to be computed in near real-time, so we cache aggregation results for 5–10 minutes in Redis rather than querying Elasticsearch on every dashboard load.
Common mistakes
- Swapping lat/lon: Double-check your coordinate order. WGS-84 standard is longitude-first in some APIs and latitude-first in objects. One silent swap and your query is silently querying the wrong hemisphere.
- Forgetting to set the mapping: New engineers sometimes try geo queries on fields without declaring
"type": "geo_point". The query will fail or fall back to expensive scanning. Always verify your mapping before indexing location data. - Over-fetching with large radius: A 50km radius in a dense urban area might return thousands of documents. Combine geo_distance with explicit
sizeparameters and pagination to avoid memory spikes. - Precision mismatch in aggregations: Using very high precision (geohash precision 8+) on billions of documents can create millions of buckets, exhausting heap memory. Start with precision 4–6 and adjust based on response times.
- Ignoring performance of distance_type: In high-QPS systems, switching from arc to plane can reduce query latency. Benchmark both in your environment before deciding.
Key takeaways
- Use
geo_pointfield type to index latitude and longitude, ensuring Elasticsearch builds the right spatial index structures. - Filter with
geo_distancequeries to find documents within a radius, typically combined with other boolean conditions. - Sort results by distance using
_geo_distanceto show nearest items first, improving user experience in location-aware apps. - Aggregate with
geohash_gridorgeo_hex_gridto build supply heatmaps and density analytics without separate queries. - Always verify coordinate order, validate mappings, and paginate large result sets to avoid silent bugs and performance regressions in production.






Comments
Loading comments…