Geospatial Search
The inverted index indexes geographic shapes for fast spatial predicates — finding which points fall inside a region, which regions contain a location and so on. Geometries can be stored either as GeoJSON in a JSON column or as values of the native GEOMETRY type, and both are indexed with a geospatial text search dictionary.
Creating a geospatial index
Use the encode_geojson dictionary template on a JSON column holding GeoJSON. The column type must be JSON or GEOMETRY — a plain VARCHAR is rejected for geo analyzers:
CREATE TABLE geo_data (id INTEGER PRIMARY KEY, geo JSON);
CREATE TEXT SEARCH DICTIONARY geojson_shape AS encode_geojson();
CREATE INDEX geo_idx ON geo_data USING inverted (id, geo geojson_shape);
INSERT INTO geo_data VALUES (1, '{"type":"Point","coordinates":[37.615895,55.703900]}'), (2, '{"type":"Point","coordinates":[37.616297,55.704831]}'), (3, '{"type":"Point","coordinates":[37.617053,55.704610]}'), (4, '{"type":"Point","coordinates":[37.559509,55.715895]}'), (5, '{"type":"Point","coordinates":[37.701645,55.832144]}'), (6, '{"type":"Polygon","coordinates":[[[37.614323,55.705898],[37.615825,55.705898],[37.615825,55.706520],[37.614323,55.706520],[37.614323,55.705898]]]}');
VACUUM (REFRESH_TABLE) geo_data;
SELECT id FROM geo_idxWHERE ST_Intersects(geo, '{"type":"Polygon","coordinates":[[[37.610,55.700],[37.620,55.700],[37.620,55.710],[37.610,55.710],[37.610,55.700]]]}')ORDER BY id;id ---- 1 2 3 6
The encode_geojson template indexes Point, LineString, Polygon, MultiPoint, MultiLineString and MultiPolygon geometries. A row holding a GeometryCollection, or a value that is not valid GeoJSON, is stored without an error but gets no geo terms, so the spatial predicates below never find it. GeoJSON coordinates are [longitude, latitude]. An encode_geopoint template is also available for extracting a point from latitude/longitude fields of a JSON object, and the encode_geojson template accepts coding := 's2point' to store a compact S2 encoding of each geometry instead of the source value.
Indexing a GEOMETRY column
The native GEOMETRY type is indexed the same way, with two requirements. The column must be declared with the CRS84 coordinate reference system, as GEOMETRY('OGC:CRS84'): without one, or with another, CREATE INDEX fails. And it takes only the encode_geojson template, with the default source coding or with coding := 's2point'; encode_geopoint reads JSON fields and needs a JSON column. Values are written from WKT, which uses the same longitude latitude axis order as GeoJSON:
CREATE TABLE geo_shapes (id INTEGER PRIMARY KEY, geo GEOMETRY('OGC:CRS84'));
CREATE TEXT SEARCH DICTIONARY geojson_s2 AS encode_geojson(coding := 's2point');
CREATE INDEX geo_shapes_idx ON geo_shapes USING inverted (id, geo geojson_s2);
INSERT INTO geo_shapes VALUES (1, 'POINT(37.615895 55.703900)'::GEOMETRY('OGC:CRS84')), (2, 'POINT(37.616297 55.704831)'::GEOMETRY('OGC:CRS84')), (3, 'POINT(37.617053 55.704610)'::GEOMETRY('OGC:CRS84')), (4, 'POINT(37.559509 55.715895)'::GEOMETRY('OGC:CRS84')), (6, 'POLYGON((37.614323 55.705898, 37.615825 55.705898, 37.615825 55.706520, 37.614323 55.706520, 37.614323 55.705898))'::GEOMETRY('OGC:CRS84'));
VACUUM (REFRESH_TABLE) geo_shapes;
SELECT id FROM geo_shapes_idxWHERE ST_Intersects(geo, '{"type":"Polygon","coordinates":[[[37.610,55.700],[37.620,55.700],[37.620,55.710],[37.610,55.710],[37.610,55.700]]]}')ORDER BY id;id ---- 1 2 3 6
Query shapes may be supplied as GeoJSON text (as above) or as GEOMETRY literals. For example, ST_Distance_Centroid accepts a GEOMETRY centroid:
SELECT id FROM geo_shapes_idxWHERE ST_Distance_Centroid(geo, 'POINT(37.616 55.704)'::GEOMETRY('OGC:CRS84')) < 500.0ORDER BY id;id ---- 1 2 3 6
Indexing latitude/longitude with encode_geopoint
When your data already stores coordinates as separate latitude and longitude fields of a JSON object, the encode_geopoint template builds a point from them directly — no GeoJSON assembly needed. Name the two fields with the latitude and longitude options:
CREATE TABLE places (id INTEGER PRIMARY KEY, loc JSON);
CREATE TEXT SEARCH DICTIONARY geopoint_dict AS encode_geopoint('lat', 'lng');
CREATE INDEX places_idx ON places USING inverted (id, loc geopoint_dict);
INSERT INTO places VALUES (1, '{"lat":55.703900,"lng":37.615895}'), (2, '{"lat":55.704831,"lng":37.616297}'), (3, '{"lat":55.704610,"lng":37.617053}'), (4, '{"lat":55.715895,"lng":37.559509}');
VACUUM (REFRESH_TABLE) places;
SELECT idFROM places_idxWHERE ST_Intersects( loc, '{"type":"Polygon","coordinates":[[[37.610,55.700],[37.620,55.700],[37.620,55.710],[37.610,55.710],[37.610,55.700]]]}' )ORDER BY id;id ---- 1 2 3
Spatial predicates
ST_Intersects
ST_Intersects(field, shape) matches rows whose indexed geometry shares any space with the query shape — the bounding-box query at the top of this page finds every point and polygon intersecting that region. Intersection with a Point matches the polygons that cover it:
SELECT id FROM geo_idxWHERE ST_Intersects(geo, '{"type":"Point","coordinates":[37.615,55.706]}')ORDER BY id;id ---- 6
ST_Intersects is commutative — the field and shape arguments may be swapped.
ST_Contains
ST_Contains is directional, so argument order matters:
-
ST_Contains(query_shape, field)— rows whose indexed geometry is fully contained withinquery_shape:SQLSELECT id FROM geo_idxWHERE ST_Contains('{"type":"Polygon","coordinates":[[[37.610,55.700],[37.620,55.700],[37.620,55.710],[37.610,55.710],[37.610,55.700]]]}', geo)ORDER BY id;id ---- 1 2 3 6
-
ST_Contains(field, query_shape)— rows whose indexed geometry fully containsquery_shape(e.g. which polygon contains a point):SQLSELECT id FROM geo_idxWHERE ST_Contains(geo, '{"type":"Point","coordinates":[37.615,55.706]}')ORDER BY id;id ---- 6
Distance predicates
ST_Distance_Between(field, centroid, min, max [, incl_min [, incl_max]]) matches rows within a geodesic distance range of a centroid. ST_Distance_Centroid(field, centroid) compared with a constant in WHERE, as in ST_Distance_Centroid(geo, c) < 500.0, keeps the rows within that many metres of the centroid. It cannot order or return rows: in ORDER BY or a select list it fails with Inverted index function called outside inverted index context. Use in WHERE clause on a table with an inverted index. See the function reference.
How rows are checked
Geo predicates are exact: a row matches only when its stored geometry satisfies the predicate. The function reference lists the pairs of shapes a predicate rejects or always answers false for. When the predicate is a required condition of the query, it is checked with the query's other filters: EXPLAIN lists it under Table Filter with Verify: table filter, and the Index Filter shows the index lookup with Verify: false. Under OR, under NOT, next to ts_offsets(), in ts_dict_agg and with a vector score, it is checked as the index finds the rows, shown as Verify: inline.