For AI agents: this page is also available as Markdown at /docs/geo-shapes.md. Every docs page is: add .md to its URL. llms.txt lists them all.
Geo shapes
A geo_point field holds locations. A geo_shape field holds geometry: delivery zones, service areas, neighborhoods, parcels, routes, or any mix of polygons, lines and points. Queries then ask how each document's shape relates to a query shape: does this zone cover the customer's address, does this route cross the closure, which parcels lie entirely inside the district.
The examples add a delivery area to some of the places in the Geo search quickstart.
#Define a geo_shape field
curl -X PATCH https://api.warpsearch.io/v1/collections/places/schema \
-H "Authorization: Bearer $WARPSEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"fields": {"delivery_area": "geo_shape"}}'
With settings, the definition is an object with params:
{"fields": {"delivery_area": {"type": "geo_shape", "params": {"max_cells": 64, "err_pct": 0.01}}}}
curl -X PUT https://api.warpsearch.io/compat/es/places/_mapping \
-H "Authorization: Bearer $WARPSEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"properties": {"delivery_area": {"type": "geo_shape"}}}'
The mapping accepts orientation, ignore_malformed and distance_error_pct (the err_pct setting below). index, doc_values and ignore_z_value are accepted when true, and coerce when false. The legacy prefix-tree settings (tree, strategy, precision, tree_levels, points_only), coerce: true, copy_to and multi-fields are rejected with a message naming the setting.
| Setting | Range | Default | Effect |
|---|---|---|---|
max_cells | 1 to 1024 | 8 to 64, growing with the shape's vertex count | The most grid cells used to index one shape. |
max_level | 2 to 32 | 24 | The finest grid level used to index a shape. Odd values round down. |
err_pct | 0 to 0.5 | 0.025 | Stop refining a shape's cells once they are this fraction of its bounding box. |
orientation | right (or ccw, counterclockwise), left (or cw, clockwise) | right | The winding of polygon outer rings in WKT values, used to decide whether a wide polygon crosses the date line. |
pyramid | true, false | true | Also index each cell's ancestors, which speeds up queries with large query shapes at the cost of more index terms. |
Beside type and params, a geo_shape definition takes ignore_malformed and parent. Other options, such as index, are rejected. The settings decide how shapes are indexed, so changing them on an existing field is refused with 409 incompatible_schema; add a new field instead. See Accuracy and index size for how to choose them.
#Send shapes
A shape value is a GeoJSON object, a WKT string (or GeoJSON text in a string), or a point in the [lon, lat] or {"lat": ..., "lon": ...} form. Positions are longitude first.
curl -X POST "https://api.warpsearch.io/v1/collections/places/documents?commit=true" \
-H "Authorization: Bearer $WARPSEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{"id": "p1", "name": "Harbor Coffee", "description": "Espresso, pour-over and pastries",
"category": "cafe", "city": "Manhattan", "rating": 4.6, "location": [-73.9851, 40.7589],
"delivery_area": {"type": "Polygon", "coordinates": [[[-74.00, 40.745], [-73.97, 40.745],
[-73.97, 40.775], [-74.00, 40.775], [-74.00, 40.745]]]}},
{"id": "p3", "name": "Midtown Market", "description": "Groceries, deli and a coffee bar",
"category": "grocery", "city": "Manhattan", "rating": 4.1, "location": [-73.9772, 40.7527],
"delivery_area": "POLYGON ((-73.99 40.74, -73.96 40.74, -73.96 40.765, -73.99 40.765, -73.99 40.74))"},
{"id": "p5", "name": "Hudson Espresso", "description": "Espresso bar by the ferry",
"category": "cafe", "city": "Hoboken", "rating": 4.5, "location": [-74.0324, 40.7440],
"delivery_area": "POLYGON ((-74.05 40.73, -74.01 40.73, -74.01 40.76, -74.05 40.76, -74.05 40.73))"}
]'
A write replaces the whole document, so the example sends every field again.
| Shape | WKT | GeoJSON type |
|---|---|---|
| Point, several points | POINT, MULTIPOINT | Point, MultiPoint |
| Line, several lines | LINESTRING, MULTILINESTRING | LineString, MultiLineString |
| Polygon with holes, several polygons | POLYGON, MULTIPOLYGON | Polygon, MultiPolygon |
| Box | BBOX (minLon, maxLon, maxLat, minLat) | envelope, "coordinates": [[minLon, maxLat], [maxLon, minLat]] |
| Mixed | GEOMETRYCOLLECTION | GeometryCollection |
- Rings must be closed (the last position repeats the first) and have a non-zero area. Coordinates outside -180 to 180 and -90 to 90 are rejected.
- Circles are refused as values; index a polygon instead.
- An empty shape (
POINT EMPTY, an empty collection) means the document has no value. - A value that cannot be read fails the document, or with
ignore_malformed: truethe field is skipped and the rest of the document is indexed. - A GeoJSON polygon is read with the right-hand rule unless the object carries its own
"orientation"; WKT polygons follow the field'sorientation. A polygon that spans more than 180 degrees of longitude and winds against the orientation is read as crossing the date line, as Elasticsearch does. - Edges are straight lines in latitude and longitude, not great circles.
Name the field in fields (or _source) to get the shape back as GeoJSON. The coordinates are the stored grid positions, so they can differ from the input in about the eighth decimal place.
#Query shape fields
geo_shape relates each document's shape to a query shape. Which places deliver to an address near Bryant Park (40.7520, -73.9850)?
{
"language": "es",
"query": {"geo_shape": {"delivery_area": {"shape": "POINT (-73.9850 40.7520)", "relation": "intersects"}}},
"fields": ["name"]
}
{
"query": {"bool": {"filter": [{"geo_shape": {"delivery_area": {
"shape": {"type": "Point", "coordinates": [-73.9850, 40.7520]},
"relation": "intersects"
}}}]}},
"_source": ["name"]
}
This matches Harbor Coffee (p1) and Midtown Market (p3): the address is inside both of their delivery areas, and outside Hudson Espresso's.
| Relation | Matches documents whose shape |
|---|---|
intersects (default) | Shares at least one point with the query shape. |
disjoint | Shares no point with it. Documents without the field do not match. |
within | Lies inside the query shape: each part of the document's shape inside one part of the query shape. |
contains | Contains the query shape, read the same way: each part of the query shape inside one part of the document's shape. |
withinandcontainscompare part by part, as Lucene does: a polygon covered only by the union of two query polygons is not within the query.- Query shapes can be any of the shapes above, and circles:
CIRCLE (lon lat radiusMeters)or{"type": "circle", "coordinates": [lon, lat], "radius": "2km"}. On a shape field, a circle is matched as the 64-sided polygon inscribed in it. - A query shape has at most 10000 vertices; more answers
422 limit_exceeded. - Relations are decided exactly on the stored geometry: the index narrows the candidates and every remaining candidate is checked against the shape, so the settings change speed and size, not results.
indexed_shape(a shape stored in another document) is not supported; send the shape inline.
The point queries work on shape fields too, as intersects queries: geo_distance matches shapes that come within the radius, geo_bounding_box shapes that meet the box, and geo_polygon shapes that meet the polygon. Areas within 1 km of Times Square:
{
"language": "es",
"query": {"geo_distance": {"distance": "1km", "delivery_area": "40.7580,-73.9855"}}
}
A geo_grid query with a geohash or geotile key matches the shapes that meet the cell. Cells include their edges, so a shape that touches a cell edge matches both cells, the same documents the shape grid aggregation counts in that bucket. geohex cells are rejected on shape fields with 400.
Not supported on shape fields: sorting by distance, geo_nearest and distance_feature. For those, keep a representative point, such as a zone's center, in a geo_point field beside the shape.
#Aggregate shape fields
The grid aggregations geohash_grid and geotile_grid and the geo_bounds and geo_centroid metrics work on shape fields, in the same request forms as on points (see Geo aggregations):
{
"language": "lucene",
"query": "*:*",
"limit": 0,
"facets": {"tiles": {"type": "geotile_grid", "field": "delivery_area", "precision": 14}},
"metrics": {"coverage": {"func": "geo_bounds", "field": "delivery_area"}}
}
{
"size": 0,
"aggs": {
"tiles": {"geotile_grid": {"field": "delivery_area", "precision": 14}},
"coverage": {"geo_bounds": {"field": "delivery_area"}}
}
}
- A document counts once in every cell its shape meets. Cells include their edges, so a shape that touches a cell edge counts in both cells.
- A shape that meets more than 65536 cells at the requested precision fails the search. Large shapes need coarse precisions.
geo_boundsis the box around all matching shapes.geo_centroidfollows Elasticsearch's shape centroid: polygons weighted by area, lines by length, points by count, and only the highest dimension present counts.geohex_gridandgeo_distancerings are rejected on shape fields with400, as aregeohexcells in ageo_gridquery.
#Accuracy and index size
A shape is indexed as a set of grid cells that cover it, plus its exact geometry. A query first uses the cells to find candidates, accepts the documents whose cells settle the answer, and checks the rest against the exact geometry. Results are the same whatever the settings; the settings trade index size against how many candidates need that check.
| To | Change | Cost |
|---|---|---|
| Make queries check fewer candidates | Raise max_cells, lower err_pct, raise max_level | More index terms per shape: a larger index and slower writes |
| Shrink the index for many small or simple shapes | Lower max_cells, raise err_pct | More candidates to check at query time |
| Speed up queries with large query shapes (a state, a country) | Keep pyramid: true (the default) | More terms per shape |
| Save index space when query shapes are small | pyramid: false | Large query shapes read more terms |
The defaults suit most data. Change them when a profile of real queries shows many candidates being checked, or when index size matters more than query time. Very detailed shapes, such as coastlines with tens of thousands of vertices, cost more to check: simplify them before indexing when exact borders are not needed.