Lesson 7 — Geographic search #
From schema to data to queries: filter documents by where they are — a coordinate, a rectangle, an arbitrary polygon, or a full geometric shape.
Pizza has two geographic field types, and picking the right one is the whole design step:
geo_point— a single coordinate. Answers “where is this document?” Distance, box and polygon filters run against it.geo_shape— a full geometry: point, line, polygon, multi-variant, envelope or geometry collection. Answers “what area does this document cover?” and supports the relation vocabulary (intersects,within,contains,disjoint).
A store is a geo_point; the district it serves is a geo_shape.
The use case #
Location-aware features — the store finder, the delivery-zone picker, the geofence. None of these are text questions: the filter is geometry. The questions this lesson answers:
- “Stores within 5 km of me” —
geo_point+ distance filter. - “Everything inside this map rectangle / this city boundary” — box and polygon filters.
- “Which delivery zones cover this address?” —
geo_shape+ relation filters (intersects,within, …). - “Does this service area overlap that one?” — the shape-relation vocabulary on full geometries.
Step 1 — declare the schema #
Create a collection whose schema declares both types up front ( create a collection):
curl -X PUT -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' \
-d '{
"schema": {
"properties": {
"name": { "type": "keyword" },
"city": { "type": "keyword" },
"location": { "type": "geo_point" },
"region": { "type": "geo_shape" }
}
}
}' \
http://localhost:28000/geo-lessons
geo_point and geo_shape fields always get a column store — exact
geometry is verified from it at query time, never guessed from an index
approximation. Later schema changes stay additive
(
update schema).
Step 2 — index documents #
A point is {lat, lon} (or a [lat, lon] array); a shape is GeoJSON —
and here is the classic trap: GeoJSON coordinates are [lon, lat],
point objects are {lat, lon}. The wire order comes from the GeoJSON
standard; the object keys read naturally. Watch it on every shape you
write:
curl -X POST -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' \
-d '{
"name": "beijing store",
"city": "beijing",
"location": { "lat": 39.9042, "lon": 116.4074 },
"region": {
"type": "polygon",
"coordinates": [[
[116.4074, 39.9542], [116.4574, 39.9042],
[116.4074, 39.8542], [116.3574, 39.9042],
[116.4074, 39.9542]
]]
}
}' \
http://localhost:28000/geo-lessons/_doc
If JSON objects are not your thing, geo_shape also accepts
WKT
strings — the same diamond as "POLYGON ((116.4074 39.9542, 116.4574 39.9042, 116.4074 39.8542, 116.3574 39.9042, 116.4074 39.9542))".
The rest of this lesson runs against the teaching dataset’s places
collection — 15 Chinese cities, each with a location point and a
small diamond region — so every query below is live and has a known
answer you can count by hand.
Step 3 — filter points #
geo_distance: everything
within 200 km of central Beijing — 2 hits, both Beijing rows:
geo_bounding_box: a
rectangle by its corners — the same two Beijing rows:
geo_polygon: any closed
polygon, points as [lat, lon] pairs — a quad around Beijing, again 2:
Over HTTP the nested form
({"geo_polygon": {"location": {"points": …}}}) is accepted too — the
gateway normalizes it. Polygons may be concave; they must not
self-intersect.
Step 4 — query shapes #
geo_shape compares the
indexed geometry against a query geometry — any GeoJSON type, or WKT.
The relation defaults to intersects; the envelope below clips both
Beijing diamonds, so 2:
within keeps documents whose shape lies entirely inside the query —
grow the envelope to swallow the whole diamond and the same 2 remain,
everything else drops:
disjoint inverts intersects — 13 of 15 cities do not touch the
Beijing envelope:
A WKT string works as the query shape as well:
"shape": "BBOX (116.1, 40.3, 116.7, 39.6)" — the same envelope in
WKT’s minLon, maxLat, maxLon, minLat order, same answers.
Under the hood #
Every geo query runs in two phases: a coarse pass over bounding boxes
in the BKD index (points are 2-dim trees; shapes index their bounding
box as a 4-dim point), then exact geometry verification against the
column store. The first phase only nominates candidates — the second
decides — so results are exact, with no false negatives. The tradeoffs
to know about: coordinates are treated as planar WGS-84 degrees (shapes
wrapping the antimeridian are not supported yet), and geo_shape
filtering has no dedicated scoring — combine with a match query in a
bool when you need ranking.
What you learned #
geo_pointfor where a document is;geo_shapefor what it covers.- GeoJSON is
[lon, lat]; point objects andgeo_polygonpoints are{lat, lon}/[lat, lon]. Mixing them up is the one bug everyone writes once. - Points filter by distance, box, polygon; shapes filter by relation —
intersects,within,contains,disjoint. - Geo filters stay DSL-side — they are demand-gated outside the
SQL M1 subset; SQL reaches geo data only
through its ordinary columns (a keyword
city, a name, a key).
Next: Lesson 8 — joins — one query across two collections, against a live node. Every parameter of the queries in this lesson lives in the geo reference.