TP MongoDB

Note du 08/02/2022 Ce sujet de TP est actuellement en réécriture d'une ancienne version en anglais vers une version mise à jour en français.

Bart.Lamiroy@univ-reims.fr

Original credits: S. Vialle, CentraleSupélec

Exercice 1 : Démarrer une session MongoDB et interagir avec des données

Il existe plusieurs façons d'utiliser MongoDB, les deux principales sont

  1. créer un compte sur www.mongodb.com et utiliser l'infrastructure cloud mise à disposition ;
  2. installer la version community de Mongodb sur son équipement personnel.

L'avantage de la première est qu'il n'y a aucune contrainte technique d'installation. En revanche la version gratuite est limitée à 512 Mo de stockage. La seconde offre plus de flexibilité au coût d'un peu plus de technicité lors de l'installation et des limites matérielles de votre équipement.

Dans les deux cas, il y aura besoin d'un serveur (distant dans le premier cas, local dans le second) qui héberge les données et qui fait les calculs, et un ou plusieurs clients (forcément locaux) permettant de se connecter et d'interagir avec le serveur.

Il existe plusieurs types de clients pouvant interagir avec le serveur :

  • une interface web, accessible depuis www.mongodb.com elle a l'avantage de ne nécessiter aucune installation sur le poste client, tout se passe par un navigateur classique. L'inconvénient est une réactivité dépendante de la charge du serveur web côté serveur et le fait que toutes les fonctionnalités ne sont pas toujours accessibles (notamment le téléversement de données en masse)
  • une application graphique, facile d'utilisation, et très similaire à ce que permet de faire l'iterface web, mais plus réactive et permettant le téléversement en masse : MongoDB-Compass. Elle nécessite l'installation de l'application en local.
  • une série d'interfaces textuelles en mode console. Elles sont plus légères et réactives, permettent une plus large gamme d'intéractions avec le serveur, mais nécessitent une maitrise des langages et leur syntaxe sous-jacente. Dans ce TP nous utiliserons mongosh et éventuellement mongoimport et mongod.

Dans ce qui suit nous supposons que le client fonctionne sous Windows. Les lecteurs concernés ajusteront les instructions pour Linux ou MacOS.

Nous supposerons également un fonctionnement par interface en ligne de commande. Les interfaces graphiques permettent généralement d'exprimer la plupart des instructions abordées ici de façon plus intuitive aussi.

Exercice 2 : utilisation d'une base de données préconfigurée

Informations préliminaires

Dans cette partie nous allons utiliser une base préconfigurée sur mongodb.com en y accédant par mongosh.

mongosh

Pour se connecter à la base de données préconfigurée utilisez le lien fourni par mongoDB pour se connecter à Compass. Cet URL doit ressembler à

mongodb+srv://<username>:<password>@cluster0.rv6yxw8.mongodb.net/test

Vous utiliserez les username et password fournis en séance.

Pour tester si vous êtes bien connectés à la bonne base, les commandes suivantes doivent fournir les résultats fournis ci-dessous

> show dbs
Master_SEP          780.00 KiB
sample_airbnb        52.97 MiB
sample_analytics      9.11 MiB
sample_geospatial     1.37 MiB
sample_guides        40.00 KiB
sample_mflix         48.81 MiB
sample_restaurants    8.71 MiB
sample_supplies       2.02 MiB
sample_training      53.70 MiB
sample_weatherdata    2.83 MiB
admin               336.00 KiB
local                 3.77 GiB

> use Master_SEP
switched to db Master_SEP

Interrogations sur la base

La commande pour interroger une collection MongoDB est find(). Sa syntaxe et son fonctionnement sont fournis dans le cours. Vous pourrez également vous référer à la documentation en ligne pour une utilisation plus avancée.

La collection de données utilisée ici s'appelle est esr_stat et provient des données ouvertes publiées par le Ministère de l'Enseignement Supérieur et de la Recherche téléchargéables ici.

Si vous travaillez sur votre propre instance, importez cette collection. Elle est déjà présente dans la version partagée en ligne.

