Pour certains projets nous avons besoin d’effectuer des requêtes spécifiques comme par exemple trouver les éléments les plus proches ou se situant dans périmètre d’un point donné. Figurez-vous que cela est possible avec MongoDB et ce sans utiliser une API externe, grâce à $geoNear. Nous allons voir comment le mettre en place sur notre projet.

Mise en place du projet

Pour mieux comprendre l’utilisation de $geoNear nous allons créer un projet avec Node.js et MongoDB. Le but du projet sera de lister tous les magasins les plus proches d’un point donné, dans notre cas ça sera notre localisation.

Initialisation du projet avec Node.js et MongoDB

Je vais dans cet exemple créer un projet d’API Node.js avec express. Je vais utiliser mongoose pour la création de mes schémas et mes models sous MongoDB.

Pour faire plus simple voici l’architecture qu’aura le projet:

project structure api

Comme vous le remarquez, nous devons créer un fichier Store.js qui nous servira de modèle, StoreController.js le contrôleur qui permettra de récupérer les données et de les afficher et enfin un fichier stores.js cette fois-ci dans nos routes qui servira à traiter les requêtes et appeler le bon contrôleur.

Comme convention pour ma part, je préfère tout nommer en anglais. Le fichier de modèle est en majuscule et au singulier. Le contrôleur est en Pascal case où le nom a pour suffixe Controller. Pour les routes, c’est en minuscule et au pluriel.

Création du modèle store

Pour implémenter $geoNear, nous devons d’abord renseigner un index géo-spatial (de type 2dsphere pour des coordonnées à la surface de la terre) sur le champ visé. Dans notre cas ça sera sur address et particulièrement sur le sous-champs location. Donc dans notre schéma on renseignera :

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

Voici le code complet de notre modèle. J’en profite par la même occasion pour créer des méthodes statiques au model permettant la récupération des entrées. Dans cet exemple j’ai crée findAll() et findByCoordinates().

Pour en savoir plus sur les methods et les statics, voici le lien de la doc:
https://mongoosejs.com/docs/2.7.x/docs/methods-statics.html

Disponible sur https://gist.github.com/itsabdessalam/d614e43618ec08ebd418666e5767ec66#file-store-js

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

// On définit le schéma du Store
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 }
});

// On crée l'index pour $geoNear
StoreSchema.index({ "address.location": "2dsphere" });

// On définit des méthodes statics propres au schéma
// On récupère tous les stores
StoreSchema.statics.findAll = function() {
	return this.find({});
};

// On récupère seulement les stores
// les plus proches
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);

Insertion des données

Pour insérer un magasin nous devons spécifier les champs tels qu’ils sont dans le modèle renseigné, à savoir le nom et l’adresse. L’adresse est un objet contenant tous les sous champs la constituant mais je mets en évidence le sous champs location. Celui-ci est de type String qui est par défaut un Point et on autorise que le Point vu qu’on a renseigné un enum avec une seule valeur possible lui correspondant. Il contiendra un tableau de coordonnées de type Number, qui aura pour valeur une longitude et une latitude, dans cet ordre précis.

Voici un exemple d’insertion avec l’API:

insert store api mongo

J’ai choisi comme point de localisation cette adresse: 6 rue Saint-Denis 75001, Paris.

Voici une capture de carte listant les magasins aux alentours:

maps nearest stores

Pour notre exemple j’ai choisi d’insérer les magasins suivants:

  • Courir: 51 rue de Rivoli 75001, Paris (environ 85 mètres)
  • Celio Club: 49 rue de Rivoli 75001, Paris (environ 75 mètres)
  • Damart: 3 rue Saint-Denis 75001, Paris (environ 40 mètres)

Récupération des données

Nous allons récupérer tous les magasins se situant dans un périmètre maximal de 100 mètres de notre localisation. Pour cela nous devons dans notre requête utiliser $geoNear avec un pipeline d’agrégation (recherche groupée), où nous pourrons renseigner les coordonnées de notre localisation actuelle ainsi que le périmètre maximal. Nous aurons en sortie en plus des champs des magasins qui satisfont la condition, la distance calculée entre les deux points, puisque nous avons renseigné distanceField.

Disponible sur 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
			}
		}
	]);
};

Nous nous attendons à bien avoir les 3 magasins étant donné qu’ils se situent bien dans ce périmètre.

results api geonear

Maintenant je vais réduire la distance maximale à seulement 50 mètres

results api geonear

🚀 Très bien ! Notre API marche comme il faut et nous avons seulement les magasins les plus proches.

Ce projet est disponible sur GitHub à cette adresse:
https://github.com/itsabdessalam/geonear-api-mongo

Avant de partir…
Merci pour votre lecture ! 😊