MongoDB Geospatial Queries: 2dsphere Indexes, $near, $geoWithin, and $geoNear

6 min readMongoDB

MongoDB has first-class geospatial support: store coordinates as GeoJSON, index them with a 2dsphere index, and query by proximity, containment, or intersection. "Restaurants within 2km", "which delivery zone contains this address", "sort stores by distance" are all single queries, no PostGIS required.

The features are solid, but the failure modes are specific: coordinates in the wrong order, distances in the wrong unit, and $geoNear placement rules. This guide covers the storage format, the index, the three query operators, and the aggregation stage.

Storing locations: GeoJSON

The modern format is GeoJSON, an embedded document with a type and coordinates:

db.places.insertOne({
  name: "Cafe Centraal",
  location: {
    type: "Point",
    coordinates: [4.8952, 52.3702]   // [longitude, latitude] -- in that order
  }
})

Longitude first, latitude second. This is the #1 geospatial bug. GeoJSON (RFC 7946) uses [lng, lat], while nearly every consumer map UI (Google Maps, most GPS readouts) displays lat, lng. If your "Amsterdam" point ends up in Somalia, you swapped them. Valid ranges: longitude -180 to 180, latitude -90 to 90; MongoDB rejects out-of-range values at index time, but silently accepts swapped values that happen to be in range.

Besides Point, MongoDB supports the other GeoJSON types: LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, and GeometryCollection. Polygons must be closed (first coordinate equals last) and outer rings should be wound counter-clockwise.

There is also a legacy format, a bare [lng, lat] pair indexed with a 2d index. It calculates on a flat plane, not a sphere, and exists for backward compatibility. For anything new, use GeoJSON + 2dsphere.

The 2dsphere index

Geospatial query operators that search by proximity require an index; containment queries do not, but are slow scans without one.

db.places.createIndex({ location: "2dsphere" })

A 2dsphere index calculates geometry on a sphere (WGS84 ellipsoid), so distances are real-world meters. It can be part of a compound index:

// Filter by category, then by proximity
db.places.createIndex({ category: 1, location: "2dsphere" })

$near returns documents sorted nearest-to-farthest from a point. It requires a geospatial index on the field.

// Places within 2000 meters, nearest first
db.places.find({
  location: {
    $near: {
      $geometry: { type: "Point", coordinates: [4.8952, 52.3702] },
      $maxDistance: 2000,    // meters with GeoJSON + 2dsphere
      $minDistance: 100      // optional: exclude the immediate vicinity
    }
  }
})

Notes that save debugging time:

  • $maxDistance/$minDistance are meters when querying GeoJSON points against a 2dsphere index.
  • Results are implicitly distance-sorted; adding .sort() on another field overrides that and is usually a sign you want $geoNear (below) or $geoWithin instead.
  • $near is not allowed inside the $match stage of an aggregation pipeline -- use $geoNear as the first stage instead.
  • $nearSphere is equivalent to $near for 2dsphere indexes; it only differs for legacy 2d indexes, where it forces spherical math.

$geoWithin: containment

$geoWithin selects documents whose geometry lies entirely within a given shape. No index required (though one helps), no implicit sorting.

// Everything inside a delivery zone polygon
db.places.find({
  location: {
    $geoWithin: {
      $geometry: {
        type: "Polygon",
        coordinates: [[
          [4.85, 52.35], [4.95, 52.35], [4.95, 52.40], [4.85, 52.40], [4.85, 52.35]
        ]]
      }
    }
  }
})
 
// Everything within a 5km circle: $centerSphere takes RADIANS, not meters
db.places.find({
  location: {
    $geoWithin: {
      $centerSphere: [ [4.8952, 52.3702], 5 / 6378.1 ]  // radius_km / earth_radius_km
    }
  }
})

The $centerSphere radius is in radians: divide kilometers by 6378.1 (Earth's radius in km) or miles by 3963.2. Mixing this up with $maxDistance's meters is the #2 geospatial bug.

$geoIntersects: overlap

$geoIntersects selects documents whose geometry intersects (shares any point with) the given geometry. The classic use case is reverse containment, where the documents hold polygons:

// Which delivery zone contains this customer's location?
db.zones.find({
  area: {
    $geoIntersects: {
      $geometry: { type: "Point", coordinates: [4.8952, 52.3702] }
    }
  }
})

$geoWithin vs $geoIntersects: within requires full containment of the document geometry inside the query shape; intersects requires any overlap. For point documents they behave the same; the difference matters for lines and polygons.

$geoNear: proximity in aggregation pipelines

When you need distance as a value (display "1.2 km away", filter on it, group by it), use the $geoNear aggregation stage:

db.places.aggregate([
  {
    $geoNear: {
      near: { type: "Point", coordinates: [4.8952, 52.3702] },
      distanceField: "distance",        // required: where to write meters
      maxDistance: 2000,
      query: { category: "cafe" },      // extra filter, can use the compound index
      spherical: true
    }
  },
  { $limit: 10 },
  { $project: { name: 1, distanceKm: { $divide: ["$distance", 1000] } } }
])

Rules:

  • $geoNear must be the first stage in the pipeline. A preceding $match is not allowed; put filters in its query option instead.
  • It requires a geospatial index. If the collection has more than one, name the field with the key option.
  • distanceField is required; the computed distance is in meters for GeoJSON input.
  • It replaces the old geoNear database command, which was removed in MongoDB 4.2.

For the aggregation framework basics, see our aggregation pipeline guide.

Common mistakes

  • Swapped coordinates. GeoJSON is [lng, lat]. If results make no sense, check this first.
  • Radians vs meters. $centerSphere takes radians; $near/$geoNear with GeoJSON take meters.
  • Missing index. $near and $geoNear error without a geospatial index: error processing query ... unable to find index for $geoNear query.
  • Unclosed polygons. A query polygon's first and last coordinates must be identical.
  • $near inside $match. Not supported; restructure with $geoNear as the first stage.
  • Big-polygon containment surprises. Polygons larger than a hemisphere need crs urn:x-mongodb:crs:strictwinding:EPSG:4326 to be interpreted as intended.

Quick reference

GoalToolIndex required
Nearest documents, sorted$near / $nearSphereyes
Within a polygon/circle$geoWithinno (recommended)
Geometries that overlap$geoIntersectsno (recommended)
Distance as a field, in a pipeline$geoNear (first stage)yes

Mako connects to MongoDB with AI-powered autocomplete, which helps when you cannot remember whether this particular operator wants meters or radians. Try it free at mako.ai.

Mako — open source

Skip the terminal. Use Mako.

Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.