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.

Advanced130–190 minutesSpatial predicates labMongoDB 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

Learning objectives

01

Use $near and $nearSphere for ordered proximity and explain their index and distance-unit requirements.

02

Use $geoWithin for containment without mistaking it for a nearest-first operator.

03

Use $geoIntersects for shape intersection and distinguish point-in-polygon from polygon-overlap questions.

04

Test boundary behavior with deterministic points/polygons instead of relying on map screenshots.

05

Combine spatial predicates with tenant/business filters while preserving correctness and measuring explain evidence.

Reproducible lab baseline

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

start the disposable MongoDB 8.3.8 lab
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);' 
seed stores and service polygons
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

compare $near and $geoWithin
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

find service polygons that intersect a pickup point or route
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.

record an edge/vertex case instead of assuming boundary semantics
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

show a radians-vs-meters trap explicitly
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.

cleanup only this lesson lab
docker rm -f atlasmart-ch20-l3 2>/dev/null || truedocker volume rm atlasmart-ch20-l3-data 2>/dev/null || true

Check your understanding

  1. Which operator guarantees nearest-to-farthest result ordering?
  2. Does $geoWithin promise distance sorting?
  3. What unit does GeoJSON $near use for maxDistance?
  4. What unit does $centerSphere use for its radius?
  5. 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.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.