Skip to main content
Version: 0.3.36

Nested geospatial

A geometry does not have to be a top-level property. RaisinDB indexes every GeoJSON geometry it finds in a node's properties, inside an object, inside a section element, inside an array, and each one is addressed by its dot path.

Addressing​

The string inside properties->>'…' is the path, and the same string is the property segment of the index key, so the query and the index always agree.

Given this node in a places workspace:

{
"title": "Zurich HB",
"location": { "type": "Point", "coordinates": [8.5402, 47.3779] },
"venue": {
"name": "Main Hall",
"geo": { "type": "Point", "coordinates": [8.5535, 47.3779] }
},
"hero": {
"element_type": "demo:MapSection",
"map_pin": { "type": "Point", "coordinates": [8.5668, 47.3779] }
},
"stops": [
{ "geo": { "type": "Point", "coordinates": [8.673, 47.3779] } },
{ "geo": { "type": "Point", "coordinates": [8.5468, 47.3779] } }
]
}
where the geometry sitshow you write itindex key segment
top levelproperties->>'location'location
inside an objectproperties->>'venue.geo'venue.geo
inside an element (a section field)properties->>'hero.map_pin'hero.map_pin
one element of an arrayproperties->>'stops.0.geo'stops.0.geo
every element of an arrayproperties->>'stops[].geo'not indexed, see below
-- a geometry inside an object
SELECT name, __distance, __matched_path FROM 'places'
WHERE ST_DWITHIN(properties->>'venue.geo', ST_POINT(8.5535, 47.3779), 500);
{"columns":["name","__distance","__matched_path"],
"rows":[{"name":"zurich-hb","__distance":0.0,"__matched_path":"venue.geo"}]}
-- one element of an array
SELECT name FROM 'places'
WHERE ST_DWITHIN(properties->>'stops.0.geo', ST_POINT(8.673, 47.3779), 500);

Array indices are zero-based. The separator is . only; a property whose own name contains a dot is read as a direct key. Nested geometry must be written as properties->>'…': a bare venue.geo parses as a table-qualified column (venue.geo), not a path.

Indexing does not depend on the node type declaring a Geometry property; any stored value shaped like a GeoJSON geometry is indexed at its path. The declared type matters for validation and for the per-property policy.

Each field is its own index​

A query names exactly the field it searches. In the example above a 200 m query at the position of location matches the node, while the same query against venue.geo does not, because that geometry is a kilometre away. No query shape searches "any geometry on the node".

Several geometries on one node: one row per node​

A node that matches through several of its geometries appears once:

  • ST_DWITHIN(properties->>'stops[].geo', …) is true when any element is within the radius.
  • ST_DISTANCE(properties->>'stops[].geo', …) is the minimum over the elements.

That is what makes

SELECT name FROM 'places'
ORDER BY ST_DISTANCE(properties->>'stops[].geo', ST_POINT(8.54, 47.37)) LIMIT 10;

mean "the ten nearest nodes". Ties resolve to the lexicographically smallest concrete path, so the answer is deterministic, and LIMIT k is k nodes.

Which geometry matched: __matched_path and __distance​

Two opt-in columns say how a spatial predicate was satisfied:

ColumnTypeMeaning
__distancedoublemetres from the query centre; for a wildcard, the minimum over the matched elements
__matched_pathtextthe concrete path that produced it, e.g. stops.1.geo
SELECT name, __distance, __matched_path
FROM 'places'
WHERE ST_DWITHIN(properties->>'stops[].geo', ST_POINT(8.54, 47.37), 2000);
{"rows":[{"name":"tour","__distance":1016.79,"__matched_path":"stops.1.geo"}]}

Name them explicitly; SELECT * does not include them.

The [] wildcard is a row scan​

stops[].geo is accepted and gives correct results, but it is not answered from the index: each element is indexed under its own concrete path, so no single cell scan covers them all. The planner applies the predicate per row and explains why:

TableScan: places (spatial index NOT USED for 'places'.'stops[].geo'
(spatial index unusable: 'stops[].geo' is a wildcard over an array of geometries;
each element is indexed under its own concrete path … Name one element
(properties->>'stops.0.geo') to use the index.))

There is no properties->>'*' and no bare ST_DWITHIN(properties, …).

A path that is not indexed yet​

A path whose index has not been built is answered by a row scan with the predicate kept, so results are correct, the scan is full, and a warning names the path and the rebuild command. To index existing data under paths that were never walked before, rebuild the whole workspace:

REBUILD SPATIAL INDEX FOR 'places';
-- {"job_id":"…","workspace":"places","property":"*","scope":"local node only"}

Run it without a PROPERTY filter; that is the form that re-walks whole property trees and discovers nested paths.

Per-field policy, including array fields​

Precisions are configured per property path:

ALTER SPATIAL INDEX FOR 'places' PROPERTY 'venue.geo' SET PRECISIONS = (8, 6);

For an array field, configure the [] spelling; one declaration covers every element:

ALTER SPATIAL INDEX FOR 'places' PROPERTY 'stops[].geo' SET PRECISIONS = (8, 6);

Array indices normalise to [] for policy lookup only (stops.3.geo uses the policy of stops[].geo); the index entry keeps the concrete path. Resolution is exact: a policy on venue does not apply to venue.geo.

SHOW SPATIAL INDEX HEALTH FOR 'places' lists one row per indexed path (area, location, venue.geo, …) with its entry counts and whether it needs a rebuild.

Limits​

  • At most 64 geometry paths per node are indexed. Beyond that the write logs a warning naming the dropped paths; model the extras as child nodes.
  • Every stored geometry is indexed wherever it sits. The node type's schema drives policy (precisions, cover mode), not whether a geometry is indexed.