Using $geoNear for proximity calculation with MongoDB

For some projects we need to perform specific queries such as finding the closest elements or those within the perimeter of a given point. Imagine that this is possible with MongoDB and this without using an external API, thanks to $geoNear. We will see how to implement it on our project.

Setting up the project

To better understand the use of $geoNear we will create a project with Node.js and MongoDB. The goal of the project will be to list all the stores closest to a given point, in our case it will be our localization.

Initialising the project with Node.js and MongoDB

In this example I will create a Node.js API project with express. I will use mongoose for the creation of my schemas and models in MongoDB.

To make it simpler here is the architecture that the project will have:

project structure api

As you can see, we need to create a Store.js file that will serve as a template, StoreController.js the controller that will allow us to retrieve the data and display it and finally a stores.js file this time in our routes that will be used to process the requests and call the right controller.

As a convention for my part,the template file is in uppercase and singular. The controller is in Pascal case where the name has the suffix Controller. For routes, it is in lower case and plural.

Creation of the store model

To implement $geoNear, we must first fill in a geo-spatial index (2dsphere type for coordinates on the earth’s surface) on the target field. In our case it will be on address and especially on the location subfield. So in our schema we will fill in :

StoreSchema.index({ "address.location": "2dsphere" });

Here is the complete code of our model. At the same time I added static methods to the model to retrieve data. In this example I created findAll() and findByCoordinates().

To learn more about methods and statics, here is the link to the doc:
https://mongoosejs.com/docs/2.7.x/docs/methods-statics.html

Available at https://gist.github.com/itsabdessalam/d614e43618ec08ebd418666e5767ec66#file-store-js

const mongoose = require("mongoose");
const Schema = mongoose.Schema;

// Define the Store schema
const StoreSchema = new Schema({
  name: {
    type: String,
    required: true
  },
  address: {
    street: String,
    number: Number,
    city: {
      type: String,
      required: true
    },
    zipCode: {
      type: String,
      required: true
    },
    location: {
      type: {
        type: String,
        enum: ["Point"],
        default: "Point"
      },
      coordinates: {
        type: [Number],
        default: [0, 0]
      }
    }
  },
  createdOn: {
    type: Date,
    default: Date.now
  },
  updatedOn: {
    type: Date,
    default: Date.now
  }
});

// We create the index for `$geoNear`
StoreSchema.index({
  "address.location": "2dsphere"
});

// We define static methods
StoreSchema.statics.findAll = function() {
  return this.find({});
};

// Only the closest stores are retrieved
StoreSchema.statics.findByCoordinates = function(coordinates, maxDistance) {
  return this.aggregate([{
    $geoNear: {
      near: {
        type: "Point",
        coordinates: coordinates
      },
      maxDistance: maxDistance,
      distanceField: "dist.calculated",
      spherical: true
    }
  }]);
};

module.exports = mongoose.model("Store", StoreSchema);

Insert data

To insert a store we need to specify the fields as they are in the template filled in, i.e. name and address. The address is an object containing all the subfields that make it up but I highlight the location subfields. This one is of type String which is by default a Point and we allow only Point since we filled in an enum with only one possible value corresponding to it. It will contain an array of coordinates of type Number, which will have a longitude and a latitude value, in this precise order.

Here is an example of insertion with the API:

insert store api mongo

I have chosen this address as my location: 6 rue Saint-Denis 75001, Paris.

Here is a map capture listing the stores in the area:

maps nearest stores

For our example I have chosen to insert the following stores:

  • Courir: 51 rue de Rivoli 75001, Paris (about 85 meters)
  • Celio Club: 49 rue de Rivoli 75001, Paris (about 75 meters)
  • Damart: 3 rue Saint-Denis 75001, Paris (about 40 meters)

Querying data

We will retrieve all the stores within a maximum perimeter of 100 meters from our location. To do this we must use $geoNear in our query with an aggregation pipeline (grouped search), where we will be able to fill in the coordinates of our current location as well as the maximum perimeter. In addition to the fields of the stores that satisfy the condition, we will have the calculated distance between the two points, since we have filled in distanceField.

Available at https://gist.github.com/itsabdessalam/10c916f8089673e18ed27c5cdae0599d#file-geonear-function-js

StoreSchema.statics.findByCoordinates = function(coordinates, maxDistance) {
  return this.aggregate([{
    $geoNear: {
      near: {
        type: "Point",
        coordinates: coordinates
      },
      maxDistance: maxDistance,
      distanceField: "dist.calculated",
      spherical: true
    }
  }]);
};

We expect to have all 3 stores as they are well within this perimeter.

results api geonear

Now I will reduce the maximum distance to only 50 meters.

results api geonear

🚀 Well! Our API is working fine and we only have the closest stores.

This project is available on GitHub: https://github.com/itsabdessalam/geonear-api-mongo

Before you leave…
Thanks for reading! 😊