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
// 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]
]]
}[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
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" })$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
// 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
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 }
]$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
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?
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)
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
$maxDistancewith$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
$geoNearso 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" }.