MongoDB Geospatial Queries: 2dsphere Indexes, $near, $geoWithin, and $geoNear
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: proximity search
$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/$minDistanceare meters when querying GeoJSON points against a2dsphereindex.- Results are implicitly distance-sorted; adding
.sort()on another field overrides that and is usually a sign you want$geoNear(below) or$geoWithininstead. $nearis not allowed inside the$matchstage of an aggregation pipeline -- use$geoNearas the first stage instead.$nearSphereis equivalent to$nearfor 2dsphere indexes; it only differs for legacy2dindexes, 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:
$geoNearmust be the first stage in the pipeline. A preceding$matchis not allowed; put filters in itsqueryoption instead.- It requires a geospatial index. If the collection has more than one, name the field with the
keyoption. distanceFieldis required; the computed distance is in meters for GeoJSON input.- It replaces the old
geoNeardatabase 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.
$centerSpheretakes radians;$near/$geoNearwith GeoJSON take meters. - Missing index.
$nearand$geoNearerror 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.
$nearinside$match. Not supported; restructure with$geoNearas the first stage.- Big-polygon containment surprises. Polygons larger than a hemisphere need
crsurn:x-mongodb:crs:strictwinding:EPSG:4326 to be interpreted as intended.
Quick reference
| Goal | Tool | Index required |
|---|---|---|
| Nearest documents, sorted | $near / $nearSphere | yes |
| Within a polygon/circle | $geoWithin | no (recommended) |
| Geometries that overlap | $geoIntersects | no (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.
Skip the terminal. Use Mako.
Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.