Spatial search

This section documents the current Solr line (10.0; 9.10.x the maintained 9.x branch), written and verified against the Apache Solr Reference Guide. No specific patch version is pinned. Some capabilities (the Solr Operator on Kubernetes, the package-manager ecosystem, Learning To Rank model training, and expert plugin development) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Solr filters, sorts, and facets on geographic (and, for RPT, plane) coordinates through a handful of dedicated field types plus two query parsers and a function query. This page covers LatLonPointSpatialField for simple point data, SpatialRecursivePrefixTreeFieldType for shapes and heatmaps, the \{!geofilt}/\{!bbox} filters, geodist() distance sorting, WKT/polygon search, and RPT heatmap faceting. The field types themselves are introduced alongside Solr’s other types in Field types; this page is the query-side companion.

Spatial field types

Type Use it for

LatLonPointSpatialField (LLPSF)

A single lat/lon point. The default choice — fast distance sort, geofilt/bbox filters, no shape support.

SpatialRecursivePrefixTreeFieldType (RPT)

Points or shapes (polygons, lines, envelopes), spatial-relation predicates, and heatmap faceting — what LLPSF cannot do.

BBoxField

A stored bounding box itself (not a point), for queries that match on box overlap/containment rather than distance.

PointType

A plain 2D Cartesian point (no Earth curvature) — the RPT/LLPSF equivalent for non-geodetic data.

<!-- managed-schema -->
<fieldType name="location" class="solr.LatLonPointSpatialField" docValues="true"/>
<field name="store_location" type="location" indexed="true" stored="true"/>

<fieldType name="location_rpt" class="solr.SpatialRecursivePrefixTreeFieldType"
           geo="true" distErrPct="0.025" maxDistErr="0.001" distanceUnits="kilometers"/>
<field name="delivery_zone" type="location_rpt" indexed="true" stored="true"/>

Both types accept "lat,lon" on input:

curl -X POST -H 'Content-Type: application/json' "http://localhost:8983/solr/stores/update?commit=true" \
  -d '[ { "id": "1", "store_location": "40.7128,-74.0060" } ]'

RPT trades LLPSF’s speed and simplicity for shape support: it indexes a point or shape as a set of grid cells at increasing precision levels (a recursive prefix tree over a geohash- or quad-based grid), which is also what makes heatmap faceting possible — the same cell structure the index already maintains is the heatmap grid. See Spatial Search for the full field-type comparison and every configuration attribute.

geofilt and bbox

\{!geofilt} filters to documents within a radius (great-circle distance) of a center point; \{!bbox} filters to the axis-aligned bounding box enclosing that same circle — cheaper to compute since it skips the per-document distance calculation, at the cost of also matching the box’s corners outside the actual circle.

# Circular: documents within 10 km of the given point.
curl --get "http://localhost:8983/solr/stores/select" \
  --data-urlencode 'q=*:*' \
  --data-urlencode 'fq={!geofilt sfield=store_location pt=40.7,-74.0 d=10}'

# Bounding-box equivalent -- faster, looser.
curl --get "http://localhost:8983/solr/stores/select" \
  --data-urlencode 'q=*:*' \
  --data-urlencode 'fq={!bbox sfield=store_location pt=40.7,-74.0 d=10}'

Outside a [source] block those same filters are written escaped, as \{!geofilt sfield=store_location pt=40.7,-74.0 d=10} and \{!bbox sfield=store_location pt=40.7,-74.0 d=10}, so AsciiDoc does not try to interpret the braces. sfield names the spatial field, pt the center point (lat,lon), and d the radius in kilometers. Both parsers work against LatLonPointSpatialField and RPT fields alike.

geodist() — distance sorting and boosting

geodist() is a function query that computes the distance from a point to a spatial field’s value, usable anywhere a function query is — sort, fl (to return the distance), or inside a boost function.

