Geo-polygon query

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:

  1. 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.
  2. 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-9 degrees, about 0.1 mm at 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 places dataset, 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 bool must_not keeps 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_shape match against the line itself.

Parameters for geo_polygon #

  • field
    (Required, string) The geo_point field 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).

Calendar October 6, 2026
Edit Edit this page