Modern ride-hailing and food delivery apps rely heavily on location-based search. When a user opens the app, it must quickly find nearby drivers or restaurants, usually within a few kilometers.
The faster the search, the better the user experience.
Elasticsearch is not only for text search, it also supports geo-spatial queries that can find locations inside a specific area. In this article, we will learn how to design radius-based location search efficiently using polygon approximation.
The Problem: Searching by Radius Is Expensive
A common idea is to search by a circle radius, for example,
“Find all merchants within 5 km from my current location.”
In Elasticsearch, this can be done using a geo_distance query:
"geo_distance": {
"distance": "5km",
"location": { "lat": -6.2, "lon": 106.8 }
}
But there is a problem. This query must calculate the Haversine distance (real-world spherical distance) for every document inside the bounding area. When you have thousands of merchants, this becomes slow and CPU-heavy.
The Idea: Use Polygon Instead of Circle
To make it faster, we can approximate the circle with a polygon, a closed shape made of straight lines.
Example:
A 5-km radius circle can be represented as a polygon with 32 points (sides).
It is not a perfect circle, but very close.
* * * * *
* *
* *
* *
* * * * *
By using polygon instead of a true circle:
- Elasticsearch can use geo-shape indexing (based on BKD trees).
- The query becomes a simple spatial check (“is inside polygon?”).
- No need to calculate distance for each document.
- Much faster with minimal accuracy loss.
Understanding Error Margin
Because a polygon cannot perfectly match a circle,
there will always be a small error margin — the difference between
the real circular area and the polygon boundary.
| Polygon sides | Approximation error |
|---|---|
| 6 (hexagon) | ~15% radius error |
| 16 | ~5% |
| 32 | ~2% |
| 64 | ~1% |
- False positive: some points outside the real circle may still be inside the polygon.
- False negative: a few edge points may be missed.
In most delivery systems, a small error (2-5%) is acceptable,
because users only care that the list feels fast and mostly correct.
How It Works (Step by Step)
Let’s look at how a ride-hailing backend can do this.
Step 1 User Sends Location
The client app sends:
{
"lat": -6.2000,
"lon": 106.8167,
"radius_km": 5
}
Step 2 Backend Generates Polygon Points
The backend converts the circle into polygon coordinates using simple math:
package main
import (
"fmt"
"math"
)
// GenerateCirclePolygon generates a polygon that approximates a circle
// centered at (lat, lon) with the given radius in kilometers.
// The polygon has the specified number of sides (default 32 for smooth shape).
func GenerateCirclePolygon(lat, lon, radiusKm float64, sides int) [][]float64 {
const EarthRadius = 6371.0 // in km
coords := make([][]float64, 0, sides+1)
for i := 0; i < sides; i++ {
angle := 2 * math.Pi * float64(i) / float64(sides)
dx := radiusKm * math.Cos(angle) / EarthRadius
dy := radiusKm * math.Sin(angle) / (EarthRadius * math.Cos(lat*math.Pi/180))
lat_i := lat + (dy * 180 / math.Pi)
lon_i := lon + (dx * 180 / math.Pi)
coords = append(coords, []float64{lon_i, lat_i})
}
// Close the polygon by repeating the first point
coords = append(coords, coords[0])
return coords
}
func main() {
lat := -6.2000
lon := 106.8167
radiusKm := 5.0
sides := 32
polygon := GenerateCirclePolygon(lat, lon, radiusKm, sides)
fmt.Println("Generated polygon coordinates:")
for _, point := range polygon {
fmt.Printf("[%.6f, %.6f]\n", point[0], point[1])
}
}
Now the backend has a polygon shape with ~32 points around the user’s location.
Step 3 Store Merchant Locations as Geo Points
Each merchant or driver location is stored as a geo_point:
PUT merchants
{
"mappings": {
"properties": {
"name": { "type": "text" },
"location": { "type": "geo_point" }
}
}
}
Example documents:
{ "name": "Mindworks Café", "location": { "lat": -6.21, "lon": 106.81 } }
{ "name": "Beauty Spa", "location": { "lat": -6.25, "lon": 106.85 } }
Step 4 Search Merchants Inside the Polygon
Now we can send a geo-shape query using the polygon from Step 2.
GET merchants/_search
{
"query": {
"geo_shape": {
"location": {
"shape": {
"type": "polygon",
"coordinates": [[
[106.80, -6.15],
[106.83, -6.18],
[106.86, -6.21],
[106.83, -6.24],
[106.80, -6.27],
[106.77, -6.24],
[106.74, -6.21],
[106.77, -6.18],
[106.80, -6.15]
]]
},
"relation": "within"
}
}
}
}
Returns all merchants whose coordinates fall inside that polygon, without doing any distance math per document.
