Cette article a été co-écrit avec Nicolas Géraud et fait partie de la série d’articles MongoDB – Faisons les présentations.

Dans cet article nous aborderons le paradigme Map/Reduce et comment celui-ci peut-être appliqué avec MongoDB.

MapReduce, c’est quoi cette bête ?

Comme son nom le suggère, l’algorithme MapReduce implique deux opérations :

  • Diviser les données à traiter en partitions indépendantes, qui seront en traitées en parallèle (Map)
  • Combiner l’ensemble des résultats obtenus (Reduce)

Il permet donc de répartir la charge sur un grand nombre de serveurs tout en gérant entièrement le cluster et la répartition de la charge.

Jouons un peu avec MapReduce

Description du schéma de données

Le modèle de données est issu d’un groupe de travail dédié à la comparaison des frameworks webs. Ainsi, pour effectuer cette comparaison, un modèle de données simple a été établi. Le périmètre fonctionnel se limite à la gestion des demandes de congés des consultants d’une SSII (les vacances estivales étant terminées, il faut bien se donner les moyens de réver un peu !).

Ce modèle, issu du projet YouGO, a été légèrement transformé avant d’être intégré dans une base MongoDB. Vous trouverez un dump de cette base sur le dépôt GoogleCode d’Ineat.

Dans le cadre de notre exemple, nous serons amenés à manipuler deux types de documents principaux :

  • les utilisateurs (ou collaborateurs), stockés dans une collection « users »
  • les demandes de congés, stockées dans une collection « requests »

Restauration du dump

Pour restaurer le dump, le mieux est de consulter la documentation succincte mais globalement suffisante sur le site de MongoDB.

Toutefois, comme vous avez hâte de jouer avec MapReduce, voici un exemple de commande de restauration (à adapter en fonction des situations) :

mongorestore --db yougo --drop --host localhost /tmp/dump/dump-yougo-mongodb.bson

Grouper les requêtes par collaborateur

Nous allons appliquer MapReduce afin de grouper les requêtes par utilisateur ainsi que les comptabiliser car notre manager souhaite vérifier qu’aucun collaborateur ne prenne plus de congés qu’il n’en posséde.

Dans ce cas précis, la clé sur laquelle les valeurs seront mappées correspond uniquement au login associé à la demande de congés.

La fonction map correspondante est donc la suivante :

map =
	function() {
		emit(this.login, {request: this, count: 1});
	};

La fonction reduce, quand à elle, est destinée à comptabiliser le nombre de demandes par collaborateur et de les grouper :

reduce =
	function(key, values) {
		var result = {count: 0, requests: []};

		print("Key : " + key.login);

		values.forEach(function(val) {
			result.count += val.count;
			result.requests.push(val.request);
		});

		return result;
	};

Vous pourrez noter l’utilisation de l’appel à la fonction print() permettant de logguer l’état des variables.
Attention toutefois, ces logs seront disponibles du côté serveur (soit dans la console, soit dans un fichier de log) et non pas côté client !

Dernière étape, l’appel à la fonction mapReduce fournie par MongoDB en spécifiant la collection sur laquelle la fonction doit-être exécutée :

db.requests.mapReduce(map, reduce, {
	out: "requests_by_user"
});

Une fois ces commandes réalisées, lancez un petit db.requests_by_user.find() pour jeter un oeil aux résultats générés.

Grouper les requêtes par collaborateur et par statut de demande

Pour ce second cas d’utilisation, nous souhaitons pouvoir grouper les demandes par collaborateur comme vu ci-dessus mais également en fonction du statut de celles-ci. Il suffit de définir une structure de type Map en tant que clé de la fonction emit() appelée comme ceci :

map =
	function() {
		emit({login: this.login, status: this.status}, {request: this, count: 1});
};

La fonction reduce, quand à elle, n’a pas à effectuer de traitement particulier et reste donc inchangée (seule la fonction print() affiche désormais le statut de la demande) :

reduce =
	function(key, values) {
		var result = {count: 0, requests: []};

		print("Key : " + key.login + " - " + key.status);

		values.forEach(function(val) {
			result.count += val.count;
			result.requests.push(val.request);
		});

		return result;
	};

La fonction mapReduce reste inchangée également (voir ci-dessus).

Limiter les résultats aux demandes de congés acceptées

Finalement, notre manager insiste en voulant récupérer le listing des demandes de congés par collaborateur qui ont été acceptées.

Pour ce faire, il est nécessaire d’appliquer un filtre sur les données en appliquant une « query » dans la fonction mapReduce().

L’exemple précédent devient donc :

db.requests.mapReduce(map, reduce, {
	out: "accepted_requests_by_user",
	query: {status: 'ACCEPTED'}
});

N’hésitez pas à analyser les résultats obtenus en exécutant la commande db.accepted_requests_by_user.find() !

MapReduce : suite et fin

La fonction mapReduce() fournie par MongoDB propose de nombreuses combinaisons à travers les options disponibles. Parmi elles, nous pouvons citer les suivantes :

  • Appliquer une fonction finalize(), non obligatoire, qui sera appliquée après la réduction : à la différence de reduce(), celle-ci ne sera appelée qu’une seule et unique fois.
  • Par défaut, la fonction mapReduce() effectue un remplacement (replace) de la collection cible. Vous pouvez également appliquer un merge afin d’overrider les résultats pour les clés similaires. Enfin, il est possible de lancer la fonction en mode inline. Dans ce cas, aucune collection n’est créée et le résultat est stocké dans un objet en RAM (attention donc à la limite de la taille d’un document !). Vous trouverez plus d’informations sur cette page.
  • Utiliser des query filter plus évoluées (au même titre que les query définies pour effectuer des sélections).

Group vs MapReduce

Dans les faits, les deux fonctions sont issues d’un même besoin : la possibilité de grouper / d’agréger les documents sur une ou plusieurs colonnes données.

Cependant, les différences entre les deux fonctions sont notables ! MapReduce doit-être privilégiée notamment pour des questions de flexibilité et de scalabilité. Mais son utilisation à un prix : celui de la performance. Si la collection en entrée se trouve sur plusieurs shards1, alors MongoDB dispatchera automatiquement le mapReduce sur chacun de ces shards afin de les exécuter en parallèle. Aucune option particulière n’est requise…

De manière générale, la fonction group est sensiblement plus rapide que son homologue mapReduce. Comme vu ci-dessus, il est conseillé d’appeler la fonction mapReduce en lieu et place de la fonction group dans le cadre d’une configuration MongoDB sharded.

Enfin, l’utilisation de group est préconisée dans le cas de collections contenant peu de données et qui serait amenée à générer un sous-ensemble de données de moins de 10 000 clés. Dans le cas inverse, une exception peut-être soulevée… il faudra, dans ce cas, privilégier mapReduce.

Retrouvez les deux auteurs sur Twitter pour discuter de ces articles : Nicolas / David
  1. En attendant notre prochain article sur les shards et les replica sets, vous pouvez vous imaginer qu’un shard correspond en quelques sorte à un noeud dans un cluster []