Prompt 20 · Lesson 04 · $geoNear aggregation
$geoNear in Aggregation Pipelines and Combining Spatial Predicates with Business Filters
Build nearest-neighbor aggregation with distance evidence and business eligibility in one pipeline.
Learning objectives
Build a $geoNear aggregation that returns calculated distance and applies business filters in the geospatial stage.
Explain why $geoNear must be the first pipeline stage and why a geospatial index is mandatory.
Use the key option when multiple geospatial indexes exist and understand current distanceField behavior.
Compare filtering inside $geoNear.query with filtering after the geospatial stage and measure execution evidence.
Diagnose illegal pipeline ordering and ambiguous-index selection safely.
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
27168 named atlasmart-ch20-l4.
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. MongoDB 8.1+ makes distanceField optional
for non-time-series collections, but this lesson specifies it
explicitly because distance is part of the AtlasMart API
response contract.
1. Why $geoNear exists when find($near) already exists
AtlasMart's pickup API needs nearest-first ordering, a
calculated distance field, tenant/open/service filters, and
later projection/grouping. $geoNear is the
aggregation form of a geospatial proximity query. It emits
documents nearest-first, can add the calculated distance and
matched location, and then allows ordinary pipeline stages to
transform the result.
The constraint is intentional: $geoNear must be the
first pipeline stage and requires a geospatial
index. Its query option is therefore the place for
ordinary business predicates that should constrain the candidate
set alongside the geospatial operation.
2. Build a compound geospatial index and a business-aware pipeline
docker rm -f atlasmart-ch20-l4 2>/dev/null || truedocker volume rm atlasmart-ch20-l4-data 2>/dev/null || truedocker run -d --name atlasmart-ch20-l4 \ -p 127.0.0.1:27168:27017 \ -v atlasmart-ch20-l4-data:/data/db \ mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim \ --bind_ip_alldocker exec atlasmart-ch20-l4 mongosh --quiet --eval 'printjson(db.adminCommand({buildInfo:1}).version);printjson(db.adminCommand({getParameter:1,featureCompatibilityVersion:1}).featureCompatibilityVersion);'
const d=db.getSiblingDB("atlasmart");d.pickup_points_ch20_l4.drop();const docs=[];for(let i=0;i<120;i++){ docs.push({_id:`p-${i}`,tenantId:i%5===0?"tenant-b":"tenant-a",open:i%4!==0,category:i%3===0?"locker":"store",location:{type:"Point",coordinates:[49.80+(i%20)*0.007,40.36+Math.floor(i/20)*0.012]}});}d.pickup_points_ch20_l4.insertMany(docs);d.pickup_points_ch20_l4.createIndex({tenantId:1,open:1,location:"2dsphere"},{name:"tenant_open_geo"});printjson(d.pickup_points_ch20_l4.getIndexes());
const d=db.getSiblingDB("atlasmart");const pipeline=[ {$geoNear:{ near:{type:"Point",coordinates:[49.8671,40.4093]}, key:"location", distanceField:"distanceM", maxDistance:12000, query:{tenantId:"tenant-a",open:true}, spherical:true }}, {$project:{_id:1,category:1,distanceM:{$round:["$distanceM",1]}}}, {$limit:10}];printjson(d.pickup_points_ch20_l4.aggregate(pipeline).toArray());printjson(d.pickup_points_ch20_l4.explain("executionStats").aggregate(pipeline));
3. Filter placement is part of the cost model
const d=db.getSiblingDB("atlasmart");const near={type:"Point",coordinates:[49.8671,40.4093]};const early=[{$geoNear:{near,key:"location",distanceField:"distanceM",maxDistance:12000,query:{tenantId:"tenant-a",open:true},spherical:true}},{$limit:20}];const late=[{$geoNear:{near,key:"location",distanceField:"distanceM",maxDistance:12000,spherical:true}},{$match:{tenantId:"tenant-a",open:true}},{$limit:20}];print("EARLY FILTER"); printjson(d.pickup_points_ch20_l4.explain("executionStats").aggregate(early));print("LATE FILTER"); printjson(d.pickup_points_ch20_l4.explain("executionStats").aggregate(late));
Both forms can return the same logical rows for this fixture,
but the evidence may show different work. Do not promise a fixed
percentage improvement; use the real explain plan, result count,
examined candidates, and latency under representative data. Also
remember that the query option cannot itself
contain a $near predicate.
4. Controlled failures: first-stage and multiple-index rules
const d=db.getSiblingDB("atlasmart");try { d.pickup_points_ch20_l4.aggregate([ {$match:{tenantId:"tenant-a"}}, {$geoNear:{near:{type:"Point",coordinates:[49.8671,40.4093]},key:"location",distanceField:"distanceM",spherical:true}} ]).toArray();} catch(e){ print("expected $geoNear-first-stage failure",e.code,e.codeName); }
If a collection has multiple geospatial indexes, specify
key so index choice is unambiguous. Without it,
MongoDB may error when there is more than one index of the
relevant family. Explicit key is also documentation
for future maintainers.
5. Production judgment
Use $geoNear when the API needs distance plus
aggregation. Keep it first, cap distance and result count, and
place ordinary eligibility predicates in query when
they belong to the same candidate contract. With GeoJSON,
distance bounds are meters. If you use legacy coordinate pairs,
units can be radians/coordinate-system dependent; avoid mixing
these forms in one API.
Observe p50/p95/p99 latency, examined candidates, result count, radius, business-filter selectivity, and index bytes. Treat large user-defined radii and complex follow-on pipelines as resource controls, not just query syntax.
Bridge. Lesson 5 turns these rules into a repeatable location-service benchmark with precision and bounding experiments.
docker rm -f atlasmart-ch20-l4 2>/dev/null || truedocker volume rm atlasmart-ch20-l4-data 2>/dev/null || true
Check your understanding
- Where must $geoNear appear in a pipeline?
- Does $geoNear require a geospatial index?
- Why specify key?
- Can $geoNear.query contain another $near predicate?
- Why compare query filtering inside $geoNear with a later $match?
Review the answers
1. As the first stage.
2. Yes.
3. It explicitly selects the geospatial indexed field and is required when multiple relevant geospatial indexes would otherwise be ambiguous.
4. No.
5. To measure candidate/work differences instead of assuming equivalent operational cost from equivalent final rows.
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.