← back

Geo
Spatial

FILE  39_geospatial
TOPIC  GeoJSON · 2dsphere · $near · $geoWithin · $geoIntersects · $geoNear
LEVEL  Intermediate
01
GeoJSON Types
Standard format for storing geographic shapes in documents
GeoJSON

MongoDB supports the GeoJSON format for storing geographic coordinates and shapes. GeoJSON uses longitude first, latitude second — the opposite of most mapping UIs which show lat/lng. This is the most common source of bugs in geospatial code.

DANGER
GeoJSON coordinate order is [longitude, latitude] — NOT [lat, lng]. The WGS84 standard places longitude on the X axis. If you store [lat, lng] you will get wrong distances and all spatial queries will return incorrect results. New York is [-74.006, 40.7128] (lng, lat), not [40.7128, -74.006].
// GeoJSON Point — a single location
{
  type: "Point",
  coordinates: [-74.006, 40.7128]   // [longitude, latitude]
}

// GeoJSON LineString — an ordered sequence of positions (a path)
{
  type: "LineString",
  coordinates: [
    [-74.006,  40.7128],   // New York
    [-87.6298, 41.8781],   // Chicago
    [-118.243, 34.0522]    // Los Angeles
  ]
}

// GeoJSON Polygon — closed ring; first and last point must be identical
{
  type: "Polygon",
  coordinates: [[
    [-74.1,  40.6],
    [-73.9,  40.6],
    [-73.9,  40.8],
    [-74.1,  40.8],
    [-74.1,  40.6]    // closes the ring (= first point)
  ]]
}

// Polygon with a hole (outer ring + inner ring as hole)
{
  type: "Polygon",
  coordinates: [
    [[-74.1, 40.6], [-73.9, 40.6], [-73.9, 40.8], [-74.1, 40.8], [-74.1, 40.6]],  // outer
    [[-74.05, 40.65], [-73.95, 40.65], [-73.95, 40.75], [-74.05, 40.75], [-74.05, 40.65]]  // hole
  ]
}

// MultiPoint, MultiLineString, MultiPolygon — arrays of the above
// GeometryCollection — mixed types array

// Storing a place with GeoJSON location:
db.places.insertOne({
  name:     "Central Park",
  category: "park",
  location: {
    type:        "Point",
    coordinates: [-73.9654, 40.7829]   // [lng, lat]
  },
  borough: "Manhattan"
})
02
Geospatial Indexes
2dsphere for globe · 2d for flat plane
indexes
Index TypeUse ForCoordinate ModelSupports GeoJSON?
2dsphereEarth-surface coordinates (longitude/latitude)Sphere — accounts for Earth's curvatureYes (preferred)
2dFlat Euclidean plane (legacy, CAD, grid systems)Flat — no curvature correctionNo (legacy pairs only)
// Create 2dsphere index for GeoJSON Point field
db.places.createIndex({ location: "2dsphere" })

// Compound index — 2dsphere + other fields for combined queries
db.restaurants.createIndex({ location: "2dsphere", cuisine: 1 })
// Allows: find Italian restaurants within 2km of point

// Legacy 2d index (flat plane — avoid for real-world coordinates)
db.grid.createIndex({ position: "2d" })
// Legacy [lng, lat] pair (NOT GeoJSON):
db.grid.insertOne({ position: [-74.006, 40.7128] })

// A collection can have multiple 2dsphere indexes but only ONE 2d index
// 2dsphere indexes can be compound with other BSON types
// 2d indexes cannot be compound with 2dsphere indexes

// Verify index created correctly:
db.places.getIndexes()
// Should show: { "location": "2dsphere" } in key spec
NOTE
Use 2dsphere for all real-world geographic data. It correctly models Earth's spherical surface, computes accurate distances in meters, and supports all GeoJSON types. The legacy 2d index is only needed for flat-plane coordinate systems (floor plans, game maps, CAD drawings) or compatibility with very old MongoDB applications.
03
$near & $nearSphere
Find documents sorted by proximity to a point
proximity

$near returns documents in order of increasing distance from a point. Results are automatically sorted nearest-first. Requires a 2dsphere (or 2d) index on the field.