Si ce n'est pas déjà fait, sélectionnez la base de données sur laquelle vous souhaitez travailler.

> use Master_SEP

Les requêtes prendront la forme de db.myCollection.find()myCollection est le nom de la collection (ici esr_stat)

Question 1 : Explorer les collections et leur contenu

  • Listez toutes les collections disponibles dans la base de données courante avec la commande show collections
  • Visualisez le contenu d'un enregistrement quelconque (document JSON) avec la commande findOne() appliquée à la collection.
  • Émettez une requête permettant de voir l'ensemble des contenus de la collection avec db.esr_stat.find().

On observe que sans arguments, la commande find() n'opère aucune sélection et affiche l'ensemble des attributs de chaque document de l'ensemble de la collection !

Question 2 : Requêtes simples sur la collection esr_stat

En vous basant sur la documentation en ligne de MongoDB, réalisez les opérations suivantes :

  • L'un des champs de chaque enregistrement est en réalité la description d'une image. Utilisez find() de façon appropriée pour projeter le champ publication_image_fichier de tous les enregistrements de la collection esr_stat.
  • Affichez maintenant seulement les hauteurs et largeurs d'images de type PNG.
  • Triez ces enregistrements selon l'ordre croissant de la hauteur des images.
  • Raffinez les enregistrements obtenus en ne retenant seulement les images PNG dont la hauteur est contenue dans l'intervalle ] 735; 800 [.

    Attention : le test (if height >735) and (if height <800) ne fonctionnera pas s'il est réalisé avec deux tests séparés et associés avec l'opérateur "," comme premier argument de find({...}, {...}) ! Il faut utiliser la syntaxe appropriée. - Comptez le nombre d'enregistrements satisfaisant la requête précédente.

Question 3 : Requêtes et modifications de la base

ATTENTION : Cette question ne pourra être traitée correctement que si vous travaillez avec un accès (hébergé ou local) ayant des droits d'écriture/modification. Si vous utilisez l'accès générique avec le compte etudiant sur la base hébergée partagée il ne vous sera pas possible de faire cette partie.

Nous utiliserons la fonction forEach()qui peut être appliquée aux documents résultant d'une requête find().

> db.myCollection.find().forEach(<function>)

La fonction passée en argument de la méthode forEach() peut soit être complètement définie et décrite entre les parenthèses du forEach(), soit être une fonction définie au préalable dans le shell de MongoDB.

Par ailleurs, la requête update, associée à l'opértateur $set, permet de changer les champs d'un ou plusieurs enregistrements sans modifier les autres. On peut ainsi soit en ajouter, soit en modifier le contenu.

> db.myCollection.update({_id:xxxx},{$set:{theField:theValue}})
  • À partir d'une requête find () et la méthode forEach(): calculez et affichez le nombre de pixels pour chaque enregistrement décrivant une image PNG.

    Vous écrirez la fonction appelée par forEach() en JavaScript.

    e.g.:

 function f(doc) {
    var x = doc.a + doc.b;
    print ("ab =" + x);
 }
  • Changez la fonction précédemment appelée dans forEach() à ce qu'elle affiche également le champ _id de l'objet avant d'afficher le nombre de pixels.
  • En utilisant l'_id du dernier enregistrement affiché par la commande précédente, ajoutez un nouveau champ nbpix à la description de l'image PNG en utilisant la requête update associée à l'opérateur $set.
  • Modifiez la requête forEach de sorte à non seulement afficher le nombre de pixels pour chaque image PNG mais que la fonction évoquée insère également un champ nbpix dans chaque description.
  • En utilisant une requête find() et la méthode sort(), triez tous les enregistrements par ordre croissant de la taille des images PNG (exprimée en pixels).

Exercice 3 : mapReduce dans MongoDB

Évolution de la prise en charge dans les dernières versions

L'accès à et l'utilisation de mapReduce sont désormais considérés comme obsolètes dans MongoDB et l'utilisation des aggrégations leur est préférée.

Dans ce qui suit, nous continuerons à utiliser les formulations sous forme de mapReduce, sachant que des méthodes de conversion sont mises en place pour les transformer en pipeline d'aggrégation. Dans la suite vous pourrez répondre aux questions invariablement soit en fournissant une solution mapReduce, soit une aggrégation.

