Prompt 20 · Lesson 03 · Spatial query predicates
$near/$nearSphere, $geoWithin, $geoIntersects, Distance, and Query Constraints
Use proximity, containment, and intersection operators according to the question they actually answer.
Learning objectives
Use $near and $nearSphere for ordered proximity and explain their index and distance-unit requirements.
Use $geoWithin for containment without mistaking it for a nearest-first operator.
Use $geoIntersects for shape intersection and distinguish point-in-polygon from polygon-overlap questions.
Test boundary behavior with deterministic points/polygons instead of relying on map screenshots.
Combine spatial predicates with tenant/business filters while preserving correctness and measuring explain evidence.
This lesson pins MongoDB Community Server
8.3.8 using
mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim, mongosh 2.10.0, and PyMongo
4.17.0 where client measurement is useful. The
mandatory lab is a disposable loopback-only standalone on port
27167 named atlasmart-ch20-l3.
Authentication and TLS are disabled only for this isolated
learning container. Default read/write concern and primary read
preference apply.
FCV is observed and never changed. Atlas,
Search, Vector Search, KMS, and Enterprise Advanced are not
required. MongoDB 8.3 creates new 2dsphere indexes
as index version 4; downgrading FCV below 8.3
requires dropping those version-4 indexes first. Product labs
were not executed in this generation environment, so distance,
explain, index-size, and latency evidence that depends on
runtime must be measured locally rather than copied as invented
output. All geographic query examples use GeoJSON and a
2dsphere index; distance bounds are therefore
expressed in meters unless the example explicitly uses
$centerSphere, whose radius is radians.
1. Three questions require three operators
“What is closest?”, “what lies inside this area?”, and “what
geometry touches this area?” are different questions.
$near/$nearSphere return results
nearest-first and require a geospatial index.
$geoWithin selects geometry contained by a region
and does not promise distance ordering.
$geoIntersects selects GeoJSON geometries that
intersect the query geometry and is supported by
2dsphere.
2. Load deterministic points and delivery zones
docker rm -f atlasmart-ch20-l3 2>/dev/null || truedocker volume rm atlasmart-ch20-l3-data 2>/dev/null || truedocker run -d --name atlasmart-ch20-l3 \ -p 127.0.0.1:27167:27017 \ -v atlasmart-ch20-l3-data:/data/db \ mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim \ --bind_ip_alldocker exec atlasmart-ch20-l3 mongosh --quiet --eval 'printjson(db.adminCommand({buildInfo:1}).version);printjson(db.adminCommand({getParameter:1,featureCompatibilityVersion:1}).featureCompatibilityVersion);'
const d=db.getSiblingDB("atlasmart");d.stores_ch20_l3.drop(); d.delivery_zones_ch20_l3.drop();d.stores_ch20_l3.insertMany([ {_id:"center",tenantId:"tenant-a",open:true,location:{type:"Point",coordinates:[49.8671,40.4093]}}, {_id:"east",tenantId:"tenant-a",open:true,location:{type:"Point",coordinates:[49.8871,40.4093]}}, {_id:"north",tenantId:"tenant-a",open:false,location:{type:"Point",coordinates:[49.8671,40.4293]}}, {_id:"far",tenantId:"tenant-a",open:true,location:{type:"Point",coordinates:[49.9671,40.5093]}}, {_id:"other-tenant",tenantId:"tenant-b",open:true,location:{type:"Point",coordinates:[49.8680,40.4100]}}]);d.stores_ch20_l3.createIndex({tenantId:1,location:"2dsphere"},{name:"tenant_location"});d.delivery_zones_ch20_l3.insertMany([ {_id:"z1",tenantId:"tenant-a",geometry:{type:"Polygon",coordinates:[[[49.84,40.39],[49.90,40.39],[49.90,40.44],[49.84,40.44],[49.84,40.39]]]}}, {_id:"z2",tenantId:"tenant-a",geometry:{type:"Polygon",coordinates:[[[49.89,40.40],[49.94,40.40],[49.94,40.45],[49.89,40.45],[49.89,40.40]]]}}]);d.delivery_zones_ch20_l3.createIndex({geometry:"2dsphere"});
3. Nearest-first versus containment
const d=db.getSiblingDB("atlasmart");const origin={type:"Point",coordinates:[49.8671,40.4093]};print("nearest open tenant-a stores within 5 km");printjson(d.stores_ch20_l3.find({tenantId:"tenant-a",open:true,location:{$near:{$geometry:origin,$maxDistance:5000}}},{_id:1}).toArray());const box={type:"Polygon",coordinates:[[[49.85,40.395],[49.90,40.395],[49.90,40.435],[49.85,40.435],[49.85,40.395]]]};print("stores within polygon; no distance-order guarantee");printjson(d.stores_ch20_l3.find({tenantId:"tenant-a",location:{$geoWithin:{$geometry:box}}},{_id:1}).toArray());printjson(d.stores_ch20_l3.explain("executionStats").find({tenantId:"tenant-a",location:{$geoWithin:{$geometry:box}}}));
With GeoJSON on a 2dsphere index, $near uses
spherical distance and its $maxDistance/$minDistance
are meters. $nearSphere also sorts by spherical
distance. Avoid applying an additional
sort() unless you truly want to discard
nearest-first order; that forces another sort.
4. Intersections answer shape-overlap questions
const d=db.getSiblingDB("atlasmart");const pickup={type:"Point",coordinates:[49.895,40.415]};printjson(d.delivery_zones_ch20_l3.find({tenantId:"tenant-a",geometry:{$geoIntersects:{$geometry:pickup}}},{_id:1}).toArray());const route={type:"LineString",coordinates:[[49.85,40.405],[49.93,40.425]]};printjson(d.delivery_zones_ch20_l3.find({tenantId:"tenant-a",geometry:{$geoIntersects:{$geometry:route}}},{_id:1}).toArray());
A point-in-zone lookup can often be expressed as either a point
queried against stored polygons with
$geoIntersects, or stored points queried against a
polygon with $geoWithin. Pick the orientation that
matches your data model.
const d=db.getSiblingDB("atlasmart");const onVertex={type:"Point",coordinates:[49.84,40.39]};printjson({ intersectsAtVertex:d.delivery_zones_ch20_l3.find({geometry:{$geoIntersects:{$geometry:onVertex}}},{_id:1}).toArray(), note:"Record this run; MongoDB does not guarantee degenerate edge/vertex intersection or containment semantics."});
MongoDB explicitly does not guarantee that degenerate geometry is treated as containing/intersecting its own edges or vertices, or another polygon that merely shares an edge/vertex without interior overlap. Therefore a legal, pricing, or authorization boundary must not depend on a vertex-only coincidence. Give the business rule a tolerance/ownership convention and test points just inside and just outside it.
5. Controlled mistakes: units and selectivity
const d=db.getSiblingDB("atlasmart");const earthRadiusM=6371008.8;const radiusM=2000;print("GeoJSON $near maxDistance uses meters");printjson(d.stores_ch20_l3.find({tenantId:"tenant-a",location:{$near:{$geometry:{type:"Point",coordinates:[49.8671,40.4093]},$maxDistance:radiusM}}},{_id:1}).toArray());print("$centerSphere radius must be radians");printjson(d.stores_ch20_l3.find({tenantId:"tenant-a",location:{$geoWithin:{$centerSphere:[[49.8671,40.4093],radiusM/earthRadiusM]}}},{_id:1}).toArray());
Another common failure is running an enormous spatial region and
filtering tenant/status later in application code. That
increases candidate work and can leak cross-tenant data. Keep
authorization/business predicates server-side and inspect
executionStats before declaring the query
efficient.
6. Production judgment
Use proximity when order-by-distance is part of the contract; use containment when order is irrelevant; use intersection for overlapping shapes. Keep units next to API fields, cap user-controlled radii, and test boundary precision. For high-volume public APIs, reject absurdly broad regions, rate-limit expensive shapes, and record radius/shape complexity alongside latency and examined-document evidence.
Bridge. Lesson 4 moves nearest-neighbor work
into aggregation with $geoNear, where business
filters, calculated distances, and downstream reshaping can live
in one pipeline.
docker rm -f atlasmart-ch20-l3 2>/dev/null || truedocker volume rm atlasmart-ch20-l3-data 2>/dev/null || true
Check your understanding
- Which operator guarantees nearest-to-farthest result ordering?
- Does $geoWithin promise distance sorting?
- What unit does GeoJSON $near use for maxDistance?
- What unit does $centerSphere use for its radius?
- Why keep tenant predicates in the database query?
Review the answers
1. $near/$nearSphere (and $geoNear in aggregation).
2. No.
3. Meters.
4. Radians.
5. For correctness/security and to reduce candidate work instead of filtering cross-tenant results in application code.
Authoritative references
- Geospatial Queries — GeoJSON versus legacy coordinates, WGS84, operators, and MongoDB 8.2 representation precedence.
- GeoJSON Objects — supported geometry forms and longitude/latitude ordering.
- 2dsphere Indexes — spherical indexing, sparse behavior, compound rules, and version 4 in MongoDB 8.3.
- 2d Indexes — flat Euclidean indexing for legacy coordinate pairs and compound limitations.
- Geospatial Index Restrictions — covered-query, collation, shard-key, supported-data, and multiple-index restrictions.
- $geometry — EPSG:4326 default coordinate-reference behavior and GeoJSON geometry syntax.
- $near — nearest-first query semantics and index requirements.
- $nearSphere — spherical proximity, distance units, validation, and sorting behavior.
- $geoWithin — containment predicates and unsorted spatial filtering.
- $geoIntersects — intersection predicates for GeoJSON geometry.
- $geoNear — first-stage/index rules, distanceField/key/query options, and distance units.
- $centerSphere — spherical-cap radius semantics in radians.
- Explain Results — executionStats interpretation and plan-format caveats.
- MongoDB 8.3 Release Notes — 2dsphere index version 4 and current 8.3 behavior.
- mongosh Release Notes — mongosh 2.10.0 baseline.
- PyMongo Release Notes — PyMongo 4.17 driver baseline.