// Find places within 2km of a point (GeoJSON $near)
db.places.find({
  location: {
    $near: {
      $geometry: {
        type: "Point",
        coordinates: [-73.9857, 40.7484]   // Empire State Building [lng, lat]
      },
      $maxDistance: 2000,   // meters (2km)
      $minDistance: 100     // meters — exclude very close (optional)
    }
  }
})
// Results sorted by distance ascending (nearest first)
// Does NOT return the distance — use $geoNear aggregation for that

// Combine with additional filter (compound index recommended)
db.restaurants.find({
  location: {
    $near: {
      $geometry: { type: "Point", coordinates: [-73.9857, 40.7484] },
      $maxDistance: 1000
    }
  },
  cuisine: "Italian",    // additional filter — uses compound index
  rating: { $gte: 4.0 }
})

// $nearSphere — equivalent for 2d legacy indexes using spherical math
// For 2dsphere indexes, $near and $nearSphere are identical
db.places.find({
  location: {
    $nearSphere: {
      $geometry: { type: "Point", coordinates: [-73.9857, 40.7484] },
      $maxDistance: 2000
    }
  }
})
WARN
$near cannot be used with $or or in aggregation pipeline $match stages (use $geoNear aggregation stage instead). It also cannot be used with $text in the same query. For complex queries combining proximity with aggregation ($group, $project, etc.), always use the $geoNear aggregation stage.
04
$geoWithin
Find documents whose location falls inside a shape
containment

$geoWithin finds documents where the stored geometry is entirely within a given shape. Unlike $near, results are not sorted by distance and do not require an index (though an index greatly improves performance).

// Points within a GeoJSON Polygon (city boundary, delivery zone, etc.)
db.places.find({
  location: {
    $geoWithin: {
      $geometry: {
        type: "Polygon",
        coordinates: [[
          [-74.1,  40.6],
          [-73.7,  40.6],
          [-73.7,  40.9],
          [-74.1,  40.9],
          [-74.1,  40.6]    // closed ring
        ]]
      }
    }
  }
})

// $centerSphere — documents within a circular area on a sphere
// Radius in radians: km / 6378.1  (Earth radius)
db.places.find({
  location: {
    $geoWithin: {
      $centerSphere: [
        [-73.9857, 40.7484],  // center [lng, lat]
        5 / 6378.1            // 5km radius in radians
      ]
    }
  }
})

// $box — legacy flat-plane bounding box (2d index only)
// $center — legacy circle (2d index only)
db.grid.find({
  position: {
    $geoWithin: {
      $box: [[0, 0], [100, 100]]   // [bottom-left], [top-right]
    }
  }
})

// Real-world: find all delivery addresses within a delivery zone polygon
const deliveryZone = {
  type: "Polygon",
  coordinates: [[
    [-74.02, 40.70], [-73.95, 40.70],
    [-73.95, 40.75], [-74.02, 40.75],
    [-74.02, 40.70]
  ]]
}
db.orders.find({
  "deliveryAddress.location": {
    $geoWithin: { $geometry: deliveryZone }
  },
  status: "pending"
})
05
$geoIntersects
Find geometries that overlap with a given shape
intersection

$geoIntersects finds documents where the stored geometry intersects (touches or overlaps) the query shape. Essential for finding coverage areas, districts, polygons that overlap a search region.

// Find all delivery zones that cover a given point
// (the stored field is a Polygon, the query is a Point)
db.deliveryZones.find({
  area: {
    $geoIntersects: {
      $geometry: {
        type: "Point",
        coordinates: [-73.9857, 40.7484]   // does this point fall in any zone?
      }
    }
  }
})

// Find districts that overlap with a bounding box (e.g., map viewport)
db.districts.find({
  boundary: {
    $geoIntersects: {
      $geometry: {
        type: "Polygon",
        coordinates: [[
          [-74.1, 40.6], [-73.7, 40.6],
          [-73.7, 40.9], [-74.1, 40.9],
          [-74.1, 40.6]
        ]]
      }
    }
  }
})
// Returns districts that partially OR fully overlap the viewport

// Find routes (LineStrings) that pass through a neighborhood polygon
db.busRoutes.find({
  path: {
    $geoIntersects: {
      $geometry: {
        type: "Polygon",
        coordinates: [[
          [-73.99, 40.73], [-73.97, 40.73],
          [-73.97, 40.75], [-73.99, 40.75],
          [-73.99, 40.73]
        ]]
      }
    }
  }
})

$near vs $geoWithin vs $geoIntersects