Notez que l'utilisation de mapReduce est bloquée sur la version gratuite hébergée et ne peut donc qu'être utilisée sur une version hébergée payante ou sur une version installée localement.

Rappel : l'implantation MongoDB mapReduce est différente de celle de Hadoop. Elle est moins générique et permet moins d'optimisations, mais la différence fondamentale concerne la phase de reduce, dans la mesure où les fonctions map et reduce doivent partager le meme format de sortie <key, value>. Ceci permet notamment d'éviter des débordements de mémoire, mais introduit des restrictions sur les tâches qui peuvent être réalisées.

Question 1 : Import a larger collection

Pour cet exercice nous tenterons d'utiliser une collection de données plus large, partiellement pour tenter de montrer les limites de l'exécution sur une seule machine et d'illustrer les besoins d'un passage à l'échelle horizontal. Cette collection de données, stockée au format JSON prend approximativement 8 Go, et occupera environ 1.5 Go une fois transformée en BSON (Binary JSON) et stockée dans MongoDB.

Ceci signifie que cette base de données ne peut pas être hébergée sur l'offre gratuite sur mongodb.com, puisque l'offre de stockage y est limitée à 512 Mo

Les options pour faire cette partie sont alors :

  • héberger les données sur une installation locale ;
  • utiliser une version tronquée sur l'offre d'hébergement gratuite ;
  • utiliser une solution d'hébergement payante.

En utilisant la même base de données que précédemment (Master_SEP) téléchargez les données en-ESR-located-workforce-of-registered-students-ESR-public.json et importez-les dans la collectionenseignement

Remarques:

  • Il s'agit à nouveau d'un jsonArray.
  • Compte-tenu de la taille de la collection, l'importation peut prendre plusieurs dizaines de minutes.

Cette collection contient des données sur les inscriptions d'étudiants dans le système d'enseignement supérieur français.

Question 2: Compter le nombre de baccalauréats pour chaque type

Quickly visualize one of the recordings of the enseignement collection. One of the fields called bac (near the end of each record) encodes each type of baccalaureate by a character string containing a single integer.

Write a mapReduce program (or alternatively a aggregate pipeline) that counts the number of occurrences of each baccalaureate type:

  • Write a map function and a reduce function.

Reminder of the syntax of a map function for mapReduce in MongoDB:

map2 = function () {
 var k = ...;
 var v = ...;
 emit (k, v);
}

Reminder of the syntax of a reduce function for mapReduce in MongoDB:

reduce2 = function (k, Array_of_v) {
  ...;
  return (result); // 'result' of same type as 'v'
}
  • Run a mapReduce according to the 2 possible syntaxes
> db.runCommand({"mapreduce": "enseignement", "map": map2, "reduce": reduce2, "out": "res2a"})

And

> db.enseignement.mapReduce (map2, reduce2,{"out": "res2b"})

The results (the res2a and res2b collections) must be identical. You can view their contents with the find() command.

In both cases, the mapReduce operation returns a quick report of its executions, and indicates in particular its execution time. The execution time is usually longer for the first mapReduce that requires loading more data in RAM.

Question 3: trier des données et l'impact des index

The MongoDB mapReduce allows sorting/ordering the input data (the collection enseignement in our case) with the option sort (specifying the attribute and the sorting order):

e.g.

db.myCollection.mapReduce(map, reduce,{"out":"results", "sort": {"abcd": -1}, ...})

See the online MongoDB mapReduce documentation for more information on the option

sort .

  • Re-run your mapReduce by sorting the input data according to the field bac in ascending order, then in descending order.

What "problem" appears?

You can make sort work more efficiently by creating an index on the sorted field (bac). This accelerates the treatments and avoids the previous problem.

  • Create an index on this collection and on the bac the increasing order.

Use: db.myCollection.createIndex (...).

  • Re-run your mapReduce by sorting the input data according to the field bac in ascending order, then in descending order.
  • Did the problem that appeared previously disappear? Both in ascending and descending order?

