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.

Advanced120–190 minutes$geoNear/explain labMongoDB 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

Learning objectives

01

Build a $geoNear aggregation that returns calculated distance and applies business filters in the geospatial stage.

02

Explain why $geoNear must be the first pipeline stage and why a geospatial index is mandatory.

03

Use the key option when multiple geospatial indexes exist and understand current distanceField behavior.

04

Compare filtering inside $geoNear.query with filtering after the geospatial stage and measure execution evidence.

05

Diagnose illegal pipeline ordering and ambiguous-index selection safely.

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 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

start the disposable MongoDB 8.3.8 lab
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);' 
seed pickup locations and create the index
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());
run $geoNear with server-side business predicates
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

compare early geoNear.query filtering with late $match
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

make invalid pipeline order visible
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.

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

Check your understanding

  1. Where must $geoNear appear in a pipeline?
  2. Does $geoNear require a geospatial index?
  3. Why specify key?
  4. Can $geoNear.query contain another $near predicate?
  5. 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.

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.