# Distance sorting and nearest

A distance filter decides which places match. This page is about order: nearest first, relevance
that fades with distance, or simply the k closest documents. The examples use the `places`
collection from the [Geo search quickstart](geo-search.md#quickstart) and measure from Times Square
(40.7580, -73.9855).

| You want | Use |
|---|---|
| Nearest first, within an area you choose | A distance or box filter, plus [a sort by distance](#sort-by-distance) |
| Relevance first, closer results higher | [A decay function](#boost-nearby-results) in `function_score` |
| The distance of each hit | [Distances in hits](#distances-in-hits) |
| The k nearest, however far | [`geo_nearest`](#nearest-k) |
| A closeness bonus beside a text query | [`distance_feature`](#distance-feature-scoring) |

## Sort by distance

::: tabs API
=== WarpSearch
In the `es` language, put the Elasticsearch `sort` in the search body:

```json
{
  "language": "es",
  "query": {
    "query": {"bool": {"filter": [
      {"term": {"category": "cafe"}},
      {"geo_distance": {"distance": "10km", "location": "40.7580,-73.9855"}}
    ]}},
    "sort": [{"_geo_distance": {"location": "40.7580,-73.9855", "order": "asc", "unit": "km"}}]
  },
  "fields": ["name"]
}
```

The hits come back as `p1` (about 0.1 km), `p5` (4.2 km) and `p2` (6.1 km).

=== Elasticsearch
```json
{
  "query": {"bool": {"filter": [
    {"term": {"category": "cafe"}},
    {"geo_distance": {"distance": "10km", "location": "40.7580,-73.9855"}}
  ]}},
  "sort": [{"_geo_distance": {"location": "40.7580,-73.9855", "order": "asc", "unit": "km"}}],
  "_source": ["name"]
}
```

=== Solr
`geodist()` is in kilometers. Name the field and point in the call, or set `sfield` and `pt` and
call `geodist()` with no arguments:

```
q=category:cafe&fq={!geofilt sfield=location pt=40.7580,-73.9855 d=10}&sort=geodist(location,40.7580,-73.9855) asc
q=category:cafe&sfield=location&pt=40.7580,-73.9855&fq={!geofilt d=10}&sort=geodist() asc
```
:::

- `_geo_distance` takes one field and one point (`"lat,lon"`, `[lon, lat]`, `{"lat", "lon"}` or
  `"POINT (lon lat)"`). Several fields or several points in one sort are rejected.
- `order` is `asc` (nearest first) or `desc`. `unit` (meters by default) changes the reported
  distances, not the order.
- For a multi-valued field, `mode` picks the point that counts: `min` (the nearest, the default
  ascending) or `max` (the farthest, the default descending). `avg`, `median` and `sum` are
  rejected.
- Documents without a point sort last in either direction. `missing` is ignored with a warning.
- A distance sort can be followed by other keys, for example `"_score"` or `{"rating": "desc"}` to
  break ties.
- Combine the sort with a distance or box filter. Sorting a whole collection by distance works, but
  a filter keeps the work, and the result list, to the area people care about.
- Sorting by a `geo_shape` field is rejected.

### Distances in hits

Each hit of a distance sort carries its sort values under `sort`, with the distance in the sort's
`unit`. On the Elasticsearch endpoint this is the usual `hits.hits[].sort`. On `/v1`, a
`geo_distance` sort object works with any query language:

```json
{
  "language": "es",
  "query": {"term": {"category": "cafe"}},
  "filters": [{"geo_distance": {"distance": "10km", "location": "40.7580,-73.9855"}}],
  "sort": [{"geo_distance": {"field": "location", "origin": "40.7580,-73.9855", "unit": "km"}}, "name"],
  "fields": ["name"]
}
```

```json
"hits": [
  {"id": "p1", "sort": [0.106], "fields": {"name": "Harbor Coffee"}},
  {"id": "p5", "sort": [4.246], "fields": {"name": "Hudson Espresso"}},
  {"id": "p2", "sort": [6.093], "fields": {"name": "Bridge Roasters"}}
]
```

(Distances rounded here.)

| Key | Meaning |
|---|---|
| `geo_distance.field` | The `geo_point` field. |
| `geo_distance.origin` | The point, in any query point form. Or give `lat` and `lon` as numbers. |
| `geo_distance.unit` | `m` (default), `km`, `mi`, `yd`, `ft`, `in`, `cm`, `mm` or `nmi`, for the reported distance. |
| `geo_distance.mode` | `min` or `max`, for multi-valued fields. |
| `desc` | Beside `geo_distance`: `true` for farthest first. |

The MCP `search` tool takes the same sort object.

## Boost nearby results

A decay function scores a document by how far it is from an origin: the full score up to `offset`,
`decay` (0.5 by default) at `offset + scale`, and less beyond. Multiply it into the text score and a
relevant place a block away ranks above an equally relevant one across town:

::: tabs API
=== WarpSearch
```json
{
  "language": "es",
  "query": {"function_score": {
    "query": {"match": {"description": "coffee"}},
    "functions": [
      {"gauss": {"location": {"origin": "40.7580,-73.9855", "scale": "2km", "offset": "500m", "decay": 0.5}}}
    ],
    "boost_mode": "multiply"
  }},
  "fields": ["name", "description"]
}
```

=== Elasticsearch
```json
{
  "query": {"function_score": {
    "query": {"match": {"description": "coffee"}},
    "functions": [
      {"gauss": {"location": {"origin": "40.7580,-73.9855", "scale": "2km", "offset": "500m", "decay": 0.5}}}
    ],
    "boost_mode": "multiply"
  }},
  "_source": ["name", "description"]
}
```
:::

| Parameter | Meaning |
|---|---|
| `origin` | The point to measure from: `"lat,lon"`, `[lon, lat]`, `{"lat", "lon"}` or `"POINT (lon lat)"`. |
| `scale` | Required. The distance past `offset` at which the score falls to `decay`, such as `"2km"`. |
| `offset` | Default `0`. Distances up to this score fully. |
| `decay` | Default `0.5`, strictly between 0 and 1. |

- `gauss` falls slowly near the origin and quickly after `scale`, `exp` falls fastest close in, and
  `linear` reaches zero at a finite distance.
- A multi-valued field decays from its nearest point. `multi_value_mode` other than `min` is
  rejected.
- `boost_mode`, `score_mode`, `weight` and filtered functions work as in Elasticsearch, so a decay
  can be one of several functions, for example next to `field_value_factor` on `rating`.
- A decay does not narrow the matches. Add a distance filter if far-away places should not appear at
  all.

## Nearest k

`geo_nearest` matches the `k` documents nearest a point, however far they are, the way `knn` finds
the nearest vectors. It is a WarpSearch extension to the Query DSL. On `/v1`, `filters` narrow the
candidates first, so "the 3 nearest cafes" is the 3 nearest documents that pass the filter, not
the 3 nearest places filtered afterwards:

```json
{
  "language": "es",
  "query": {"geo_nearest": {"field": "location", "origin": "40.7580,-73.9855", "k": 3}},
  "filters": [{"term": {"category": "cafe"}}],
  "fields": ["name"]
}
```

This returns `p1`, `p5` and `p2`, nearest first. On the Elasticsearch endpoint, send
`{"query": {"geo_nearest": {...}}}`.

| Parameter | Meaning |
|---|---|
| `field` | The `geo_point` field. |
| `origin` | The point, in any query point form. Or give `lat` and `lon` as numbers. |
| `k` | Required, positive, at most your plan's result window. How many documents to return at most. |
| `max_distance` | Optional. Stop at this radius, such as `"25km"`, so fewer than `k` may match. |

- Hits score `1 / (1 + meters)`, so the default order by score is nearest first. Add a
  [distance sort](#distances-in-hits) to get the distances.
- A document with several points counts once, at its nearest point. Documents at the same distance
  keep index order.
- `k` is over the whole collection, so pages beyond `k` are empty: page within `k`.
- With a points index the cost follows `k`, not the size of the collection.

Use `geo_nearest` when the answer must have `k` results even in sparse areas. When the area is what
matters ("within 5 km"), a distance filter with a sort by distance is the better fit.

## distance_feature scoring

`distance_feature` gives every document with a point a score of `boost * pivot / (pivot + meters)`
from its nearest point: the full `boost` at the origin, half of it at `pivot`. Its usual place is a
`should` clause beside a text query, so it reorders the text matches without adding or removing
any:

```json
{
  "language": "es",
  "query": {"bool": {
    "must": [{"match": {"description": "coffee"}}],
    "should": [{"distance_feature": {"field": "location", "origin": "40.7580,-73.9855", "pivot": "1km", "boost": 2}}]
  }},
  "fields": ["name"]
}
```

The same body, without `"language"`, works on the Elasticsearch endpoint.

| Parameter | Meaning |
|---|---|
| `field` | The `geo_point` field. |
| `origin` | The point: `"lat,lon"`, `[lon, lat]`, `{"lat", "lon"}` or `"POINT (lon lat)"`. |
| `pivot` | Required. The distance at which the score halves, such as `"1km"`. |
| `boost` | Optional. The score at the origin, to weigh closeness against the text score. |

- On its own as the query, it matches every document with a point, ranked by closeness, and skips
  documents that cannot reach the top results.
- It does not narrow the matches: add a distance or box filter for that.
- Elasticsearch's `distance_feature` on date fields is not supported.

Compared with a decay function, `distance_feature` adds to the text score rather than multiplying
it, and needs only one parameter.
