TP MongoDB: prise en main et pipeline d’agrégation

L’objectif de ce TP est de mettre en pratique le requêtage via une algèbre en utilisant MongoDB.

Dans une première partie, on prendra en main le shell MongoDB ainsi que les requêtes de base. Dans une seconde partie, on prendra en main quelques opérateurs d’aggrégation. Dans la troisième partie on combinera les opérations d’aggrégation dans des pipelines plus complexes.

Pensez à rédiger un compte-rendu au format Markdown dans lequel vous indiquerez vos remarques ainsi que les différentes requêtes que vous aurez exécuté.

1. Connexion

Pour faire le TP, trois choix possibles: utiliser le serveur mongodb commun, installer mongodb sur votre propre machine via des packages ou bien démarrer un serveur MongoDB via docker. Dans les deux dernier cas, il faudra importer les données contenues dans le fichier mif04-mongodb.zip à télécharger.

Pour effectuer les requêtes, on pourra utiliser le shell mongosh, le shell mongo ou bien MongoDB Compass.

1.1 Connexion au serveur commun

Un serveur MongoDB est disponible sur bd-pedago.univ-lyon1.fr. Ce serveur n’est disponible que depuis le réseau du campus: depuis les machine de TP ou depuis le Wifi Eduroam. Il faut donc activer le VPN pour y avoir accès depuis l’extérieur du campus ou depuis des réseaux Wifi comme Eduspot ou UCBL-Portail.

La base à laquelle on peut se connecter pour le TP est mif04 avec le compte mif04. Le mot de passe sera indiqué via tomuss.

On pourra adapter la commande suivante pour se connecter:

mongosh -u mif04 -p "remplacez-moi" mongodb://bd-pedago.univ-lyon1.fr/mif04

Changer remplacez-moi par le mot de passe qui aura été communiqué

Changer mongosh par mongo si la première commande n’est pas disponible. Si mongo n’est pas non plus disponible, télécharger mongosh-1.1.7-linux-x64.tgz (utiliser /tmp au besoin car l’archive fait environ 60 Mo)

Pour MongoDB Compass, utiliser la chaîne de connexion suivante, en remplaçant remplacez-moi par le mot de passe:

mongodb://mif04:remplacez-moi@bd-pedago.univ-lyon1.fr:27017/mif04?authSource=mif04&readPreference=primary&appname=MongoDB%20Compass&directConnection=true&ssl=false

1.2 Installation du serveur via un système de packages

Installer MongoDB Community Edition via un système de packages, voir la documentation d’installation.

Importer ensuite les données via la commande suivante (à exécuter dans le répertoire où le zip a été extrait):

for i in *.json; do mongoimport -c $(basename $i .json) --file=$i --type=json --jsonArray; done

On peut ensuite simplement lancer mongosh pour se connecter. Pour MongoDB Compass, laisser la chaîne de connexion vide.

1.3 Démarrage via docker.

Cette option nécessite d’avoir déjà Docker installé (Windows/MacOS: utiliser Docker Desktop, Linux: installer via des packages).

La commande suivante permet de lancer un serveur MongoDB en tâche de fond:

docker run --name mongodb -d -p 27017:27017 mongo:5.0.5-focal

Pour arrêter le serveur:

docker stop mongodb

Pour redémarrer un serveur arrêté:

docker start mongodb

Pour détruire le serveur:

docker rm --force mongodb

Pour importer les données (à lancer depuis le répertoire le zip a été décompressé):

for i in *.json; do cat $i | docker exec -i mongodb mongoimport --type=json --jsonArray -c $(basename $i .json) ; done

Pour lancer le shell:

docker exec -it mongodb mongosh

Pour MongoDB Compass, laisser la chaîne de connexion vide.

1.4 Vérification de la connexion

Dans le shell mongo, vérifiez que vous êtes bien connecté en lançant la commande:

show collections

qui doit produire une sortie du type:

grades
neighborhoods
restaurants
zips

Cette sortie indique les collections disponibles dans la base courante.

Dans MongoDB Compass, ces 4 collections doivent apparaître dans la base mif04 ou dans la base test.

2. Premières requêtes

