Geospatial queries: 2dsphere, $near, $geoWithin and $geoNear
Store locations as GeoJSON, index them with 2dsphere, and answer nearest-store, within-radius and inside-polygon questions with correct units.
- Store points as GeoJSON with [longitude, latitude] order
- Write $near, $geoWithin and $geoNear queries
- Convert kilometres to radians for $centerSphere
- Know which operators need an index
conceptThe concept
Store locations as GeoJSON: { type: "Point", coordinates: [longitude, latitude] }. Longitude comes first; swapping the order is the most common geo bug. Index with { loc: "2dsphere" } for earth-like (spherical) geometry; 2d indexes are for legacy flat coordinate pairs.
| Question | Operator | Index | Units |
|---|---|---|---|
| Nearest first, optionally within a distance | $near / $nearSphere in find | Required | metres with GeoJSON |
| Everything within a radius or polygon (unsorted) | $geoWithin with $centerSphere or $geometry | Optional (faster with) | radians for $centerSphere |
| Nearest with the distance returned, plus more pipeline stages | $geoNear aggregation stage (must be first) | Required | metres with GeoJSON |
| Shapes that touch | $geoIntersects | Optional | - |
Radians for $centerSphere: distance divided by the earth's radius, so 20 km is 20 / 6378.1.
whyWhy it matters
Store finders, delivery zones, ride hailing, asset tracking and geo-fenced alerts all reduce to these three or four operators. Getting units or coordinate order wrong silently returns the wrong stores, which is why interviewers like the topic.
Use caseReal-world scenario
A grocery app shows the five nearest stores within 10 km that are open and stock the item. $geoNear with query: { open: true, skus: sku }, maxDistance: 10000 and distanceField: "meters", then $limit: 5 and a $project rounding the distance for display.
Try itExamples
Run and edit these in the interactive lesson.
db.lab_stores.drop()
db.lab_stores.insertMany([
{ name: "Indiranagar", city: "Bengaluru", open: true, loc: { type: "Point", coordinates: [77.6408, 12.9784] } },
{ name: "Koramangala", city: "Bengaluru", open: true, loc: { type: "Point", coordinates: [77.6271, 12.9352] } },
{ name: "Whitefield", city: "Bengaluru", open: false, loc: { type: "Point", coordinates: [77.7500, 12.9698] } },
{ name: "Jayanagar", city: "Bengaluru", open: true, loc: { type: "Point", coordinates: [77.5838, 12.9250] } },
{ name: "Bandra", city: "Mumbai", open: true, loc: { type: "Point", coordinates: [72.8406, 19.0596] } }
])
db.lab_stores.createIndex({ loc: "2dsphere" })db.lab_stores.find({
open: true,
loc: { $near: { $geometry: { type: "Point", coordinates: [77.6066, 12.9756] }, $maxDistance: 8000 } }
}, { _id: 0, name: 1 })db.lab_stores.aggregate([
{ $geoNear: {
near: { type: "Point", coordinates: [77.6066, 12.9756] },
distanceField: "meters",
maxDistance: 20000,
query: { open: true }
} },
{ $project: { _id: 0, name: 1, km: { $round: [{ $divide: ["$meters", 1000] }, 1] } } }
])db.lab_stores.find(
{ loc: { $geoWithin: { $centerSphere: [[77.6066, 12.9756], 10 / 6378.1] } } },
{ _id: 0, name: 1 }
)db.lab_stores.find({ loc: { $geoWithin: { $geometry: { type: "Polygon", coordinates: [[
[77.58, 12.92], [77.66, 12.92], [77.66, 12.99], [77.58, 12.99], [77.58, 12.92]
]] } } } }, { _id: 0, name: 1 })db.lab_stores_noidx.drop()
db.lab_stores_noidx.insertOne({ loc: { type: "Point", coordinates: [77.6, 12.97] } })
db.lab_stores_noidx.find({ loc: { $near: { $geometry: { type: "Point", coordinates: [77.6, 12.97] } } } }).toArray()internalsUnder the hood
A 2dsphere index maps each geometry to cells of a hierarchical sphere grid (S2 cells) stored as B-tree keys. $near searches outward ring by ring until it has enough results, which is why it needs the index and returns results sorted by distance. $geoWithin can use the index to visit only covering cells, but works as a plain filter without one. Compound geo indexes such as { loc: "2dsphere", category: 1 } narrow results further.
In this simulator, distances use the haversine formula; real servers use S2 geometry, so values can differ slightly.
Avoid / preferDo this, not that
{ loc: { type: "Point", coordinates: [12.9716, 77.5946] } } // lat, lng{ loc: { type: "Point", coordinates: [77.5946, 12.9716] } } // lng, latGeoJSON is longitude first. Swapped coordinates put Bengaluru in the Indian Ocean and silently return wrong distances.
db.stores.find({ loc: { $near: { $geometry: here } } }) // no maxDistancedb.stores.find({ loc: { $near: { $geometry: here, $maxDistance: 5000 } } }).limit(10)Without a distance cap or limit, $near walks outward through the whole collection. Bound the search by distance and result count.
PitfallsCommon mistakes
- Storing [latitude, longitude] instead of [longitude, latitude].
- Passing kilometres to $centerSphere instead of radians.
- Putting $geoNear anywhere but the first pipeline stage.
- Expecting $geoWithin results to be sorted by distance.
LimitsWhen not to use it
MongoDB geo queries cover points, polygons, distance and containment well. Routing, travel time, map tiles and heavy spatial analytics belong in a dedicated GIS such as PostGIS or a routing service. Straight-line distance is not driving distance.
SecuritySecurity notes
Precise locations are personal data. Store only the precision you need, restrict who can read raw coordinates, and avoid returning exact distances to other users: repeated distance queries from different points can triangulate someone's position.
ChallengeTry it yourself
Create stores_lab with four Bengaluru stores and a 2dsphere index, then list stores within 5 km of MG Road (77.6101, 12.9716), nearest first, with distance in km.
Show one solution
db.stores_lab.drop()
db.stores_lab.insertMany([
{ name: "Indiranagar", loc: { type: "Point", coordinates: [77.6408, 12.9784] } },
{ name: "Koramangala", loc: { type: "Point", coordinates: [77.6245, 12.9352] } },
{ name: "Whitefield", loc: { type: "Point", coordinates: [77.7499, 12.9698] } },
{ name: "Jayanagar", loc: { type: "Point", coordinates: [77.5838, 12.9250] } }
])
db.stores_lab.createIndex({ loc: "2dsphere" })
db.stores_lab.aggregate([
{ $geoNear: { near: { type: "Point", coordinates: [77.6101, 12.9716] }, distanceField: "meters", maxDistance: 5000, spherical: true } },
{ $project: { _id: 0, name: 1, km: { $round: [{ $divide: ["$meters", 1000] }, 2] } } }
])InterviewQuestions you should be able to answer
Find the 5 nearest open stores within 10 km and show the distance. How?
Store loc as a GeoJSON Point with [lng, lat], create a 2dsphere index, and run an aggregation whose first stage is $geoNear with near, distanceField, maxDistance: 10000 (metres) and query: { open: true }, followed by $limit: 5. $near in find also sorts by distance but does not return the distance.
QuizCheck your understanding
4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.