4 min readconfiguration

Geospatial Queries in Apache Solr

On this page

“Find the five nearest stores,” “everything within 10 km of the user,” “sort results by distance” — these are everyday requirements, and Apache Solr handles them well once you know which field type and which query parser to reach for. This post walks through indexing coordinates and running the common spatial queries: radius filters, bounding boxes, and sorting or scoring by distance.

# Choosing a spatial field type

Solr gives you a couple of spatial field types, and the right choice depends on what you’re storing:

  • LatLonType — a single latitude/longitude point per document. It’s the simplest, fastest option for the classic “distance from a point” use case, and it’s what most store-locator style features need.
  • SpatialRecursivePrefixTreeFieldType (usually declared as location_rpt) — the more powerful option. It supports multiple points per document, indexing shapes (bounding boxes, and — with the JTS library — polygons), and is the field to use when a point isn’t enough.

For most work you start with LatLonType. Declare the type and a field in schema.xml:

<!-- LatLonType needs a helper dynamic field to store the two ordinates -->
<fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/>
<dynamicField name="*_coordinate" type="tdouble" indexed="true" stored="false"/>

<field name="store_location" type="location" indexed="true" stored="true"/>

# Indexing coordinates

A point is indexed as a single "latitude,longitude" string — note the order:

<add>
  <doc>
    <field name="id">store-100</field>
    <field name="name">Downtown Roasters</field>
    <field name="store_location">45.1729,-93.8712</field>
  </doc>
</add>
It's lat,lon — not lon,lat

Solr expects latitude first, then longitude, comma-separated. This trips up almost everyone, because GeoJSON and many mapping APIs use the opposite order (lon,lat). A point indexed backwards lands in the wrong hemisphere and every distance query returns nonsense — check this first when results look wrong.

# Filtering by radius with geofilt

The geofilt parser returns documents within an exact circular radius of a point. It takes three parameters: sfield (the spatial field), pt (the center point), and d (the distance). Distance is in kilometres by default:

q=*:*
fq={!geofilt sfield=store_location pt=45.15,-93.85 d=5}

Because it’s a filter query (fq), the result is cached and reused — good for facets and repeated searches over the same area.

# The bounding-box shortcut: bbox

geofilt computes a true circle, which is precise but a little more work. bbox uses the same parameters but matches a rectangle around the point — it’s an approximation (the corners reach slightly beyond d), but it’s cheaper:

fq={!bbox sfield=store_location pt=45.15,-93.85 d=5}

A common performance pattern is to combine the two: use the cheap bbox as a coarse pre-filter to knock out most documents, then let the exact geofilt refine what’s left.

flowchart LR
    ALL["All documents"] --> BBOX["bbox filter<br/>(cheap rectangle)"]
    BBOX --> GEO["geofilt filter<br/>(exact circle)"]
    GEO --> HITS["Precise radius hits"]

    classDef all fill:#475569,color:#fff,stroke:#334155;
    classDef coarse fill:#d97706,color:#fff,stroke:#92400e;
    classDef precise fill:#059669,color:#fff,stroke:#065f46;
    class ALL all;
    class BBOX coarse;
    class GEO precise;
    class HITS precise;

# Sorting and returning distance with geodist

geodist() is a function that returns the distance between a document’s point and a query point. That makes “nearest first” a one-liner in the sort parameter:

q=*:*
sfield=store_location
pt=45.15,-93.85
sort=geodist() asc
fl=id,name,dist:geodist()

Here fl=...,dist:geodist() also returns the computed distance as a pseudo-field called dist, so the client can display “2.3 km away” without recomputing anything.

# Boosting by distance

Sometimes you don’t want to sort purely by distance — you want relevance and a nudge toward closer results. Fold geodist() into the score with a boost function so nearer documents rank higher without discarding text relevance entirely:

q=coffee
sfield=store_location
pt=45.15,-93.85
bf=recip(geodist(),2,200,20)
defType=dismax

The recip(...) function turns “smaller distance” into “larger boost,” and dismax adds that boost to the text score — a good default for local search where both keyword match and proximity matter.

# When you need shapes: location_rpt

If a document has more than one location (a chain with many branches) or you need to query by area rather than a point, switch to the RPT field type:

<fieldType name="location_rpt"
           class="solr.SpatialRecursivePrefixTreeFieldType"
           geo="true" distErrPct="0.025" maxDistErr="0.000009" units="degrees"/>
<field name="coverage" type="location_rpt" indexed="true" stored="true" multiValued="true"/>

RPT indexes each shape as a grid of cells, which is what lets it handle multi-valued points and, with the JTS Topology Suite on the classpath, WKT polygons — so you can ask “which delivery zones contain this point” as well as “which points fall inside this zone.”

Pick the field type for the question you're asking

One point per document, “distance from here” → LatLonType. Many points per document, or querying by shapes and polygons → location_rpt (add JTS for polygons). Choosing the heavier RPT type when a simple point would do just costs you index size and query time.

# Wrapping up

Geospatial search in Solr comes down to two decisions: the field type (LatLonType for points, location_rpt for shapes and multi-value) and the query construct (geofilt for exact radius, bbox for a cheap rectangle, geodist() for sorting, scoring, and returning distance). Get the lat,lon order right, filter with fq so results cache, and combine bbox with geofilt when you need both speed and precision.

# References

views

Click to show appreciation

Discussion