Pour ces requêtes, on utilisera les collections grades et zips. La syntaxe dans mongosh est db.collection.methodecollection est le nom de la collection à requêter (l’équivalent d’un FROM en SQL) et méthode est la manière de requêter la collection.

  1. db.collection.findOne(): renvoie un document de la collection (doc).

    Pour chacune des collections grades et zip, récupérer un document, puis en déduire un type pour les éléments de cette collection.

  2. db.collection.find({}): récupère les documents de la collection sans les filtrer (doc).

    Récupérer quelques documents de grades et vérifier qu’ils sont conformes au type de la question précédente.

  3. db.collection.find({}).count(): compte le nombre de résultats de la requête.

    Donner le nombre de résultats de la requête précédente (résultat attendu: 280).

  4. db.collection.find({ field: value }): récupère les documents dont le champ field a la valeur value. Il est possible de spécifier plusieurs champs.

    Récupérer les documents dont le class_id vaut 20 dans la collection grades (7 résultats).

  5. db.collection.find({ field: { op: value } }): exprime une condition de sélection sur le champ field: op est la fonctionde comparaison et value la valeur à laquelle on veut comparer la valeur du champ (doc des opérateur de sélection).

    Récupérer les documents dont le class_id est inférieur ou égal à 20. L’opérateur inférieur ou égal se note $lte. (188 résultats)

  6. db.collection.find({ $expr: { arbre de syntaxe de l’expression en json} } ): exprime une condition générique. La syntaxe est la représentation en json de l’expression (doc). Les champs sont représentés par la syntaxe $champ ou $champ1.souschamp2. Les arguments sont souvent donnés sous forme d’une liste (mais il faut vérifier la documentation de chaque opérateur). Par exemple, { $lte: [ "$class_id", 20 ] } est une expression indiquant que le champ class_id (via "$class_id") est inférieur ($lte) à 20.

    Récupérer les documents pour lesquels le student_id est supérieur ou égal au class_id (188 résultats).

  7. Récupérer les documents dont le class_id est compris entre 10et 20 (100 résultats). Faire une version avec un double filtre sur le champ class_id (via la syntaxe { field: { op1: value1, op2: value2 } }), puis une autre version avec un $expr contenant un $and.

  8. db.collection.find({condition}, { projection spec } ): permet de choisir les champs de sortie de la requête (doc). Il est possible de supprimer des champs, voir de créer de nouveaux champs en utilisant une expression comme ci-dessus.

    Donner tous les documents de la collection avec un champ supplémentaire qui est la somme des champs class_id et student_id.

3. Prise en main de quelques étapes d’agrégation

En MongoDB, les requêtes complexes s’effectuent entre autres via le pipeline d’agrégation. Ce pipeline est constituées d’étapes de transformation successives appliquées à une collection et passées sous forme de liste à db.collection.aggregate. Dans cette partie, on va appréhender les effets de quelques opérateurs pris individuellement. Syntaxiquement, on spécifiera les requêtes comme suit:

db.grades.aggregate([
    { /* operateur */ : { /* specification */ }},
])

Pour compter le nombre de résultats, on pourra précéder comme suit:

