MongoDB Mastery
MongoDB Mastery lessons / Module 16: Index Types & Options

Geospatial queries: 2dsphere, $near, $geoWithin and $geoNear

Level 3: Advanced22 min

Store locations as GeoJSON, index them with 2dsphere, and answer nearest-store, within-radius and inside-polygon questions with correct units.

This lesson is interactive. Run every example against an in-browser MongoDB, see explain() plans drawn as trees, and take the quiz.
Open the interactive lesson
You will learn to
  • 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.

QuestionOperatorIndexUnits
Nearest first, optionally within a distance$near / $nearSphere in findRequiredmetres with GeoJSON
Everything within a radius or polygon (unsorted)$geoWithin with $centerSphere or $geometryOptional (faster with)radians for $centerSphere
Nearest with the distance returned, plus more pipeline stages$geoNear aggregation stage (must be first)Requiredmetres with GeoJSON
Shapes that touch$geoIntersectsOptional-

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.

Stores with a 2dsphere index
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" })
Nearest open stores within 8 km of MG Road
db.lab_stores.find({
  open: true,
  loc: { $near: { $geometry: { type: "Point", coordinates: [77.6066, 12.9756] }, $maxDistance: 8000 } }
}, { _id: 0, name: 1 })
Distances with $geoNear
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] } } }
])
Within a 10 km radius ($centerSphere uses radians)
db.lab_stores.find(
  { loc: { $geoWithin: { $centerSphere: [[77.6066, 12.9756], 10 / 6378.1] } } },
  { _id: 0, name: 1 }
)
Inside a delivery polygon
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 })
Polygon rings must be closed: the last point repeats the first.
$near without a geo index fails
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

Avoid
{ loc: { type: "Point", coordinates: [12.9716, 77.5946] } }   // lat, lng
Prefer
{ loc: { type: "Point", coordinates: [77.5946, 12.9716] } }   // lng, lat

GeoJSON is longitude first. Swapped coordinates put Bengaluru in the Indian Ocean and silently return wrong distances.

Avoid
db.stores.find({ loc: { $near: { $geometry: here } } })   // no maxDistance
Prefer
db.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
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.