NoSQL

MongoDB est une base de données NoSQL distribué de type Document Store (site web)

Objectifs :

  • Gérer de gros volumes
  • Facilité de déploiement et d’utilisation
  • Possibilité de faire des choses complexes tout de même

Modèle des données

Principe de base : les données sont des documents

  • stocké en Binary JSON (`BSON)
  • documents similaires rassemblés dans des collections
  • pas de schéma des documents définis en amont
    • contrairement à un BD relationnel ou NoSQL de type Column Store
  • les documents peuvent n’avoir aucun point commun entre eux
  • un document contient (généralement) l’ensemble des informations
    • pas (ou très peu) de jointure à faire
  • BD respectant CP (dans le théorème CAP)
    • propriétés ACID au niveau d’un document

Point sur JSON

  • JavaScript Object Notation
  • Créé en 2005
  • On parle de littéral
  • Format d’échange de données structurées léger
  • Schéma des données non connu
    • contenu dans les données
  • Basé sur deux notions :
    • collection de couples clé/valeur
    • liste de valeurs ordonnées
  • Structures possibles :
    • objet (couples clé/valeur) :
    • {}
    • { "nom": "jollois", "prenom": "fx" }
    • tableau (collection de valeurs) :
    • []
    • [ 1, 5, 10]
    • une valeur dans un objet ou dans un tableau peut être elle-même un littéral
  • Deux types atomiques (string et number) et trois constantes (true, false, null)

Validation possible du JSON sur jsonlint.com/

{
    "tubd": {
        "formation": "DU Analyste Big Data",
        "responsable": { "nom": "Poggi", "prenom": "JM" },
        "etudiants" : [
            { "id": 1, "nom": "jollois", "prenom": "fx" },
            { "id": 2, "nom": "aristote", "details": "délégué" },
            { "id": 5, "nom": "platon" }
        ],
        "ouverte": true
    },
    "tudv": {
        "formation": "DU Data Visualisation",
        "ouverte": false,
        "todo": [
            "Creation de la maquette",
            "Validation par le conseil"
            ],
        "responsable": { "nom": "Métivier" }
    }
}

Compléments

BSON : extension de JSON

  • Quelques types supplémentaires (identifiant spécifique, binaire, date, …)
  • Distinction entier et réel

Schéma dynamique

  • Documents variant très fortement entre eux, même dans une même collection
  • On parle de self-describing documents
  • Ajout très facile d’un nouvel élément pour un document, même si cet élément est inexistant pour les autres
  • Pas de ALTER TABLE ou de redesign de la base

Pas de jointures entre les collections

Langage d’interrogation

  • Pas de SQL (bien évidemment), ni de langage proche
  • Définition d’un langage propre
  • Langage permettant plus que les accès aux données
    • définition de variables
    • boucles

Commandes suivantes réalisables à partir du shell

Utilisation avec R

On peut interroger une base de données de ce type via le package mongolite dans R. Dans la suite, nous allons nous connecter sur un serveur distant, et travailler pour l’exemple sur une base des pays du monde (datant de fin 99).

library(mongolite)
m = mongo(url = "mongodb://193.51.82.104:2231",
          db = "world",
          collection = "country")

Le premier document est présenté ci-dessous. La base contient les informations de 239 pays.

{
    "_id" : ObjectId("586a0f9e9a4df5e90a24d1ed"),
    "Code" : "ABW",
    "Name" : "Aruba",
    "Continent" : "North America",
    "Region" : "Caribbean",
    "SurfaceArea" : 193,
    "IndepYear" : "NA",
    "Population" : 103000,
    "LifeExpectancy" : 78.4,
    "GNP" : 828,
    "GNPOld" : 793,
    "LocalName" : "Aruba",
    "GovernmentForm" : "Nonmetropolitan Territory of The Netherlands",
    "HeadOfState" : "Beatrix",
    "Capital" : {
        "ID" : 129,
        "Name" : "Oranjestad",
        "District" : "–",
        "Population" : 29034
    },
    "Code2" : "AW",
    "OffLang" : [
        {
            "Language" : "Dutch",
            "Percentage" : 5.3
        }
    ],
    "NotOffLang" : [
        {
            "Language" : "English",
            "Percentage" : 9.5
        },
        {
            "Language" : "Papiamento",
            "Percentage" : 76.7
        },
        {
            "Language" : "Spanish",
            "Percentage" : 7.4
        }
    ]
}

Document dans R

R ne gérant pas nativement les données JSON, les documents sont traduits, pour la librairie mongolite, en data.frame. Pour récupérer le premier document, nous utilisons la fonction find() de l’objet créé m.

d = m$find(limit = 1)
d
class(d)

Les objets Capital, OffLang et NotOffLang sont particuliers, comme on peut le voir dans le JSON. Capital est une liste, et les deux autres sont des tableaux de listes. Voila leur classe en R.

class(d$Capital)
d$Capital
class(d$OffLang)
d$OffLang
class(d$NotOffLang)
d$NotOffLang

Restriction et Projection

La fonction find() de l’objet m permet de retourner tous les documents. On peut se limiter à un certain nombre de documents avec l’option limit, comme précédemment.

Pour faire une restriction sur la valeur d’un attribut, il faut utiliser l’option query, avec un formalisme particulier. Il faut écrire au format JSON dans une chaîne, avec pour les champs à comparer leur nom suivi de la valeur (pour l’égalité) ou d’un objet complexe pour les autres tests (infériorité, supériorité, présence dans une liste).

Pour une projection, c’est l’option fields à renseigner. On écrit au format JSON, avec la valeur 1 pour les champs qu’on souhaite avoir en retour. Par défaut, l’identifiant (_id) est toujours présent, mais on peut le supprimer en indiquant 0.

Dans cet exemple, on recherche le document dont l’attribut "Name" est égal à "France", et on affiche uniquement les attributs "Name" et "Population". Dans la deuxième expression, on supprime l’affichage de l’identifiant interne à MongoDB.

m$find(query = '{"Name": "France"}', 
       fields = '{"Name": 1, "Population": 1}')
m$find(query = '{"Name": "France"}', 
       fields = '{"_id": 0, "Name": 1, "Population": 1}')

Ici, on recherche les pays du continent "Europe" dont la population est supérieur à 50M d’habitants.

m$find(query = '{"Continent": "Europe", "Population": {"$gt": 50000000}}',
       fields = '{"_id": 0, "Name": 1, "Population": 1}')
m$find(query = '{"Continent": {"$in": ["Antarctica", "South America"]}}', 
       fields = '{"_id": 0, "Name": 1, "Continent": 1}')

Dans ces exemples, nous cherchons en premier les pays avec comme capital "Paris", et ensuite les pays dont une des langues officielles est le français.

m$find(query = '{"Capital.Name": "Paris"}',
       fields = '{"_id": 0, "Name": 1, "Capital": 1}')
m$find(query = '{"OffLang.Language": "French"}',
       fields = '{"_id": 0, "Name": 1, "OffLang.Percentage": 1}')

Il est aussi posible de trier les documents retournés, via l’option sort. Toujours en JSON, on indique 1 pour un tri croissant et -1 pour un tri décroissant.

m$find(fields = '{"_id": 0, "Name": 1}', 
       sort = '{"Population": -1}', 
       limit = 10)

Agrégat

Dénombrement

On peut déjà faire un dénombrement avec la fonction count() de l’objet m. Sans option, on obtient le nombre de documents de la collection. On peut aussi ajouter une restriction pour avoir le nombre de documents respectant ces conditions. Les requêtes s’écrivent de la même manière que pour la fonction find().

m$count()
m$count(query = '{"Name": "France"}')
m$count(query = '{"Continent": "Europe"}')

Autre

Il existe la fonction aggregate() pour tous les calculs d’agrégat (et même plus). Il faut passer dans le paramètre pipeline un tableau d’actions, pouvant contenir les éléments suivants :

  • $project : redéfinition des documents (si nécessaire)
  • $match : restriction sur les documents à utiliser
  • $group : regroupements et calculs à effectuer
  • $sort : tri sur les agrégats
  • $unwind : découpage de tableaux

group by NULL

m$aggregate(pipeline = '[
    {"$group": {"_id": "$Continent", "NbPays": {"$sum": 1}}}
]')
m$aggregate(pipeline = '[
    {"$group": {"_id": "$Continent", "NbPays": {"$sum": 1}, "Population": {"$sum": "$Population"}}}, 
    {"$sort": { "NbPays": -1}}
]')

Si on veut avoir le pourcentage de population parlant une langue spécifique officiellement, il faut utiliser cette fonction, avec l’action $unwind. Celle-ci permet de dupliquer les lignes de chaque document, avec chaque valeur du tableau indiqué dans $unwind. Ensuite, on se retreint aux pays dont une langue officielle est le français avec $match. Enfin, on n’affiche que le nom du pays et le pourcentage de la population parlant le français.

m$aggregate('[
    { "$unwind": "$OffLang" },
    { "$match": { "OffLang.Language": "French"}},
    { "$project": { "_id": 0, "Name": 1, "OffLang.Percentage": 1 }}
]')
m$aggregate('[
    { "$unwind": "$OffLang" },
    { "$project": { "Language": "$OffLang.Language", "Percentage": "$OffLang.Percentage"}},
    { "$group": { "_id": "$Language", "NbPays": {"$sum": 1}, "PctMoyen": { "$avg": "$Percentage" } }},
    { "$sort": { "NbPays": -1}}
]')

Map-Reduce

Le paradigme Map-Reduce permet de décomposer une tâche en deux étapes :

  1. Map : application d’un algorithme sur chaque document, celui-ci renvoyant un résultat ou une série de résultat
  2. Reduce : synthèse des résultats renvoyés dans l’étape précédente selon certains critères

Exemple classique : décompte des mots présents dans un ensemble de texte

  • Map : pour chaque texte, à chaque mot rencontré, on créé un couple <mot, 1> (un document = beaucoup de résultats générés)
  • Reduce : pour chaque mot, on fait la somme des valeurs pour obtenir le nombre de fois où chaque mot apparaît dans l’ensemble des textes à disposition

On utilise la fonction mapreduce() de m pour appliquer l’algorithme Map-Reduce sur les documents de la collection, avec les paramètres suivants :

  • map : fonction JavaScript
    • aucun paramètre
    • emit(key, value) pour créer un couple résultat
  • reduce : fonction JavaScript
    • deux paramètres : key et values (tableau des valeurs créés à l’étape précédente)
    • return result pour renvoyer le résultat
  • out : collection éventuelle dans laquelle stocker les résultats dans MongoDB

Dans la fonction concernant l’étape Map, on utilise l’objet this pour accéder aux attributs du document. Le langage utilisé est le JavaScript.

Dans l’exemple ci-dessous, nous calculons pour chaque continent la population de celui-ci.

m$mapreduce(
    map = 'function() { emit(this.Continent, this.Population)}',
    reduce = 'function(cont, nb) { return Array.sum(nb) }'
)

On peut utiliser ce paradigme pour calculer le pourcentage médian de la population parlant cette langue.

m$mapreduce(
    map = 'function() {
        if (this.OffLang) {
            for (i = 0; i < this.OffLang.length; i++) {
                emit(this.OffLang[i].Language, this.OffLang[i].Percentage);
            }
        }
    }',
    reduce = 'function(id, values) { 
        values.sort( function(a,b) {return a - b;} );
        var half = Math.floor(values.length/2);
        if(values.length % 2)
            return values[half];
        else
            return (values[half-1] + values[half]) / 2.0;
    }'
)

Exercices

Les données de la base médicaments ont été importées dans MongoDB via ce script (merci de ne pas l’exécuter).

Vous pouvez vous connecter à cette base à l’aide du code suivant.

library(mongolite)
bd = mongo(url = "mongodb://193.51.82.104:2231",
           db = "medicaments",
           collection = "import20161109")

Trouver les informations suivantes :

  1. Le médicament dont le CodeCIS est 60051234
  2. Les médicaments dont le titulaire est "MEDICE"
  3. Les médicaments correspondant au générique "IBUPROFENE 400 mg - BRUFEN 400 mg, comprimé pelliculé." (Afficher seulement la dénomination et le titulaire)
  4. Le nombre de médicament au total
  5. Le nombre de médicament avec "Autorisation active" comme StatutAMM
  6. Le nombre de médicaments avec surveillance renforcée (cf Surveillance)
  7. Le taux moyen de remboursement (pour ceux avec autorisation active)
  8. Le prix moyen (idem que le taux)
  9. Le nombre de médicaments par laboratoire titulaire, classés dans l’ordre décroissant du nombre de médicaments
  10. Le nombre de présentation (cf CIP) des médicaments qui sont à "prescription hospitalière" (cf CPD)

Essayer de faire les exercices du TP1 :

  1. Quels sont les dix médicaments avec le plus de composants (Code CIS, Dénomination et nombre de composants) ?
  2. Pour chaque type de générique, on veut savoir le nombre de médicaments associés, ainsi que leur taux de remboursement moyen et leur prix moyen.
  3. Quelles sont les voies d’administration possibles ? Et combien de médicaments sont concernés pour chaque voie ?
  4. Quels sont les médicaments dont le service médical rendu (ou SMR) est jugé insuffisant ? Indiquez leur taux de remboursement et leur prix, en les classant par prix décroissant.

NB :

  • les voies d’administration sont listées dans une seule variable, et séparées par des “;” (par exemple "cutanée;orale;sublinguale"). Il faut donc ici aussi faire un pré-traitement.