db.grades.aggregate([
    { /* operateur */ : { /* specification */ }},
    { $count: "count" },
])
  1. { $project: { projection spec }}: applique une projection spécifiée comme pour le deuxième argument de db.collection.find() (doc).

    Reprendre la requête précédente, mais en utilisant le pipeline d’agrégation avec un $project.

  2. { $match: { find spec } }: applique un filtre similaire au premier argument de db.collection.find() (doc).

    Dans la collection zips récupérer les documents ayant une population supérieure ou égale à 10000 (7634 résultats).

  3. { $sort: {sort spec } }: trie la collection selon les champs spécifiés (doc). Pour chaque champ, on indique s’il est trié par ordre croissant ou décroissant. Les champs sont listés par ordre d’apparition dans l’ordre lexicographique utilisé. Par exemple, sur la collection zips, { state: 1, pop: -1 } inquera de trier par état (state), puis par population (pop) décroissante.

    Dans la collection grades, renvoyer les documents triés par class_id, puis par student_id (par ordre croissant pour les 2 champs).

  4. { $unwind: chemin} : chemin indique un champ, éventuellement dans un document imbriqué (via la syntaxe commençant avec $). Ce champ contient un tableau. L’opérateur produit un document pour chaque élément du tableau. Les documents produits sont identique au document de départ, sauf pour le champ désigné qui, au lieu du tableau, contient un élément de ce tableau.

    Dans la collection grades, produire un document pour chaque élément du tableau contenu dans le champ scores (1241 résultats).

  5. { $group: { _id: spec id, champ1: { agg1: expr1 }, ...} } (doc): regroupe les documents selon la valeur spec id, qui est une expression utilisant la syntaxe des expressions d’agrégation (chemins avec $, opérateurs d’expressions, etc). On peut fabriquer des documents, ce qui permet de représenter des n-uplets pour utiliser des combinaisons de valeurs comme clés. Pour chaque groupe, produit un document avec la valeur de _id, ainsi que chaque champ supplémentaire (champ1, etc) avec la valeur calculée par l’opérateur d’agrégation correspondant (agg1, etc) à qui on aura passé le tableau des valeurs calculées par les expressions (expr1, etc). Les fonctions d’agrégation utilisables sont indiquées dans la documentation. Par exemple { $group: { _id: "$state", total: { $sum: "$pop" }}} va créer un document pour chaque valeur du champ state avec un champ total qui sera la somme des champs pop des documents du groupe.

    Dans la collection zips, donner pour chaque ville la population minimale (16584 résultats).

  6. { $lookup: { from: coll, localField: champ local, foreignField: champ ext, as: champ out } } (doc): effectue une jointure avec coll. La jointure se fait sur une égalité entre le champ champ local pour les documents venant du pipeline courant et le champ champ ext pour les documents venant de la collection coll. Produit un document pour chaque document de la collection du pipeline, avec un champ supplémentaire (champ out) contenant le tableau des documents de coll satisfaisant le critère de jointure.

    Effectuer une jointure entre grades et zips sur les champs student_id et pop. Quel est le type du résultat ?

4. Pipelines d’agrégation

Dans cette partie, on va créer des pipelines d’aggrégation en enchaînant plusieurs opérateurs. Il faudra prévoir l’enchaînement des transformations à effectuer, et pour chaque transformation du pipeline, bien prévoir le type des documents du résultat afin de pourvoir configurer correctement les transformations suivantes.

Exemple: donner, par ordre de population décroissante, les états ayant plus de 10 000 000 d’habitants:

db.zips.aggregate([
    { $group: { _id: "$state",
                population: { $sum: "$pop" } } },
    { $match: { population: { $gte: 10000000 } } },
    { $sort: { population: -1 }},
])

Répondre aux questions suivantes via une requête utilisant un pipeline d’agrégation.

  1. Calculer le nombre de notes de chaque type. Indice: Commencer par extraire les notes individuellement avec $unwind. Indice: On peut calculer un nombre d’occurrences en sommant la valeur 1 sur un groupe.
  2. Donner pour chaque matière, la meilleure note d’examen.
  3. Donner les dix villes les plus peuplées. Indice: On pourra utiliser l’étape $limit.
  4. Donner la population moyenne des villes pour chaque état.
  5. Donner pour chaque matière, les étudiants ayant une note d’examen supérieure à 50. Indice: On pourra utiliser l’opérateur d’agrégation $push dans un $group pour constituer la liste.
  6. Donner pour chaque étudiant et chaque matière sa note générale. La note générale est la moyenne de chaque type de note. S’il y a plusieurs notes d’un même type, on prendra d’abord la moyenne de ce type avant de l’utiliser pour calculer la moyenne avec les autres types. Indice: Commencer par calculer pour chaque étudiant, chaque matière et chaque type de note sa moyenne.
  7. Donner, pour chaque matière le type d’épreuve la mieux réussie. Le type d’épreuve la mieux réussie est celui ayant la meilleure moyenne, calculée sur l’ensemble des notes de ce type pour cette matière. Indice: Un moyen de faire consiste à calculer la meilleure moyenne (via $max), tout en conservant l’ensemble des moyennes dans un tableau via $push.
Emmanuel Coquery
Emmanuel Coquery
Maître de conférences en Informatique