curl --get "http://localhost:8983/solr/stores/select" \
  --data-urlencode 'q=*:*' \
  --data-urlencode 'fq={!geofilt sfield=store_location pt=40.7,-74.0 d=10}' \
  --data-urlencode 'sort=geodist(store_location,40.7,-74.0) asc' \
  --data-urlencode 'fl=id,store_location,dist:geodist(store_location,40.7,-74.0)'

geodist() takes the field and point either as explicit arguments (as above) or implicitly from sfield/pt request parameters when called as bare geodist(). Because it is an ordinary function query, it also composes with relevance the same way any other function does — see Function queries for boosting a text query by proximity instead of only sorting by it, and Relevance & scoring for how a boost function folds into the final score.

RPT fields (and BBoxField, for boxes) accept Well-Known Text shapes on both indexing and querying, using the \{!field} query parser (or the RPT-specific spatial predicate syntax) rather than geofilt/bbox, which only understand a center point and radius.

curl -X POST -H 'Content-Type: application/json' "http://localhost:8983/solr/zones/update?commit=true" \
  -d '[
        { "id": "1", "delivery_zone": "POLYGON((-74.05 40.68, -73.95 40.68, -73.95 40.75, -74.05 40.75, -74.05 40.68))" }
      ]'

# Documents whose delivery_zone intersects the query polygon.
curl --get "http://localhost:8983/solr/zones/select" \
  --data-urlencode 'q=*:*' \
  --data-urlencode 'fq={!field f=delivery_zone}Intersects(POLYGON((-74.1 40.6, -73.9 40.6, -73.9 40.8, -74.1 40.8, -74.1 40.6)))'

Outside a [source] block that filter is written escaped, as \{!field f=delivery_zone}Intersects(POLYGON(…​)). The spatial predicate defaults to Intersects (shares any point with the query shape) and also accepts IsWithin (field shape entirely inside the query shape), Contains (field shape entirely encloses the query shape), IsDisjointTo (shares no point), and IsEqualTo — the same relation vocabulary as Elasticsearch’s geo_shape query, spelled differently. RPT ships two spatial-context implementations selectable per field type: JTS (spatialContextFactory="JTS"), flat-plane geometry with support for polygons with holes and self-intersection repair, and Geo3D (spatialContextFactory="Geo3D"), which computes on the actual ellipsoid rather than a flat projection — more accurate for very large shapes, at higher CPU cost per query.

Heatmap faceting

RPT fields support a facet type that returns a grid of document counts over a bounding region — the data behind a density heatmap overlay, computed from the same prefix-tree cells the field indexes with. It is available both as the legacy facet.heatmap request parameter and, preferably, as a type: heatmap block in the JSON Facet API — see Faceting for the JSON Facet API in full.

curl "http://localhost:8983/solr/zones/select" \
  --data-urlencode 'q=*:*' \
  --data-urlencode 'rows=0' \
  --data-urlencode 'json.facet={
    "zone_density": {
      "type": "heatmap",
      "field": "delivery_zone",
      "geom": "[\"-180,-90\" TO \"180,90\"]",
      "gridLevel": 3
    }
  }'

geom bounds the region as a WKT envelope or a ["minX,minY" TO "maxX,maxY"] range; gridLevel picks a fixed cell resolution directly (higher = finer), while distErrPct picks one relative to `geom’s size instead — set exactly one of the two. The response is a compact counts matrix (row-major, bottom-to-top) sized to fit the requested resolution, meant for rendering rather than paging through like a terms facet.

  • Field types — where LatLonPointSpatialField and RPT are declared alongside Solr’s other field-type classes.

  • Function queries — geodist() and the other functions it composes with.

  • Faceting — the JSON Facet API’s heatmap type in the context of every other facet type.

  • Query parsers — geofilt, bbox, and \{!field} alongside Solr’s other query parsers.

  • Elasticsearch geospatial data & queries — the geo_point/geo_shape equivalents, geo_distance/geo_shape queries, and grid aggregations.