MongoDBGeospatial Indexes

Geospatial Indexes

MongoDB has first-class support for location data. With a 2dsphere index you can answer questions like "which stores are within 5 km of the user?", "is this point inside that delivery zone?", or "which routes cross this neighborhood?" — all with a single query.

GeoJSON: How Location Data Is Stored

Modern geospatial queries work on GeoJSON objects. A GeoJSON value has a type and coordinates — and the coordinate order is always [longitude, latitude] (the reverse of what most map apps show you).

GeoJSON Point and Polygon

JS
// A Point: a single location
{
  type: "Point",
  coordinates: [-73.9857, 40.7484]   // [lng, lat] — Empire State Building
}

// A Polygon: an area. The first and last coordinate must be identical
// to close the ring.
{
  type: "Polygon",
  coordinates: [[
    [-74.0, 40.75],
    [-73.98, 40.75],
    [-73.98, 40.73],
    [-74.0, 40.73],
    [-74.0, 40.75]
  ]]
}
Warning
Coordinates are [longitude, latitude], not lat/lng. Getting this backwards is the single most common geospatial bug — your points end up in the ocean and queries silently return nothing nearby.

GeoJSON also supports LineString, MultiPoint, MultiPolygon, and GeometryCollection for more complex shapes such as roads and multi-part regions.

Creating a 2dsphere Index

2dsphere index

JS
db.stores.insertMany([
  {
    name: "Downtown Coffee",
    location: { type: "Point", coordinates: [-73.9857, 40.7484] }
  },
  {
    name: "Uptown Books",
    location: { type: "Point", coordinates: [-73.9632, 40.7794] }
  },
  {
    name: "Brooklyn Bagels",
    location: { type: "Point", coordinates: [-73.9442, 40.6782] }
  }
])

// The index that makes geo queries possible
db.stores.createIndex({ location: "2dsphere" })
Note
A 2dsphere index calculates on an earth-like sphere, so distances are real-world meters. Queries such as $near on GeoJSON points require this index — without it they error out.
$near — Find the Closest Documents

$near returns documents sorted from nearest to farthest, with optional distance bounds in meters:

Store locator query

JS
// Stores within 3 km of the user, closest first
db.stores.find({
  location: {
    $near: {
      $geometry: { type: "Point", coordinates: [-73.98, 40.75] },
      $maxDistance: 3000,   // meters
      $minDistance: 0
    }
  }
})
[
  { name: 'Downtown Coffee', location: { type: 'Point', coordinates: [ -73.9857, 40.7484 ] } }
]

$nearSphere behaves identically for GeoJSON data — the difference only matters for legacy coordinate pairs, where $nearSphere forces spherical math. To also get the computed distance back, use the aggregation stage $geoNear:

$geoNear with distances

JS
db.stores.aggregate([
  {
    $geoNear: {
      near: { type: "Point", coordinates: [-73.98, 40.75] },
      distanceField: "distanceMeters",
      maxDistance: 5000,
      spherical: true
    }
  },
  { $project: { _id: 0, name: 1, distanceMeters: { $round: ["$distanceMeters", 0] } } }
])
[
  { name: 'Downtown Coffee', distanceMeters: 523 },
  { name: 'Uptown Books', distanceMeters: 3541 }
]
Tip
$geoNear must be the first stage of the pipeline, and the collection needs exactly one geospatial index for the queried field (or a key option naming which one to use).
$geoWithin — Documents Inside an Area

$geoWithin finds documents whose geometry lies completely inside a given shape — ideal for "which stores are in this delivery zone?" Unlike $near, it does not sort by distance and does not strictly require an index (though one makes it fast).

Points inside a polygon

JS
db.stores.find({
  location: {
    $geoWithin: {
      $geometry: {
        type: "Polygon",
        coordinates: [[
          [-74.02, 40.77],
          [-73.94, 40.77],
          [-73.94, 40.72],
          [-74.02, 40.72],
          [-74.02, 40.77]
        ]]
      }
    }
  }
})

// Or within a circle: center + radius in radians
// (divide meters by earth radius 6378137)
db.stores.find({
  location: {
    $geoWithin: {
      $centerSphere: [[-73.98, 40.75], 3000 / 6378137]
    }
  }
})
$geoIntersects — Geometries That Touch

$geoIntersects matches documents whose geometry shares any point with the query geometry — a route crossing a district, a zone overlapping another zone. Commonly used the other way around too: store polygons (delivery zones) and ask which zone contains a customer point.

Which delivery zone covers this address?

JS
db.zones.insertOne({
  name: "Zone A",
  area: {
    type: "Polygon",
    coordinates: [[
      [-74.02, 40.77], [-73.94, 40.77],
      [-73.94, 40.72], [-74.02, 40.72],
      [-74.02, 40.77]
    ]]
  }
})
db.zones.createIndex({ area: "2dsphere" })

// The customer's point intersects which zone polygon?
db.zones.find({
  area: {
    $geoIntersects: {
      $geometry: { type: "Point", coordinates: [-73.98, 40.75] }
    }
  }
})
[ { name: 'Zone A', area: { type: 'Polygon', ... } } ]
Operator Summary

Operator

Returns

Sorted by distance?

Index required?

$near / $nearSphere

Documents near a point, within optional bounds

Yes

Yes (2dsphere)

$geoNear (aggregation)

Near matches plus a computed distance field

Yes

Yes

$geoWithin

Geometry fully inside a shape

No

No (but recommended)

$geoIntersects

Geometry sharing any point with a shape

No

No (but recommended)

Legacy 2d Indexes

Before GeoJSON, MongoDB stored locations as plain [x, y] coordinate pairs indexed with a 2d index, which computes on a flat plane. It still exists for backward compatibility, but for real-world coordinates you should always use GeoJSON with a 2dsphere index — flat-plane math becomes noticeably wrong over city-scale distances and near the poles.

Legacy 2d (avoid for new apps)

JS
db.places.createIndex({ loc: "2d" })
db.places.find({ loc: { $near: [-73.98, 40.75] } })
Best Practices
  • Always store coordinates as GeoJSON in [longitude, latitude] order.

  • Use $maxDistance with $near — unbounded near queries scan far more index entries than you need.

  • Validate polygons: rings must be closed (first point equals last point) and must not self-intersect, or inserts into an indexed field fail.

  • For "distance shown in the UI" use $geoNear so the server computes it once.

  • Compound usage: filter fields can be combined, e.g. db.stores.find({ category: "coffee", location: { $near: ... } }), and 2dsphere indexes can be part of a compound index like { category: 1, location: "2dsphere" }.