# Geo search

Geo search finds documents by where they are. A location is a `geo_point` field and an area or
route is a `geo_shape` field. A geo condition is a query like any other: a filter beside a text
query ("coffee within 5 km"), the whole query ("everything in this polygon"), a sort key ("nearest
first"), a scoring signal ("prefer closer results") or an aggregation ("matches per map cell"). It
runs in the same request, over the same consistent view of the collection, as your text queries,
filters and facets.

## What you can do

| Feature | What it does |
|---|---|
| [Distance queries](geo-points-and-queries.md#distance-queries) | Match points within a radius of a center. |
| [Bounding boxes](geo-points-and-queries.md#bounding-boxes) | Match points inside a latitude and longitude box, including boxes that cross the date line. |
| [Point formats](geo-points-and-queries.md#send-points) | Send points as `[lon, lat]`, objects, GeoJSON, WKT, `"lat,lon"` or geohashes, and type fields by the `_p` and `_ps` suffixes. |
| [Sort by distance](geo-distance-and-nearest.md#sort-by-distance) | Order hits nearest (or farthest) first. |
| [Distance decay scoring](geo-distance-and-nearest.md#boost-nearby-results) | Blend text relevance with closeness: full score near a point, fading with distance. |
| [Query buckets by distance or area](geo-aggregations.md#query-buckets) | Count matches within each of several radii or boxes. |
| [Polygons and shapes](geo-points-and-queries.md#polygons-and-shapes) | Match points inside, or outside, a polygon, multipolygon, circle or envelope, or along a line. |
| [Grid cells](geo-points-and-queries.md#grid-cells) | Match the points in one geohash, map tile or H3 cell. |
| [Distances in hits](geo-distance-and-nearest.md#distances-in-hits) | Each hit of a distance sort carries its distance; `/v1` gets a `geo_distance` sort object. |
| [Nearest k](geo-distance-and-nearest.md#nearest-k) | The k documents nearest a point, with filters applied first. |
| [`distance_feature`](geo-distance-and-nearest.md#distance-feature-scoring) | A scoring clause that rewards closeness without narrowing the matches. |
| [Grid aggregations](geo-aggregations.md#grid-aggregations) | Geohash, map tile and H3 heat-map buckets, with metrics and facets per cell. |
| [Distance rings](geo-aggregations.md#distance-rings) | Counts per distance band from a point. |
| [Bounds and centroid](geo-aggregations.md#bounds-and-centroid) | The box around the matches and their average location. |
| [Shape fields](geo-shapes.md) | Index areas and lines, such as delivery zones, and query them by intersects, disjoint, within or contains. |

## Pages in this guide

- [Geo points and queries](geo-points-and-queries.md): defining `geo_point` fields, the point
  formats you can send, and the distance, box, polygon, shape and grid-cell queries.
- [Distance sorting and nearest](geo-distance-and-nearest.md): sorting by distance, boosting
  nearby results, nearest-k search and `distance_feature` scoring.
- [Geo aggregations](geo-aggregations.md): grids, distance rings, bounds, centroids and query
  buckets.
- [Geo shapes](geo-shapes.md): `geo_shape` fields for areas and lines, their queries and
  aggregations, and the accuracy and index size settings.

## Which API to use

Geo queries have no form in Lucene syntax or SQL. Write them as Elasticsearch Query DSL, or use
Solr's spatial parameters:

| Where | How |
|---|---|
| WarpSearch API (`/v1/collections/{c}/search`) | Set `"language": "es"` and put geo queries in `query`, `filters` or `post_filters`. The geo facet types, geo metrics and the `geo_distance` sort object work with any language. |
| [Elasticsearch](compat-elasticsearch.md) and [OpenSearch](compat-opensearch.md) endpoints | Geo queries, the `_geo_distance` sort and geo aggregations, answered in Elasticsearch's shapes. |
| [Solr](compat-solr.md) endpoint, or `"language": "solr"` | `{!geofilt}`, `{!bbox}` and sorting by `geodist()`. |

The MCP `search` tool takes the same geo queries, facets, metrics and sort; see
[Guide for AI agents](agents.md). Searches with geo queries have no native replay form; see
[Native queries and replays](query-languages.md#native-queries-and-replays).

## Choosing a feature

- **Show only nearby results:** a distance, box or polygon query in `filters`. Filters do not
  change scores, so text relevance still orders the hits.
- **Show the closest first:** a filter to bound the area, plus a sort by distance.
- **Rank by relevance, but prefer closer:** a decay function in `function_score`, or a
  `distance_feature` clause beside the text query.
- **"The 5 nearest", whatever the distance:** `geo_nearest`. Until it is live, filter by a generous
  radius and sort by distance.
- **"How many within 1, 5 and 10 km":** distance rings, or query buckets today.
- **A heat map or clustered map markers:** a grid aggregation, then a `geo_grid` query to fetch the
  documents of the cell someone clicks.
- **Areas, not points (delivery zones, regions, routes):** a [`geo_shape` field](geo-shapes.md).

## Quickstart

The examples in this guide use one collection, `places`: cafes, shops and bakeries around New York,
each with a `location`. [Geo shapes](geo-shapes.md) adds a delivery area to some of them.

Create the collection with a `geo_point` field. `index: "range"` builds a points index, which makes
distance and box queries fast on large collections:

```bash
curl -X PUT https://api.warpsearch.io/v1/collections/places \
  -H "Authorization: Bearer $WARPSEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "schema": {
      "fields": {
        "name":        "text",
        "description": "text",
        "category":    "string",
        "city":        "string",
        "rating":      "double",
        "location":    {"type": "geo_point", "index": "range"}
      }
    }
  }'
```

Load a few places. A point is `[longitude, latitude]`, longitude first:

```bash
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]},
    {"id": "p2", "name": "Bridge Roasters", "description": "Coffee roastery with a cafe under the bridge",
     "category": "cafe", "city": "Brooklyn", "rating": 4.4, "location": [-73.9897, 40.7033]},
    {"id": "p3", "name": "Midtown Market", "description": "Groceries, deli and a coffee bar",
     "category": "grocery", "city": "Manhattan", "rating": 4.1, "location": [-73.9772, 40.7527]},
    {"id": "p4", "name": "Astoria Bakery", "description": "Bread, pastries and coffee",
     "category": "bakery", "city": "Queens", "rating": 4.7, "location": [-73.9235, 40.7644]},
    {"id": "p5", "name": "Hudson Espresso", "description": "Espresso bar by the ferry",
     "category": "cafe", "city": "Hoboken", "rating": 4.5, "location": [-74.0324, 40.7440]}
  ]'
```

Find the cafes within 5 km of Times Square (40.7580, -73.9855), nearest first:

::: tabs API
=== WarpSearch
```bash
curl -X POST https://api.warpsearch.io/v1/collections/places/search \
  -H "Authorization: Bearer $WARPSEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "language": "es",
    "query": {
      "query": {"bool": {"filter": [
        {"term": {"category": "cafe"}},
        {"geo_distance": {"distance": "5km", "location": "40.7580,-73.9855"}}
      ]}},
      "sort": [{"_geo_distance": {"location": "40.7580,-73.9855", "order": "asc", "unit": "km"}}]
    },
    "fields": ["name", "city"]
  }'
```

=== Elasticsearch
```bash
curl -X POST https://api.warpsearch.io/compat/es/places/_search \
  -H "Authorization: Bearer $WARPSEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": {"bool": {"filter": [
      {"term": {"category": "cafe"}},
      {"geo_distance": {"distance": "5km", "location": "40.7580,-73.9855"}}
    ]}},
    "sort": [{"_geo_distance": {"location": "40.7580,-73.9855", "order": "asc", "unit": "km"}}],
    "_source": ["name", "city"]
  }'
```

The same body works on `/compat/opensearch/places/_search`.

=== Solr
`d` is in kilometers and `pt` is `lat,lon`:

```bash
curl "https://api.warpsearch.io/compat/solr/places/select" \
  -H "Authorization: Bearer $WARPSEARCH_API_KEY" \
  --data-urlencode "q=*:*" \
  --data-urlencode "fq=category:cafe" \
  --data-urlencode "fq={!geofilt sfield=location pt=40.7580,-73.9855 d=5}" \
  --data-urlencode "sort=geodist(location,40.7580,-73.9855) asc" \
  --data-urlencode "fl=id,name,city"
```
:::

The hits are `p1` (Harbor Coffee, about 0.1 km away), then `p5` (Hudson Espresso, about 4.2 km).
Bridge Roasters, 6.1 km away, is outside the radius.

## Limits

These apply to geo queries and aggregations:

| Limit | Value |
|---|---|
| Vertices in a query shape or polygon | 10000 |
| `k` of `geo_nearest` | Your plan's result window |
| Cells a grid aggregation returns | Your plan's maximum facet size. The default `size` is 10000, or that maximum if it is lower |
| Distinct cells a grid aggregation counts | 65536 |
| Ranges of a distance-ring aggregation | Your plan's maximum facet size |

Going past a limit answers `422 limit_exceeded`, except the distinct-cell count, which fails the
search with an error. See [Limits and errors](limits-and-errors.md).

Distances are great-circle (haversine) distances. Elasticsearch's `distance_type: plane` is
accepted with a warning. Query centers can be `"lat,lon"`, `[lon, lat]`, `{"lat": ..., "lon": ...}`
or `"POINT (lon lat)"`; geohash centers are rejected.
