Using GeoJSON

GeoJSON is a format for storing geographic points and polygons. MongoDB has excellent support for geospatial queries on GeoJSON objects. Let's take a look at how you can use Mongoose to store and query GeoJSON objects.

Point Schema

The most simple structure in GeoJSON is a point. Below is an example point representing the approximate location of San Francisco. Note that longitude comes first in a GeoJSON coordinate array, not latitude.

{
  "type" : "Point",
  "coordinates" : [
    -122.5,
    37.7
  ]
}

Below is an example of a Mongoose schema where location is a point.

const citySchema = new mongoose.Schema({
  name: String,
  location: {
    type: {
      type: String, // Don't do `{ location: { type: String } }`
      enum: ['Point'], // 'location.type' must be 'Point'
      required: true
    },
    coordinates: {
      type: [Number],
      required: true
    }
  }
});

Using subdocuments, you can define a common pointSchema and reuse it everywhere you want to store a GeoJSON point.

const pointSchema = new mongoose.Schema({
  type: {
    type: String,
    enum: ['Point'],
    required: true
  },
  coordinates: {
    type: [Number],
    required: true
  }
});

const citySchema = new mongoose.Schema({
  name: String,
  location: {
    type: pointSchema,
    required: true
  }
});

Polygon Schema

GeoJSON polygons let you define an arbitrary shape on a map. For example, the below polygon is a GeoJSON rectangle that approximates the border of the state of Colorado.

{
  "type": "Polygon",
  "coordinates": [[
    [-109, 41],
    [-102, 41],
    [-102, 37],
    [-109, 37],
    [-109, 41]
  ]]
}

Polygons are tricky because they use triple nested arrays. Below is how you create a Mongoose schema where coordinates is a triple nested array of numbers.

const polygonSchema = new mongoose.Schema({
  type: {
    type: String,
    enum: ['Polygon'],
    required: true
  },
  coordinates: {
    type: [[[Number]]], // Array of arrays of arrays of numbers
    required: true
  }
});

const citySchema = new mongoose.Schema({
  name: String,
  location: polygonSchema
});

Geospatial Queries with Mongoose

Mongoose queries support the same geospatial query operators that the MongoDB driver does. For example, the below script saves a city document those location property is a GeoJSON point representing the city of Denver, Colorado. It then queries for all documents within a polygon representing the state of Colorado using the MongoDB $geoWithin operator.

Colorado GeoJSON Polygon
const City = db.model('City', new Schema({
  name: String,
  location: pointSchema
}));

const colorado = {
  type: 'Polygon',
  coordinates: [[
    [-109, 41],
    [-102, 41],
    [-102, 37],
    [-109, 37],
    [-109, 41]
  ]]
};
const denver = { type: 'Point', coordinates: [-104.9903, 39.7392] };
return City.create({ name: 'Denver', location: denver }).
  then(() => City.findOne({
    location: {
      $geoWithin: {
        $geometry: colorado
      }
    }
  })).
  then(doc => assert.equal(doc.name, 'Denver'));

Mongoose also has a within() helper that's a shorthand for $geoWithin.

const denver = { type: 'Point', coordinates: [-104.9903, 39.7392] };
return City.create({ name: 'Denver', location: denver }).
  then(() => City.findOne().where('location').within(colorado)).
  then(doc => assert.equal(doc.name, 'Denver'));

Geospatial Indexes

MongoDB supports 2dsphere indexes for speeding up geospatial queries. Here's how you can define a 2dsphere index on a GeoJSON point:

const denver = { type: 'Point', coordinates: [-104.9903, 39.7392] };
const City = db.model('City', new Schema({
  name: String,
  location: {
    type: pointSchema,
    index: '2dsphere' // Create a special 2dsphere index on `City.location`
  }
}));

return City.create({ name: 'Denver', location: denver }).
  then(() => City.findOne().where('location').within(colorado)).
  then(doc => assert.equal(doc.name, 'Denver'));

You can also define a geospatial index using the Schema#index() function as shown below.

citySchema.index({ location: '2dsphere' });

MongoDB's $near query operator and $geoNear aggregation stage require a 2dsphere index.