Dans ce document est l’utilisation du package mongolite permettant la connection à une base de données MongoDB.

Connexion

Pour interroger MongoDB dans R, il faut créer une connexion entre les deux. Ici, je créé une connexion à un serveur distant (situé dans les locaux de l’Université rue des St-Pères). A l’IUT, l’adresse du serveur local MongoDB est : 172.19.32.10.

library(mongolite)
conSportifs = mongo(collection = "Sportifs", db = "gym", url = "mongodb://193.51.82.104:2231")
conGymnases = mongo(collection = "Gymnases", db = "gym", url = "mongodb://193.51.82.104:2231")

L’objet ainsi créé contient un ensemble de fonctions permettant l’accès aux données de MongoDB.

Récupération d’informations simples

Une fois cette connexion créée, il est possible de regarder quelles sont les bases de données contenues, ainsi que les collections présentes dans ces BD. .

# Nombre d'enregistrement de la collection Sportifs de la BD gym
conSportifs$count()

Premier enregistrement d’une collection

Dans Mongo, il n’y a pas de schéma pour une base de données, ni même pour une collection. Un moyen de s’approprier les données est donc de regarder le premier élément. C’est pourquoi il existe la fonction find() avec le paramètre limit. Cette fonction transforme le résultat en data.frame, ce qui n’est pas forcément désiré. Il existe donc la fonction iterate(), qui renvoie un curseur dans lequel il est possible de naviguer avec la fonction one() (renvoie les éléments un par un) ou la fonction batch() (renvoie tous les éléments en une liste).

# Premier enregistrement de la collection Sportifs de la BD gym
conSportifs$find(limit = 1)
# Idem mais conservé en liste
conSportifs$iterate(limit = 1)$one()
conSportifs$iterate(limit = 1)$batch()
conSportifs$iterate(limit = 1)$page()

Sur cet enregistrement, on peut vouloir avoir accès aux données directement. Voici comment faire avec soit le retour de find(), soit le retour de iterate()

# Avec find(limit = 1)
enr1 = conSportifs$find(limit = 1)
enr1$Nom
enr1$Sports
# Avec iterate(limit = 1)$one()
enr1bis = conSportifs$iterate(limit = 1)$one()
enr1bis$Nom
enr1bis$Sports

Valeurs distinctes d’un élément d’une collection

Puisqu’on n’a pas de schéma, on peut aussi vouloir avoir la liste des valeurs prises par un élément particulier.

# Liste des valeurs prises par l'élement Sexe dans tous les enregistrements de gym.Sportifs
conSportifs$distinct("Sexe")

Interrogation de données

On fait référence ici aussi à l’interrogation de type SQL, mais dans un environnement NoSQL (sans schéma explicite, sans stricte égalité de schéma entre les enregistrements, sans jointure).

Restriction et projection

La fonction find() permet de renvoyer tous les enregistements par défaut. Il faut donc la manipuler prudemment si on en a beaucoup. Il est possible d’indiquer dans celle-ci les restrictions et/ou projections que l’on souhaite faire.

Pour effectuer une restriction, on utilise le paramètre query de la fonction find(). Cette requête doit être une chaîne de caractère, et dans le même format que dans le langage JS de la console Mongo.

conSportifs$find(query = '{"Sexe": "m"}')
conSportifs$find(query = '{"Sexe": "F", "Age": { "$lte": 22 }}')

La projection se fait avec le paramètre fields, recevant une chaîne de caractère, au format du langage console de Mongo. Ici, on n’affiche que les premières lignes du résultat.

head(conSportifs$find(fields = '{"_id": 0, "Nom": 1}'))

Il est bien évidemment possible de combiner ces deux paramètres pour faire les deux opérations.

conSportifs$find(query = '{"Sexe": "F", "Age": { "$lte": 22 }}',
                 fields = '{"_id": 0, "Nom": 1, "Prenom": 1, "Age": 1, "Sexe": 1}')

On peut ajouter un tri sur le résultat.

conSportifs$find(query = '{"Sexe": "F", "Age": { "$lte": 23 }}',
                 fields = '{"_id": 0, "Nom": 1, "Prenom": 1, "Age": 1, "Sexe": 1}',
                 sort = '{"Age": -1, "Nom": 1}')

Traitement par document

Comme indiqué précédemment, la fonction iterate() permet de traiter les documents un par un dans R. Dans ce cas, soit on utilise le curseur que l’on vide avec la fonction one(), jusqu’à ce qu’il retourne la valeur NULL.

it = conGymnases$iterate()
while (!is.null(doc <- it$one())) {
    cat(doc$NomGymnase, doc$Ville, doc$Surface, sep = ", ")
    cat(": ", length(doc$Seances), " séances\n", sep = "")
}