Question 4: limitation des données d'entrée

In addition to sorting the input data, we can limit their number with the limit option of mapReduce. This makes it possible to test an algorithm, a command on a subset of the data and not to have to wait for too long.

  • Use the sort and limit options together, and limit the number of entries to 1000.
  • Are the results identical with sorting in ascending and descending order? Why ?

Question 5: reformatage des résultats

The MongoDB mapReduce requires that the key-output pairs of the reducer are the same as those of the map output. However, a function finalize(k, v) makes it possible to reformat outputs of the reduce phase.

  • Implement a function finalize (k, v) {newVal = ...; return (newVal);} in order to generate outputs in the format {"Bin": bin code, "number": number}

  • Run your mapReduce specifying to execute your finalize() function

  • View the results with the find() command

Question 6: adjointre une requête aux données d'entrée

It is possible to specify a query on the input data as to apply simple filtering before entering the map phase.

  • Extend the execution of your mapReduce with a query that retains only those records whose dep_etab_lib is "Marne".
  • Rerun your mapReduce and view the result

Exercice 4: identifier toutes les valeurs possibles d'un champ

The field dep_etab_lib can take multiple different values, difficult to predict because of the accentuation of certain characters, but in finite numbers (according to the nature of the values). We wish to get a list of all the possible values of this field, by presenting in the end each value once and only once.

Question 1: code mapReduce

Write and run a mapReduce program to list once and only once each existing value from dep_etab_lib. Which pattern will you use?

Question 2: compter les résultats

Use the count() method to count the number of responses of the result.

Exercice 5: opérer une jointure entre deux collections avec l'opérateur $lookup

NoSQL databases remain weak for joining. It is always assumed that the join must be done at the time of writing, meaning that we must put in a collection "all that is necessary" to be able to query it without needing other data.

There are still solutions to execute the necessary joins, but they are expensive in processing time. Hadoop proposes a Map-Reduce on two flows of entries (two distributed files). However, MongoDB limits its mapReduce to a single collection, which prevents it from joining on two collections.

MongoDB allows "joining" accross two collections with the $lookup operator in an aggregation .

Question 1: Generation of Two Specialized Tables

From the larger collection enseignement we start by generating two simple collections, that we than will "join" together.

  • We want to generate a collection res51 of all documents containing the fields:

    • establishment_lib, the name of the establishment
    • cursus_lmd, containing "L", "M" or "D"

In fact, we will generate documents / records containing only keys, composed of these two fields: _id: { "etablissement_lib": leEtablissementLib, "cursus_lmd": leCursusLMD}

This will allow us to benefit from the deduplication capability of the key management mechanism, and will get as many records as there are different pairs of these two fields.

  • Write and run the small mapReduce program that will produce this collection res51 of two-field key records.

  • Repeat the same operations to obtain the res52 collection of recordings of keys with the fields:

    • establishment_lib the name of the establishment
    • aca_etab_lib the name of the home academy

The goal is to obtain records containing:

`_id: { "etablissement_lib": leEtablissementLib, "aca_etab_lib": laAcademie}

Question 2: effectuer une jointure avec l'opérateur $lookup dans une aggregation

We will now operate a join of the res51 and res52 collections using the shared field etablissement_lib as a join key, and using the $lookup operator in an aggregate made especially for this purpose.

  • Implement an aggregation that realises this join and stores the result in the collection res5j.
db.res51.aggregate ([{"$lookup": {...}},{"$out": "res5j"}]);

Refer to the online MongoDB documentation.

  • View the result in the res5j collection. The output format should contain all the relevant information, but sometimes redundantly and not very straightforward to use.

Question 3: transformation du résultat de la jointure

  • Complete the aggregation with the $project and $unwind operators to obtain a collection output whose records are similar to:
>db.res5j.findOne()
{
  "_id": ObjectId ("592d91222bb10509205a4f77"), 
  "establishment_lib": "Université Aix-Marseille", 
  "cursus_lmd": "D",
  "aca_etab_lib": "Aix-Marseille"
}
  • View the result in the new res5j collection.