OperatorQuestion AnsweredResults Sorted?Index Required?
$nearWhat's closest to this point?Yes — by distanceYes (2dsphere)
$geoWithinWhat's inside this shape?NoNo (but recommended)
$geoIntersectsWhat overlaps or touches this shape?NoNo (but recommended)
06
$geoNear Aggregation Stage
Proximity with distance field — required for aggregation pipelines
aggregation

$geoNear is the aggregation pipeline equivalent of $near, with additional power: it adds a computed distance field to each document and can be combined with other aggregation stages.

// $geoNear must be the FIRST stage in an aggregation pipeline
db.restaurants.aggregate([
  {
    $geoNear: {
      near:         { type: "Point", coordinates: [-73.9857, 40.7484] },
      distanceField: "distance",      // adds this field with meters to each doc
      maxDistance:   2000,            // meters
      spherical:     true,            // required for 2dsphere index
      query:         { cuisine: "Italian" }  // pre-filter (applied before distance)
    }
  },
  { $match: { rating: { $gte: 4.0 } } },       // filter after distance computed
  { $project: {
    name: 1,
    cuisine: 1,
    rating: 1,
    distanceMeters: "$distance",
    distanceKm: { $divide: ["$distance", 1000] }
  }},
  { $sort: { distance: 1 } },
  { $limit: 10 }
])

// distanceFactor: convert distance to different unit
// 1 = meters (default), 0.001 = km, 0.000621371 = miles
db.places.aggregate([
  {
    $geoNear: {
      near:          { type: "Point", coordinates: [-73.9857, 40.7484] },
      distanceField: "distanceMiles",
      distanceFactor: 0.000621371,    // output in miles
      maxDistance:   8046,            // 5 miles in meters
      spherical:     true
    }
  },
  { $group: {
    _id: "$category",
    count:       { $sum: 1 },
    nearestItem: { $first: "$name" },
    avgDistMi:   { $avg: "$distanceMiles" }
  }},
  { $sort: { avgDistMi: 1 } }
])
NOTE
If the collection has multiple 2dsphere indexes, specify key: "fieldName" in $geoNear to indicate which index to use. Without key, MongoDB will error when multiple geospatial indexes exist.
07
Practical Examples
Real-world geospatial query patterns
examples
// ── "Find nearby drivers" (ride-sharing) ──────────────────────────────────
db.drivers.createIndex({ location: "2dsphere", status: 1 })

db.drivers.aggregate([
  {
    $geoNear: {
      near:          { type: "Point", coordinates: [userLng, userLat] },
      distanceField: "distanceMeters",
      maxDistance:   5000,
      query:         { status: "available" },
      spherical:     true
    }
  },
  { $limit: 5 },
  { $project: { driverName: 1, vehicleType: 1, distanceMeters: 1, rating: 1 } }
])

// ── "Is this delivery address within our zone?" ────────────────────────────
db.deliveryZones.insertOne({
  name: "Zone A",
  coverageArea: {
    type: "Polygon",
    coordinates: [[
      [-74.02, 40.70], [-73.95, 40.70],
      [-73.95, 40.75], [-74.02, 40.75],
      [-74.02, 40.70]
    ]]
  },
  deliveryFee: 2.99
})
db.deliveryZones.createIndex({ coverageArea: "2dsphere" })

const userLocation = { type: "Point", coordinates: [userLng, userLat] }
const zone = db.deliveryZones.findOne({
  coverageArea: { $geoIntersects: { $geometry: userLocation } }
})
if (!zone) throw new Error("Delivery not available at your location")

// ── "Find all restaurants, grouped by distance bucket" ────────────────────
db.restaurants.aggregate([
  {
    $geoNear: {
      near:          { type: "Point", coordinates: [-73.9857, 40.7484] },
      distanceField: "dist",
      maxDistance:   3000,
      spherical:     true
    }
  },
  {
    $bucket: {
      groupBy: "$dist",
      boundaries: [0, 500, 1000, 2000, 3000],
      default:  "3000+",
      output:   { count: { $sum: 1 }, restaurants: { $push: "$name" } }
    }
  }
])
TIP
For real-time location tracking (drivers, delivery, assets), store the current location as GeoJSON Point, add a 2dsphere index, and update the document with each location ping using $set: { location: newPoint, updatedAt: new Date() }. Use a TTL index on updatedAt to auto-expire stale location records.