Soit on utilise la fonction batch() pour récupérer tous les éléments dans une liste.

sapply(conGymnases$iterate()$batch(),
       function (e) {
           return(c(Nom = e$NomGymnase, Ville = e$Ville,
                    NbSeances = length(e$Seances)))
       })

Agrégat

Les agrégats sont effectués avec la fonction aggregate(), dans laquelle on définit un pipeline sous forme de list, avec une syntaxe proche de celle de Mongo. Les variables de regroupement sont celles indiquées dans le _id de l’élement $group. Si la valeur est null, il regroupe tous les enregistrements.

conSportifs$aggregate('[{"$group": { "_id": "null", "AgeMoyen": { "$avg": "$Age" }, "Nb": { "$sum": 1 } } }]')

Par contre, si on indique une variable (ici Sexe par exemple), il fait le regroupement par modalité, classiquement.

conSportifs$aggregate('[{"$group": { "_id": "$Sexe", "AgeMoyen": { "$avg": "$Age" }, "Nb": { "$sum": 1 } } }]')

On remarque ici qu’il y a la modalité m et la modalité M. Pour mettre en majuscule, on peut le définir directement dans la définition de _id.

conSportifs$aggregate('[{"$group": { "_id": { "$toUpper" : "$Sexe" }, "AgeMoyen": { "$avg": "$Age" }, "Nb": { "$sum": 1 } } }]')

Calcul

Pour créer des nouveaux éléments dans un enregistrement, il est nécessaire d’utiliser la fonction d’aggrégation, avec $project.

conSportifs$aggregate('[{
                    "$project": {
                      "_id": 0,
                      "Nom": 1,
                      "Prenom": { "$toLower": "$Prenom" },
                      "Sexe": { "$toUpper": "$Sexe" }
                    }
                  }]'
)

Recherche complexe

Cette commande d’agrégation sert aussi à faire des recherches compexes dans les sous-éléments présents dans les enregistrements.

Par exemple, dans la collection Gymnases, nous avons la liste des séances avec les jours et les sports. Si on cherche les jours et les gymnases où il y a des séances de Hand ball, la recherche simple comme suit ne suffit pas. On obtient bien les informations voulues, mais on a aussi les séances des gymnases pour les autres sports.

conGymnases$iterate(
  query = '{ "Seances.Libelle": "Hand ball" }',
  fields = '{ 
    "_id": 0, "NomGymnase": 1, "Ville": 1, "Seances.Jour": 1, "Seances.Libelle": 1 
  }'
)$batch()

Pour résoudre ce problème, nous allons utilisons la fonction d’aggrégation, avec la commande $unwind. Celle-ci permet de séparer les éléments du tableau passé en paramètre. Ici, d’un enregistrement par gymnase, on passe à un enregistrement par gymnase et par séance.

head(conGymnases$aggregate('[{ "$unwind": "$Seances" }]'))

Dans le pipeline, on peut aussi ajouter des restrictions, avec la commande $match. On peut mettre celui-ci avant et/ou après le $unwind. Et choisir les éléments retournés avec $project. Pour répondre à la question jours et gymnases où il y a des séances de Hand ball, on peut faire ainsi :

conGymnases$aggregate('[
    { "$match": { "Seances": { "$elemMatch": { "Libelle": "Hand ball" } } } },
    { "$unwind": "$Seances" },
    { "$match": { "Seances.Libelle": "Hand ball"} },
    { 
      "$group": { 
        "_id": "$NomGymnase", 
        "Jours" : { "$addToSet": { "$toLower": "$Seances.Jour" } } ,
        "Sports" : { "$addToSet": "$Seances.Libelle" }
      } 
    }]'
)

Map-Reduce

L’algorithme Map-Reduce est un paradigme important dans le NoSQL, qui se décompose en deux étapes :

  • Map : Sur chaque document, on va émettre un ou plusieurs messages de type (clé, valeur)
  • Reduce : Pour chaque clé, on va réduire les valeurs émises en une seule. Les fonctions doivent être écrites en JavaScript.

Dans un cas simple, tel que celui ci-dessous, on émet un message par document.

conGymnases$mapreduce(
    map = 'function(){ emit(this.Ville, this.Surface) }',
    reduce = 'function(cle, valeurs) { return Array.sum(valeurs) / valeurs.length }'
)

Mais il est aussi possible d’émettre plusieurs messages par documents.

conGymnases$mapreduce(
    'function(){
        if (this.Seances) {
            for (s = 0; s < this.Seances.length; s++) {
                emit(this.Seances[s].Libelle, 1)
            }
        }
    }',
    'function(lib, nb) {
        return Array.sum(nb)
    }'
)