Spatial search

This section documents the current Apache Lucene 10.x line — Lucene 10 requires Java 21 — as published at the Apache Lucene documentation and Javadoc, which is the reference these pages are written and verified against. No specific patch version is pinned; examples target lucene-core 10.x and the companion modules. Some areas (the Panama foreign-memory / Vector API internals, codec file-format internals, and the nightly benchmark harness) 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, as Lucene iterates quickly.

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

Lucene stores latitude/longitude points and 2D geometries as BKD-tree numeric fields in lucene-core, so the common cases — "within this box", "within this radius", "inside this polygon", "sort by distance" — need no extra module. lucene-spatial-extras adds grid (prefix-tree) and serialized strategies for richer shape relations and non-geodetic planes, and lucene-spatial3d computes on the actual ellipsoid so shapes that cross a pole or the date line behave correctly.

Points with LatLonPoint (lucene-core)

LatLonPoint encodes one (latitude, longitude) pair into an indexed BKD point. Add a matching LatLonDocValuesField when you also need distance sorting or per-hit distance values — the indexed point drives the query, the doc-values field drives the sort.

// https://lucene.apache.org/core/10_0_0/core/org/apache/lucene/document/LatLonPoint.html
import org.apache.lucene.document.Document;
import org.apache.lucene.document.LatLonPoint;
import org.apache.lucene.document.LatLonDocValuesField;
import org.apache.lucene.search.*;

Document d = new Document();
d.add(new LatLonPoint("location", 40.7128, -74.0060));            // indexed, for queries
d.add(new LatLonDocValuesField("location", 40.7128, -74.0060));   // doc values, for sort / retrieval
writer.addDocument(d);

// --- query forms, all built as static factories on LatLonPoint ---
Query box = LatLonPoint.newBoxQuery("location",
        40.68, 40.75, -74.05, -73.95);                            // minLat, maxLat, minLon, maxLon
Query radius = LatLonPoint.newDistanceQuery("location",
        40.7128, -74.0060, 5_000.0);                              // metres (great-circle)
Query poly = LatLonPoint.newPolygonQuery("location",
        new org.apache.lucene.geo.Polygon(
                new double[] {40.6, 40.6, 40.8, 40.8, 40.6},
                new double[] {-74.1, -73.9, -73.9, -74.1, -74.1}));

// distance sort needs the LatLonDocValuesField, not the point
Sort nearest = new Sort(
        LatLonDocValuesField.newDistanceSort("location", 40.7128, -74.0060));
TopFieldDocs hits = searcher.search(radius, 20, nearest);

// relevance boost that decays with distance from a pivot (recency-style, but geographic)
Query feature = LatLonPoint.newDistanceFeatureQuery(
        "location", 3.0f, 40.7128, -74.0060, 10_000.0);           // boost weight, origin, pivot distance

newBoxQuery handles date-line crossing when minLon > maxLon. newDistanceFeatureQuery is the geographic sibling of LongPoint.newDistanceFeatureQuery used for recency in Function & custom scoring — it contributes a score that falls off with distance rather than filtering. Nearest-neighbour retrieval (LatLonPoint.nearest) returns the k closest points directly without a bounding query. The points & range queries page covers the BKD structure these all ride on.

Geometries with LatLonShape and XYShape (lucene-core)

LatLonPoint only indexes points. To index polygons, lines, or a point that must answer CONTAINS/WITHIN/DISJOINT relations, use LatLonShape: it tessellates each geometry into triangles and indexes those as shape-encoded points. XYShape is the identical API over a flat Cartesian plane (floats, no Earth curvature) for floor plans, game maps, or projected data.

// https://lucene.apache.org/core/10_0_0/core/org/apache/lucene/document/LatLonShape.html
import org.apache.lucene.document.Field;
import org.apache.lucene.document.LatLonShape;
import org.apache.lucene.document.ShapeField;
import org.apache.lucene.geo.Polygon;
import org.apache.lucene.search.Query;

Polygon zone = new Polygon(
        new double[] {40.68, 40.68, 40.75, 40.75, 40.68},
        new double[] {-74.05, -73.95, -73.95, -74.05, -74.05});

Document d = new Document();
for (Field f : LatLonShape.createIndexableFields("area", zone)) {   // Field[]
    d.add(f);
}
writer.addDocument(d);

// query: which indexed shapes intersect this polygon?
Query q = LatLonShape.newPolygonQuery("area", ShapeField.QueryRelation.INTERSECTS, zone);
// also: newBoxQuery, newLineQuery, newDistanceQuery (against a Circle),
//       newGeometryQuery (mix of shapes), with WITHIN / CONTAINS / DISJOINT relations

ShapeField.QueryRelation is the relation vocabulary (INTERSECTS, WITHIN, CONTAINS, DISJOINT); it is Lucene’s equivalent of the Elasticsearch geo_shape relation parameter described in Elasticsearch geospatial. See the XYShape Javadoc for the planar variant.

Prefix-tree strategies in lucene-spatial-extras

lucene-spatial-extras predates the BKD shape fields and is still the route when you need spatial4j shapes, custom grids, or the serialized doc-values approach. A SpatialStrategy maps a spatial4j Shape to indexable fields and turns a SpatialArgs (operation + query shape) into a Query.

Strategy What it does

RecursivePrefixTreeStrategy

Indexes a shape as grid cells at increasing precision over a SpatialPrefixTree (GeohashPrefixTree, QuadPrefixTree, S2PrefixTree). Supports INTERSECTS/WITHIN/CONTAINS/DISJOINT and heatmap faceting. The usual choice.

