For AI agents: this page is also available as Markdown at /docs/geo-points-and-queries.md. Every docs page is: add .md to its URL. llms.txt lists them all.
Geo points and queries
A geo_point field holds one location, or several, per document. This page covers defining the field, the point formats you can send, and the queries that match points: distance, bounding box, polygon, shape and grid cell. The examples use the places collection from the Geo search quickstart. For areas and lines rather than points, see Geo shapes.
#Define a geo_point field
A field whose name ends in _p (one point) or _ps (several points) is a geo_point field with a points index, with no definition needed: location_p just works. Any other name needs a definition:
{
"fields": {
"location": {"type": "geo_point", "index": "range"},
"branches": {"type": "geo_point", "index": "range", "multi": true}
}
}
Send it as the schema when you create the collection, or later with PATCH /v1/collections/{collection}/schema. See Schemas and field types.
curl -X PUT https://api.warpsearch.io/compat/es/places \
-H "Authorization: Bearer $WARPSEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mappings": {"properties": {
"name": {"type": "text"},
"category": {"type": "keyword"},
"location": {"type": "geo_point"}
}}}'
| Option | Effect |
|---|---|
index: "range" | Builds a points index, so geo queries read only the matching area. Without it, the same queries scan the field's column and return the same results, more slowly on large collections. |
multi: true | Several points per document, such as a chain's branches. A document matches when any of its points does, and is returned once. |
ignore_malformed | Default false. When true, a point that cannot be read is skipped and the rest of the document is indexed; in a multi-valued field only the bad points are skipped. |
ignore_z_value | Default true: a third coordinate (altitude) is accepted and dropped. When false, a point with a third coordinate is malformed. |
#Send points
A point can be written in any of these forms:
| Form | Example | Order |
|---|---|---|
| Array | [-73.9851, 40.7589] | longitude, latitude |
| Object | {"lat": 40.7589, "lon": -73.9851} | named |
| GeoJSON | {"type": "Point", "coordinates": [-73.9851, 40.7589]} | longitude, latitude |
| String | "40.7589,-73.9851" | latitude, longitude, as in Elasticsearch |
| WKT | "POINT (-73.9851 40.7589)" | longitude, latitude |
| Geohash | "dr5ru7" | 1 to 12 characters, read as the center of the cell |
{"id": "p1", "name": "Harbor Coffee", "location": [-73.9851, 40.7589]}
{"id": "p1", "name": "Harbor Coffee", "location": "40.7589,-73.9851"}
Keep the order straight: arrays, GeoJSON and WKT put longitude first, while the "lat,lon" string puts latitude first.
A multi-valued field takes an array of points, in any mix of forms:
{"id": "c1", "name": "Harbor Coffee (all branches)", "branches": [[-73.9851, 40.7589], "40.7440,-74.0324"]}
- An array of numbers is always one point, so
[-73.9851, 40.7589]sent to a multi-valued field is one point, not two. - A string without a comma that is not WKT is read as a geohash, so a stray
"40"becomes a coarse geohash cell, not a number. - Longitude must be within -180 to 180 and latitude within -90 to 90. A point that is out of range or cannot be read fails that document (unless
ignore_malformedis set), and the rest of the batch is still written; see Partial failures. - A third coordinate (
[lon, lat, altitude],POINT Z (...)) is accepted and the altitude is dropped, unlessignore_z_valueisfalse. Altitude is never indexed. - Points are stored on a fine grid, at roughly centimeter resolution.
- Swapped coordinates cannot be detected when both are in range.
[40.7589, -73.9851]is a valid point in Antarctica.
These forms are for documents. Query centers and corners take "lat,lon", [lon, lat], {"lat": ..., "lon": ...} or "POINT (lon lat)", but not geohashes.
#Return points
Name a geo field in fields (or _source) and each hit returns its points as [lon, lat], whatever form you sent. The values are the stored grid points, so they can differ from the input in about the eighth decimal place.
#Distance queries
A distance query matches points within a radius of a center, edges included. Distances are great-circle distances over the Earth's mean radius.
Cafes within 1 km of Times Square:
{
"language": "es",
"query": {"term": {"category": "cafe"}},
"filters": [{"geo_distance": {"distance": "1km", "location": "40.7580,-73.9855"}}],
"fields": ["name"]
}
{
"query": {"bool": {
"must": [{"term": {"category": "cafe"}}],
"filter": [{"geo_distance": {"distance": "1km", "location": "40.7580,-73.9855"}}]
}},
"_source": ["name"]
}
d is in kilometers:
q=category:cafe&fq={!geofilt sfield=location pt=40.7580,-73.9855 d=1}&fl=id,name
distanceis a number of meters or a string with a unit:m,km,mi,yd,ft,in,cm,mmornmi(for example"500m","2mi").distance_type: planeis accepted with a warning; the distance is always the great-circle one.- Solr's
{!geofilt}takessfield,ptandd.score=is rejected; sort bygeodist()instead (see Sort by distance).
#Bounding boxes
A box matches points between two latitudes and two longitudes, edges included:
Everything in a box around Midtown Manhattan:
{
"language": "es",
"query": {"geo_bounding_box": {"location": {
"top_left": {"lat": 40.77, "lon": -74.00},
"bottom_right": {"lat": 40.74, "lon": -73.97}
}}},
"fields": ["name"]
}
This matches Harbor Coffee (p1) and Midtown Market (p3).
{
"query": {"bool": {"filter": [{"geo_bounding_box": {"location": {
"top_left": {"lat": 40.77, "lon": -74.00},
"bottom_right": {"lat": 40.74, "lon": -73.97}
}}}]}}
}
{!bbox} matches the box around a circle of d kilometers:
q=*:*&fq={!bbox sfield=location pt=40.7580,-73.9855 d=2}
The corners can be given as top_left and bottom_right, as top_right and bottom_left, as the numbers top, left, bottom and right, or as {"wkt": "BBOX (minLon, maxLon, maxLat, minLat)"}. The top must not be below the bottom. A box whose left edge is east of its right edge crosses the date line: left: 170, right: -170 covers 170 degrees east through 180 to 170 degrees west.
#Combine with text and other filters
A geo query used as a query gives every match the same score, and in filters it does not affect scores at all. Put the geo condition in filters and the words in query, and text relevance orders the nearby results:
{
"language": "es",
"query": {"match": {"description": "coffee"}},
"filters": [
{"geo_distance": {"distance": "6km", "location": "40.7580,-73.9855"}},
{"range": {"rating": {"gte": 4.0}}}
],
"fields": ["name", "description"]
}
Geo queries combine with others under bool (must, filter, should, must_not), so "in this box but not within 500 m of this point" is a bool with a geo_bounding_box filter and a geo_distance in must_not. To rank closer results higher instead of cutting them off, see Boost nearby results.
#Polygons and shapes
geo_polygon matches points inside a polygon given as a list of points, at least three:
{
"language": "es",
"query": {"match": {"description": "coffee"}},
"filters": [{"geo_polygon": {"location": {"points": [
{"lat": 40.69, "lon": -74.02}, {"lat": 40.69, "lon": -73.97},
{"lat": 40.74, "lon": -73.97}, {"lat": 40.74, "lon": -74.02}
]}}}],
"fields": ["name"]
}
This matches Bridge Roasters (p2), the only place that mentions coffee inside the area around lower Manhattan and Dumbo. Points can be objects, [lon, lat] arrays or "lat,lon" strings. A geo_polygon has no holes and cannot cross the date line; use geo_shape for those.
geo_shape takes any shape, as a GeoJSON object or a WKT string, and a relation. The same area as WKT, keeping the cafes outside it:
{
"language": "es",
"query": {"term": {"category": "cafe"}},
"filters": [{"geo_shape": {"location": {
"shape": "POLYGON ((-74.02 40.69, -73.97 40.69, -73.97 40.74, -74.02 40.74, -74.02 40.69))",
"relation": "disjoint"
}}}],
"fields": ["name"]
}
This matches Harbor Coffee (p1) and Hudson Espresso (p5).
{
"query": {"bool": {
"must": [{"term": {"category": "cafe"}}],
"filter": [{"geo_shape": {"location": {
"shape": {"type": "Polygon", "coordinates": [[
[-74.02, 40.69], [-73.97, 40.69], [-73.97, 40.74], [-74.02, 40.74], [-74.02, 40.69]
]]},
"relation": "disjoint"
}}}]
}}
}
| Relation | On a geo_point field |
|---|---|
intersects (default) | Some point of the document is in the shape. |
within | Every point of the document is in the shape. For a single-valued field this is the same as intersects. |
disjoint | No point of the document is in the shape. Documents without the field do not match. |
contains | Needs a geo_shape field; rejected on points. |
| 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]] |
| Circle | CIRCLE (lon lat radiusMeters) | circle, "coordinates": [lon, lat], "radius": "10km" |
| Mixed | GEOMETRYCOLLECTION | GeometryCollection |
- Positions are longitude first. Rings must be closed (the last position repeats the first) and have a non-zero area. Out-of-range coordinates are rejected.
- A query shape has at most 10000 vertices; more answers
422 limit_exceeded. - A point inside a hole does not match. A point exactly on an edge can fall either way, so do not rely on edges to separate adjacent areas.
- A polygon that spans more than 180 degrees of longitude and is wound clockwise is read as crossing the date line, as Elasticsearch does.
- A box or circle matches exactly what a bounding box or distance query with the same bounds matches. Shape queries use the points index when the field has one.
indexed_shape(a shape stored in another document) is not supported; send the shape inline.
#Grid cells
geo_grid matches the documents with a point in one grid cell, named by exactly one of:
| Key | Cell | Example |
|---|---|---|
geohash | A geohash, precision 1 to 12 | {"geohash": "dr5ru"} |
geotile | A web map tile, zoom/x/y, zoom 0 to 29 | {"geotile": "12/1206/1539"} |
geohex | An H3 cell, as its index in hex | {"geohex": "<H3 index>"} |
{
"language": "es",
"query": {"term": {"category": "cafe"}},
"filters": [{"geo_grid": {"location": {"geohash": "dr5ru"}}}],
"fields": ["name"]
}
The geohash dr5ru covers Midtown, so this matches Harbor Coffee (p1). Invalid keys are rejected. A cell matches exactly the documents that a grid aggregation bucket with the same key counts, so a click on a map cell can fetch exactly the documents that bucket counted.
On a geo_shape field, a geohash or geotile cell matches the shapes that meet the cell, edges included, so a shape on a cell edge matches both cells. geohex cells work on geo_point fields only and answer 400 on shape fields.