Geo-polygon query #
Matches documents whose geo_point field lies inside an arbitrary
polygon. The ring is given as points in (lat, lon) order; vertices
on the boundary count as inside, and holes can be modelled with a
concave ring.
Real regions are rarely rectangular. City limits, districts, service areas and lakes have corners everywhere — this query filters points by an exact ring instead of the four edges of a bounding box.
How it works #
The field is the standard two-dimensional (lat, lon) BKD tree (any
of the three ingest forms — object, [lon, lat] array, "lat,lon"
string — normalizes to the same pair). Execution is two-phase:
- Bounding-box prefilter. The polygon’s own extents (the min/max latitude and longitude over its vertices) become one BKD tree range. Only leaves overlapping that rectangle are visited.
- Exact point-in-polygon check. Each surviving point is tested
with ray casting (the even-odd rule): a ray fired from the point
toggles inside/outside at every ring edge it crosses, and an odd
count means inside. The closing segment between the last and first
vertex is implied. Points lying exactly on an edge or vertex count
as inside (a tolerance of
1e-9degrees, about0.1 mmat the equator).
The second phase is what makes the query exact: a point inside the ring’s bounding box but in a concave notch is correctly rejected — the coverage suite pins this with a “C”-shaped ring whose box covers a point that the ring itself excludes.
The ring is a single latitude/longitude loop on the plane: edges are
straight in degrees (no great-circle bulge), holes need a concave
ring rather than a second loop, and rings that wrap the antimeridian
are not supported (split them into two queries joined by a bool
should instead).
Examples #
The live example matches the two Beijing places (beijing store,
beijing north):
As a curl request the same query reads:
POST /my-collection/_search
{
"query": {
"geo_polygon": {
"field": "location",
"points": [
[40.85, -74.05],
[40.85, -73.85],
[40.70, -73.85],
[40.70, -74.05]
]
}
}
}
The nested form also works — points may use any of the three point
forms ({"lat": .., "lon": ..}, [lon, lat], "lat,lon"), and
options such as validation_method are accepted for compatibility
and ignored:
{
"query": {
"geo_polygon": {
"location": {
"points": [
{"lat": 40.85, "lon": -74.05},
{"lat": 40.85, "lon": -73.85},
{"lat": 40.70, "lon": -73.85},
{"lat": 40.70, "lon": -74.05}
],
"validation_method": "ignore_malformed"
}
}
}
}
Use cases #
- City and district boundaries. Filter stores or users by the
actual administrative polygon — over the teaching
placesdataset, a four-vertex quad around Beijing picks exactly the two Beijing places out of fifteen. - Service and delivery areas. Zones drawn by hand (a peninsula delivery route, an area bounded by two rivers) whose shape only a ring expresses; concave rings behave exactly as drawn.
- Exclusion zones. The same ring under
boolmust_notkeeps results out of a no-go area — the exact phase makes both the inclusion and the exclusion trustworthy. - Corridor checks. Routes, coastlines or borders approximated by
a many-vertex ring, used as a coarse “inside the corridor” filter
before finer tools such as
geo_shapematch against the line itself.
Parameters for geo_polygon
#
field
(Required, string) Thegeo_pointfield to filter on.points
(Required, array) Three or more ring vertices. In Pizza’s flat form each vertex is[lat, lon]; in the nested form every point form is accepted.
Polygons are interpreted as plain latitude/longitude rings; rings
that wrap the antimeridian are not supported (split them into two
queries joined by a bool should instead).
Related #
geo_bounding_box— the cheaper special case when four edges are enough.geo_distance— round regions around one centre.geo_shape— the indexed side is a full geometry, not a point: match regions against a query region.geohash-gridaggregation andgeotile-gridaggregation — grid the matched points onto map tiles.geo-centroidaggregation — the mean position of the matched points.