TermQueryPrefixTreeStrategy

The simpler PrefixTreeStrategy base behaviour — grid terms, INTERSECTS only, no recursive pruning.

SerializedDVStrategy

Stores the full geometry in a BinaryDocValues field and evaluates the predicate exactly per document. Precise but slow; usually paired with a coarse prefix-tree strategy in a CompositeSpatialStrategy (fast grid filter, exact confirm).

PointVectorStrategy

Two numeric doc-values fields for a plain point — distance sort / bbox only.

// https://lucene.apache.org/core/10_0_0/spatial-extras/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.html
import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree;
import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.apache.lucene.document.Field;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.shape.Shape;

SpatialContext ctx = SpatialContext.GEO;
SpatialPrefixTree grid = new GeohashPrefixTree(ctx, 11);          // ~ 1 m leaf cells
RecursivePrefixTreeStrategy strategy =
        new RecursivePrefixTreeStrategy(grid, "zone");

// index
Shape polygon = ctx.getShapeFactory().polygon()
        .pointXY(-74.05, 40.68).pointXY(-73.95, 40.68)
        .pointXY(-73.95, 40.75).pointXY(-74.05, 40.75)
        .pointXY(-74.05, 40.68).build();
Document d = new Document();
for (Field f : strategy.createIndexableFields(polygon)) {   // Field[]
    d.add(f);
}
writer.addDocument(d);

// query
Shape queryShape = ctx.getShapeFactory().rect(-74.1, -73.9, 40.6, 40.8);
Query q = strategy.makeQuery(new SpatialArgs(SpatialOperation.Intersects, queryShape));

SpatialContext.GEO uses spatial4j’s own geometry; adding the JTS-backed context (org.locationtech.jts) enables polygons with holes and validity repair. The lucene-spatial-extras module overview lists every strategy and prefix-tree implementation. Solr’s SpatialRecursivePrefixTreeFieldType in Solr spatial search is this strategy wrapped as a field type.

geo3d — geometry on the ellipsoid (lucene-spatial3d)

Grid and projected-plane approaches distort near the poles and across the date line. lucene-spatial3d (the geo3d code) represents every point as an (x, y, z) position on a PlanetModel (WGS84 or SPHERE), so great-circle distance, polygons that enclose a pole, and paths are computed without a projection.

// https://lucene.apache.org/core/10_0_0/spatial3d/org/apache/lucene/spatial3d/Geo3DPoint.html
import org.apache.lucene.spatial3d.Geo3DPoint;
import org.apache.lucene.spatial3d.geom.GeoPolygonFactory;
import org.apache.lucene.spatial3d.geom.GeoPoint;
import org.apache.lucene.spatial3d.geom.GeoPolygon;
import org.apache.lucene.spatial3d.geom.PlanetModel;
import java.util.List;

Document d = new Document();
d.add(new Geo3DPoint("location", 40.7128, -74.0060));             // degrees in, x/y/z stored (WGS84)
writer.addDocument(d);

Query radius = Geo3DPoint.newDistanceQuery(
        "location", PlanetModel.WGS84, 40.7128, -74.0060, 5_000.0);   // metres

// a polygon covering the North Pole -- no special-casing needed
GeoPolygon capPoly = GeoPolygonFactory.makeGeoPolygon(PlanetModel.WGS84, List.of(
        new GeoPoint(PlanetModel.WGS84, Math.toRadians(85), Math.toRadians(0)),
        new GeoPoint(PlanetModel.WGS84, Math.toRadians(85), Math.toRadians(120)),
        new GeoPoint(PlanetModel.WGS84, Math.toRadians(85), Math.toRadians(-120))));
Query polar = Geo3DPoint.newShapeQuery("location", capPoly);

Geo3DPoint also offers newBoxQuery and newPathQuery (a corridor of fixed width along a polyline). The tradeoff is index size and per-query CPU: three encoded dimensions instead of two. The lucene-spatial3d module overview documents the GeoShape hierarchy and the planet models.

Parsing WKT and GeoJSON

Geometries usually arrive as text. SimpleWKTShapeParser in lucene-core parses a Well-Known Text string into the org.apache.lucene.geo objects the shape fields expect; Polygon.fromGeoJSON parses a GeoJSON Polygon / MultiPolygon document.

// https://lucene.apache.org/core/10_0_0/core/org/apache/lucene/geo/SimpleWKTShapeParser.html
import org.apache.lucene.geo.SimpleWKTShapeParser;
import org.apache.lucene.geo.Polygon;
import org.apache.lucene.document.LatLonShape;

Object shape = SimpleWKTShapeParser.parse(
        "POLYGON((-74.05 40.68, -73.95 40.68, -73.95 40.75, -74.05 40.75, -74.05 40.68))");
// returns Polygon / Line / double[] point / Polygon[] / Line[] depending on the WKT type

if (shape instanceof Polygon p) {
    for (var f : LatLonShape.createIndexableFields("area", p)) {
        d.add(f);
    }
}

// GeoJSON -> Polygon[]  (an outer ring plus any holes / multipolygon parts)
Polygon[] fromJson = Polygon.fromGeoJSON(geoJsonString);

SimpleWKTShapeParser.parseExpectedType restricts the accepted type when you know it in advance. For a document store that speaks GeoJSON natively rather than as a parsing step, contrast MongoDB’s 2dsphere indexes.