7.2. Requêtes depuis R : mongolite

  • Auteurs/trices : Faisal JAYOUZI, Paul LANCELIN, Yolan PERONNET

Le chapitre suivant a pour objectif d’explorer le package mongolite permettant d’effectuer des requêtes MongoDB depuis R. La rédaction du tutoriel suivant trouve sa source dans la documentation rédigée par l’auteur du package mongolite, Jeroen Ooms. Celle-ci est accessible à l’adresse suivante : https://jeroen.github.io/mongolite/

7.2.1. Installation du package mongolite et connexion à un serveur MongoDB

7.2.1.1. Installation et chargement du package mongolite

Les packages binaires de mongolite peuvent être installés directement depuis le CRAN via la manipulation suivante à partir de la barre d’outils de RStudio :


Tools -> Install Packages -> mongolite -> Install


Ou via la commande suivante à exécuter dans la console ou depuis un script R :

install.packages("mongolite")

Vous pouvez également installer la version de développement, qui contient les dernières fonctionnalités. Pour cela, exécutez la commande suivante :

devtools::install_github("jeroen/mongolite")

Remarque

L’installation et le chargement du package devtools au préalable sera nécessaire pour cette dernière exécution.

Une fois le package installé, il vous suffira de le charger dans votre envionnement de travail R via la commande suivante à exécuter dans la console ou depuis un script R :

library(mongolite)

7.2.1.2. Connexion à une collection d’une base de données présente sur un serveur MongoDB

Après avoir chargé les packages nécessaires dans votre environnement R, vous pourrez vous connecter à une collection d’une base de données présente sur un serveur MongoDB à partir d’un lien URI, du nom de la base de données, et du nom de la collection à laquelle vous souhaitez accéder. Pour ce faire, il suffit d’utiliser la fonction mongo() de la librairie mongolite de la manière suivante :

coll <- mongo(collection="ma_collection", db="ma_BDD",
            url="mon_uri",
            verbose=TRUE)

La fonction mongo() prend en entrée les arguments suivants :

  • collection : nom de la collection à laquelle se connecter. La valeur par défaut est “test”

  • db : nom de la base de données à laquelle se connecter. La valeur par défaut est “test”.

  • url : adresse du serveur MongoDB au format URI standard.

  • verbose : si TRUE, émet une sortie supplémentaire

  • options : options de connexion supplémentaires telles que les clés et certificats SSL que nous ne developperons pas dans ce tutoriel.

Remarque

La fonction mongo() prend obligatoirement en entrée le nom d’une collection d’une base de données. Nous comprenons alors que mongolite nous permet seulement d’intéragir avec une collection d’une base données, et non pas avec la base tout entière. Ici nous aurons donc qu’un seul objet pointant sur une collection avec laquelle intéragir. Nous n’aurons donc pas comme sur python et sa librarie pymongo d’objets clients et d’objets base de données.

Astuce

L’adresse URI à spécifier dans le paramètre URL définit l’adresse du serveur et des options de connexion supplémentaires. Parmi ces options, nous pouvons notamment spécifier des mots de passe que nous vous conseillons de lire dans des fichiers externes afin d’en préserver la confidentialité. Afin d’obtenir plus de précisions sur le format exact de l’URI attendu (authentification, tunnel SSH, options SSL et options de réplique), nous vous renvoyons à la documentation.

Pour ce qui est de sa sortie, la fonction mongo() renvoie un objet propre à sa librairie mère, une Mongo collection, qui, comme nous l’avons vu précédemment, pointe sur une collection d’une base de données. Regardons de plus près à quoi correspond ce type d’objet sur un exemple concret. Ici nous nous connectons à la collection “NYfood” d’une base de données “food” contenant de nombreuses informations sur les restaurants de New-York.

library(mongolite)
coll <- mongo(collection="NYfood", db="food",url = "mongodb://localhost:27017/food", verbose=TRUE)
coll
<Mongo collection> 'NYfood' 
 $aggregate(pipeline = "{}", options = "{\"allowDiskUse\":true}", handler = NULL, pagesize = 1000, iterate = FALSE) 
 $count(query = "{}") 
 $disconnect(gc = TRUE) 
 $distinct(key, query = "{}") 
 $drop() 
 $export(con = stdout(), bson = FALSE, query = "{}", fields = "{}", sort = "{\"_id\":1}") 
 $find(query = "{}", fields = "{\"_id\":0}", sort = "{}", skip = 0, limit = 0, handler = NULL, pagesize = 1000) 
 $import(con, bson = FALSE) 
 $index(add = NULL, remove = NULL) 
 $info() 
 $insert(data, pagesize = 1000, stop_on_error = TRUE, ...) 
 $iterate(query = "{}", fields = "{\"_id\":0}", sort = "{}", skip = 0, limit = 0) 
 $mapreduce(map, reduce, query = "{}", sort = "{}", limit = 0, out = NULL, scope = NULL) 
 $remove(query, just_one = FALSE) 
 $rename(name, db = NULL) 
 $replace(query, update = "{}", upsert = FALSE) 
 $run(command = "{\"ping\": 1}", simplify = TRUE) 
 $update(query, update = "{\"$set\":{}}", filters = NULL, upsert = FALSE, multiple = FALSE) 

Nous constatons alors que la Mongo collection est un environnement contenant les informations de la collection “NYfood” avec lequel nous pouvons intéragir via de nombreuses méthodes. Chacune de ces méthodes s’appliquera sur une Mongo collection à l’aide d’un $ et permettra d’effectuer l’équivalent d’une requête MondoDB sur une collection. Par exemple, pour faire une simple requête find en NoSQL récupérant tous les documents d’une collection, il suffira d’écrire :

coll$find()

Ou encore pour affichez la liste des index de la collection NYfood, il suffira d’écrire :

coll$index()

L’objet des prochaines sections de ce chapitre sera alors d’explorer ces différentes méthodes et de voir leurs requêtes équivalentes en NoSQL. Nous reviendrons notamment sur les objets renvoyés par ces différentes méthodes. Toutefois, nous pouvons d’ores et déjà remarquer qu’une méthode find() renvoie la collection complète dans un dataframe ce qui, dans des cas de grands volumes de données, pourrait entrainer des saturations de mémoire de votre machine. Nous verrons par la suite que pour s’affranchir de ce problème, nous pourrons utiliser une méthode iterate(), similaire à la méthode find(), renvoyant non plus un dataframe, mais un Mongo iterator. Un Mongo iterator est un objet propre à mongolite permettant de ne pas stocker explicitement en mémoire le résultat d’une requête mais d’en conserver un itérateur.

7.2.2. Requêtes de données

Mongolite utilise une syntaxe JSON dans les arguments de ses méthodes pour interroger des collections. Cette syntaxe JSON devra être spécifiée sous forme de chaîne de caractère dans R. Ces différentes chaînes feront alors l’objet de divers paramètres pour les méthodes présentées dans ce chapitre.

7.2.2.1. Méthode count

Tout comme le .count() en MongoDB (plus de précisions ici), la méthode count() nous permet de calculer le nombre de résultats d’une collection, ou encore le nombre de résultats d’une certaine requête. Cette méthode n’a qu’un seul argument (query) et sa valeur par défaut est “{}”. Elle renvoie un entier.

Exemples :

Afficher le nombre de résultats de la base NYfood par exemple :

req = "{}"
coll$count(query=req)
25357

Nous pouvons également noter l’équivalent en MongoDB :

req = "{}"
coll$count(query=req)
db.NYfood.count()

Et pour afficher le nombre de restaurants chinois :

req = '{"cuisine":"Chinese"}'
coll$count(query=req)
db.NYfood.count({"cuisine":"Chinese"})

7.2.2.2. Méthode find

La méthode find() permet, à l’instar du .find en MongoDB (plus de précisions ici), d’interroger une collection en filtrant les documents et les champs. De plus, cette méthode intègre la possibilité de limiter, trier et skiper les documents d’un résultat. Autrement dit, cette méthode comprends en ses arguments le .limit, le .sort et le .skip de MongoDB. Présentons alors les 5 arguments de cette méthode :

  • query : correspond à la requête de filtrage des documents passée en premier agument d’un .find en MongoDB ; ‘{}’ est la valeur par défaut

  • fields : correspond à la requête de filtrage des champs passée en second argument d’un .find en MongoDB

  • sort : correspond à la requête de tri passée argument d’un .sort en MongoDB

  • limit : correspond à la requête de limitation du nombre de documents retournées, passée en argument d’un .limit en MongoDB

  • skip : correspond à la requête de choix d’une ligne de départ des documents retournées, passée en argument d’un .skip en MongoDB

Comme nous avons pu l’énoncer précedémment, cette méthode stocke le résultat dans un dataframe sous R, prenant ainsi de la place en mémoire. Cette prise de place en mémoire peut être problématique, c’est pourquoi nous verrons par la suite l’intérêt de la méthode iterate() et de l’objet Mongo iterator qu’elle renvoie.

Exemple :

Regardons les 5 premières lignes du dataframe contenant les noms des restaurants chinois à Brooklyn triés par ordre alphabétique inverse (i.e, de Z à A) :

data <- coll$find(query = '{"cuisine":"Chinese", "borough":"Brooklyn"}',
                  fields = '{"name": true, "_id":false}',
                  sort = '{"name":-1}',
                  limit = 5)
data
 Found 5 records...
 Imported 5 records. Simplifying into dataframe...
A data.frame: 5 × 1
name
<chr>
1Zhang'S Garden
2Zhang'S Fortune Restaurant
3Zeng'S Restaurant
4Yun Nan Flavour Garden
5Yummy Yummy Kitchen

L’équivalent en MongoDB est le suivant :

data <- coll$find(query = '{"cuisine":"Chinese", "borough":"Brooklyn"}',
                  fields = '{"name": true, "_id":false}',
                  sort = '{"name":-1}',
                  limit = 5)
data
db.NYfood.find({"cuisine":"Chinese", "borough":"Brooklyn"}, 
{"name":true, "_id":false}).sort({"name":-1}).limit(5)

Rappelons que le tri par toute variable autre que l’identifiant peut être relativement lent, surtout lorsque la collection est de taille importante car seul l’identifiant est indexé. En ajoutant un index, le champ est pré-trié et son tri est presque immédiat. Pour ajouter un index avec mongolite, il suffit de le déclarer avec la méthode index(add='{"variable":1}').

Exemple :

Créer un index sur le champs name :

coll$index(add = '{"name" : 1}')
db.NYfood.createIndex({"name": 1}

Il est aussi possible de faire des requêtes textuelles avec mongolite. Toutefois attention, cela se fait obligatoirement à l’aide de l’opérateur $regex. En effet, avec mongolite on ne peut pas faire de requêtes textuelles à l’aide d’expressions régulières car le package permettant de convertir une chaîne de caractères en fichier JSON dans R ne connait pas les expressions régulières. Hormis ce détail, la syntaxe entre simples quotes est la même que pour les requêtes textuelles en MongoDB (plus de précisions ici).

Ainsi, pour afficher les 10 premiers restaurants de Manhattan dont le nom commence par la lettre ‘A’ par exemple, la requête adaptée est :

q = '{"borough": "Manhattan", "name": {"$regex": "^A", "$options":"i"}}'
res <- coll$find(query = q, limit=10)
print(res)
 Found 10 records...
 Imported 10 records. Simplifying into dataframe...
   address.building address.loc.type address.loc.coordinates
1                18            Point     -73.99698, 40.72589
2               146            Point     -73.99730, 40.71887
3               106            Point     -74.00033, 40.72749
4               101            Point     -73.97831, 40.76323
5               807            Point     -73.96643, 40.76417
6              1617            Point     -73.94785, 40.77548
7                              Point     -73.97911, 40.78348
8              1207            Point     -73.95926, 40.80886
9               463            Point       -73.9897, 40.7517
10              762            Point     -73.98820, 40.76414
        address.street address.zipcode   borough   cuisine
1  West Houston Street           10012 Manhattan American 
2      Mulberry Street           10013 Manhattan   Italian
3  West Houston Street           10012 Manhattan   Italian
4       West 55 Street           10019 Manhattan American 
5     Lexington Avenue           10065 Manhattan    Indian
6          York Avenue           10028 Manhattan   Italian
7          W 79 Street           10024 Manhattan American 
8     Amsterdam Avenue           10027 Manhattan American 
9             7 Avenue           10018 Manhattan American 
10            9 Avenue           10019 Manhattan   Mexican
                                                                                                            grades
1                                          1396483200, 1365120000, 1332288000, 1303862400, A, A, A, A, 9, 4, 13, 5
2                     1398988800, 1363219200, 1348617600, 1329264000, 1316044800, A, A, A, A, A, 11, 13, 9, 13, 11
3                     1396224000, 1381190400, 1364515200, 1346803200, 1330905600, A, A, A, A, A, 13, 12, 12, 7, 12
4                                           1413849600, 1392336000, 1357257600, 1326844800, A, A, A, A, 7, 9, 8, 7
5  1414454400, 1404864000, 1389916800, 1369699200, 1332806400, 1320192000, A, B, A, A, A, C, 10, 24, 11, 9, 13, 28
6                    1396396800, 1363132800, 1347408000, 1329782400, 1311120000, A, A, A, B, B, 13, 12, 13, 21, 21
7                     1399507200, 1378944000, 1360627200, 1319500800, 1306368000, A, A, A, A, A, 12, 11, 13, 3, 13
8                     1409702400, 1397779200, 1379980800, 1362960000, 1324339200, A, C, A, A, A, 12, 0, 10, 11, 12
9   1393545600, 1382313600, 1350518400, 1338854400, 1321315200, 1306454400, A, C, A, A, A, A, 12, 9, 13, 12, 9, 10
10                                                            1419811200, 1385942400, 1353888000, A, A, A, 9, 8, 5
                                            name restaurant_id
1                           Angelika Film Center      40362274
2                         Angelo Of Mulberry St.      40365293
3                                       Arturo'S      40365387
4                               Astro Restaurant      40373113
5                                Agra Restaurant      40375376
6                                       Arturo'S      40375616
7  American Museum Of Natural History Food Court      40376515
8            Amsterdam Restaurant & Tapas Lounge      40377111
9                            Andrews Coffee Shop      40378579
10                                 Arriba Arriba      40380457

Notons alors l’équivalent possible en MongoDB :

q = '{"borough": "Manhattan", "name": {"$regex": "^A", "$options":"i"}}'
coll$find(query = q, limit=10)
db.NYfood.find({"borough": "Manhattan", "name": /^A/i }).limit(10)

Remarque

Comme en NoSQL, l’accent circonflèxe sert à préciser que seulement les chaînes commençant par la lettre A seront acceptées. L’opérateur "$options":"i" précise que la casse n’est pas importante (i.e, la chaine “abc” sera jugée acceptable).


Pour ce qui est des requêtes géospatiales, la syntaxe entre simples quotes est la même que pour les requêtes géo-spatiales en MongoDB (plus de précisions ici).

7.2.2.3. Méthode iterate

Nous avons vu précedemment que le renvoi d’une requête dans un dataframe R à l’aide de la méthode find() pouvait être problématique pour de grands volumes de données. La méthode iterate() permet de s’affranchir de ce problème. Celle-ci fonctionne exactement de la même manière que la méthode find() mais renvoie le résultat d’une requête non pas dans un dataframe mais dans un objet propre à mongolite, un Mongo iterator. Cet objet va permettre de ne pas stocker toutes les documents de la requête en mémoire mais de les lire un par un. Pour ce faire, il existe 4 méthodes applicables à un Mongo iterator :

  • batch(n) : permet de parcourir n documents à la fois

  • json() : retourne les résultats de la requête dans un format JSON

  • one() : permet de parcourir un document à la fois

  • page() : retourne les résultats de la requête sous forme de dataframe

Lorsque l’itérateur a épuisé tous les résultats de la collection, il retourne la valeur vide (NULL).

Exemple :

La création d’un itérateur sur l’ensemble de la collection NYfood se fait comme ci-dessous :

it <- coll$iterate()
it
<Mongo iterator> 
 $batch(size = 1000) 
 $json(size = 1000) 
 $one() 
 $page(size = 1000) 

Affichons par exemple les 5 premières lignes :

it$batch(5)
[[1]]
[[1]]$address
[[1]]$address$building
[1] "469"

[[1]]$address$loc
[[1]]$address$loc$type
[1] "Point"

[[1]]$address$loc$coordinates
[[1]]$address$loc$coordinates[[1]]
[1] -73.9617

[[1]]$address$loc$coordinates[[2]]
[1] 40.66294



[[1]]$address$street
[1] "Flatbush Avenue"

[[1]]$address$zipcode
[1] "11225"


[[1]]$borough
[1] "Brooklyn"

[[1]]$cuisine
[1] "Hamburgers"

[[1]]$grades
[[1]]$grades[[1]]
[[1]]$grades[[1]]$date
[1] "2014-12-30 UTC"

[[1]]$grades[[1]]$grade
[1] "A"

[[1]]$grades[[1]]$score
[1] 8


[[1]]$grades[[2]]
[[1]]$grades[[2]]$date
[1] "2014-07-01 UTC"

[[1]]$grades[[2]]$grade
[1] "B"

[[1]]$grades[[2]]$score
[1] 23


[[1]]$grades[[3]]
[[1]]$grades[[3]]$date
[1] "2013-04-30 UTC"

[[1]]$grades[[3]]$grade
[1] "A"

[[1]]$grades[[3]]$score
[1] 12


[[1]]$grades[[4]]
[[1]]$grades[[4]]$date
[1] "2012-05-08 UTC"

[[1]]$grades[[4]]$grade
[1] "A"

[[1]]$grades[[4]]$score
[1] 12



[[1]]$name
[1] "Wendy'S"

[[1]]$restaurant_id
[1] "30112340"


[[2]]
[[2]]$address
[[2]]$address$building
[1] "1007"

[[2]]$address$loc
[[2]]$address$loc$type
[1] "Point"

[[2]]$address$loc$coordinates
[[2]]$address$loc$coordinates[[1]]
[1] -73.85608

[[2]]$address$loc$coordinates[[2]]
[1] 40.84845



[[2]]$address$street
[1] "Morris Park Ave"

[[2]]$address$zipcode
[1] "10462"


[[2]]$borough
[1] "Bronx"

[[2]]$cuisine
[1] "Bakery"

[[2]]$grades
[[2]]$grades[[1]]
[[2]]$grades[[1]]$date
[1] "2014-03-03 UTC"

[[2]]$grades[[1]]$grade
[1] "A"

[[2]]$grades[[1]]$score
[1] 2


[[2]]$grades[[2]]
[[2]]$grades[[2]]$date
[1] "2013-09-11 UTC"

[[2]]$grades[[2]]$grade
[1] "A"

[[2]]$grades[[2]]$score
[1] 6


[[2]]$grades[[3]]
[[2]]$grades[[3]]$date
[1] "2013-01-24 UTC"

[[2]]$grades[[3]]$grade
[1] "A"

[[2]]$grades[[3]]$score
[1] 10


[[2]]$grades[[4]]
[[2]]$grades[[4]]$date
[1] "2011-11-23 UTC"

[[2]]$grades[[4]]$grade
[1] "A"

[[2]]$grades[[4]]$score
[1] 9


[[2]]$grades[[5]]
[[2]]$grades[[5]]$date
[1] "2011-03-10 UTC"

[[2]]$grades[[5]]$grade
[1] "B"

[[2]]$grades[[5]]$score
[1] 14



[[2]]$name
[1] "Morris Park Bake Shop"

[[2]]$restaurant_id
[1] "30075445"


[[3]]
[[3]]$address
[[3]]$address$building
[1] "351"

[[3]]$address$loc
[[3]]$address$loc$type
[1] "Point"

[[3]]$address$loc$coordinates
[[3]]$address$loc$coordinates[[1]]
[1] -73.98514

[[3]]$address$loc$coordinates[[2]]
[1] 40.76769



[[3]]$address$street
[1] "West   57 Street"

[[3]]$address$zipcode
[1] "10019"


[[3]]$borough
[1] "Manhattan"

[[3]]$cuisine
[1] "Irish"

[[3]]$grades
[[3]]$grades[[1]]
[[3]]$grades[[1]]$date
[1] "2014-09-06 UTC"

[[3]]$grades[[1]]$grade
[1] "A"

[[3]]$grades[[1]]$score
[1] 2


[[3]]$grades[[2]]
[[3]]$grades[[2]]$date
[1] "2013-07-22 UTC"

[[3]]$grades[[2]]$grade
[1] "A"

[[3]]$grades[[2]]$score
[1] 11


[[3]]$grades[[3]]
[[3]]$grades[[3]]$date
[1] "2012-07-31 UTC"

[[3]]$grades[[3]]$grade
[1] "A"

[[3]]$grades[[3]]$score
[1] 12


[[3]]$grades[[4]]
[[3]]$grades[[4]]$date
[1] "2011-12-29 UTC"

[[3]]$grades[[4]]$grade
[1] "A"

[[3]]$grades[[4]]$score
[1] 12



[[3]]$name
[1] "Dj Reynolds Pub And Restaurant"

[[3]]$restaurant_id
[1] "30191841"


[[4]]
[[4]]$address
[[4]]$address$building
[1] "2780"

[[4]]$address$loc
[[4]]$address$loc$type
[1] "Point"

[[4]]$address$loc$coordinates
[[4]]$address$loc$coordinates[[1]]
[1] -73.98242

[[4]]$address$loc$coordinates[[2]]
[1] 40.5795



[[4]]$address$street
[1] "Stillwell Avenue"

[[4]]$address$zipcode
[1] "11224"


[[4]]$borough
[1] "Brooklyn"

[[4]]$cuisine
[1] "American "

[[4]]$grades
[[4]]$grades[[1]]
[[4]]$grades[[1]]$date
[1] "2014-06-10 UTC"

[[4]]$grades[[1]]$grade
[1] "A"

[[4]]$grades[[1]]$score
[1] 5


[[4]]$grades[[2]]
[[4]]$grades[[2]]$date
[1] "2013-06-05 UTC"

[[4]]$grades[[2]]$grade
[1] "A"

[[4]]$grades[[2]]$score
[1] 7


[[4]]$grades[[3]]
[[4]]$grades[[3]]$date
[1] "2012-04-13 UTC"

[[4]]$grades[[3]]$grade
[1] "A"

[[4]]$grades[[3]]$score
[1] 12


[[4]]$grades[[4]]
[[4]]$grades[[4]]$date
[1] "2011-10-12 UTC"

[[4]]$grades[[4]]$grade
[1] "A"

[[4]]$grades[[4]]$score
[1] 12



[[4]]$name
[1] "Riviera Caterer"

[[4]]$restaurant_id
[1] "40356018"


[[5]]
[[5]]$address
[[5]]$address$building
[1] "97-22"

[[5]]$address$loc
[[5]]$address$loc$type
[1] "Point"

[[5]]$address$loc$coordinates
[[5]]$address$loc$coordinates[[1]]
[1] -73.86012

[[5]]$address$loc$coordinates[[2]]
[1] 40.73117



[[5]]$address$street
[1] "63 Road"

[[5]]$address$zipcode
[1] "11374"


[[5]]$borough
[1] "Queens"

[[5]]$cuisine
[1] "Jewish/Kosher"

[[5]]$grades
[[5]]$grades[[1]]
[[5]]$grades[[1]]$date
[1] "2014-11-24 UTC"

[[5]]$grades[[1]]$grade
[1] "Z"

[[5]]$grades[[1]]$score
[1] 20


[[5]]$grades[[2]]
[[5]]$grades[[2]]$date
[1] "2013-01-17 UTC"

[[5]]$grades[[2]]$grade
[1] "A"

[[5]]$grades[[2]]$score
[1] 13


[[5]]$grades[[3]]
[[5]]$grades[[3]]$date
[1] "2012-08-02 UTC"

[[5]]$grades[[3]]$grade
[1] "A"

[[5]]$grades[[3]]$score
[1] 13


[[5]]$grades[[4]]
[[5]]$grades[[4]]$date
[1] "2011-12-15 UTC"

[[5]]$grades[[4]]$grade
[1] "B"

[[5]]$grades[[4]]$score
[1] 25



[[5]]$name
[1] "Tov Kosher Kitchen"

[[5]]$restaurant_id
[1] "40356068"

L’affichage avec la méthode batch, qui est essentiellement des listes imbriquées, n’est pas toujours facile à visualiser et nous pouvons décider de stocker le résultat dans un dataframe pour mieux le visualiser ; cela se fait avec la méthode page(). Par exemple, pour stocker les 5 premières lignes dans un dataframe :

df <- it$page(5)
print(df)
  address.building address.loc.type address.loc.coordinates    address.street
1             2206            Point     -74.13773, 40.61196 Victory Boulevard
2             8825            Point     -73.88038, 40.76431 Astoria Boulevard
3             7114            Point     -73.90685, 40.61990          Avenue U
4             6409            Point     -74.00529, 40.62889         11 Avenue
5             1839            Point     -73.94826, 40.64083   Nostrand Avenue
  address.zipcode       borough                         cuisine
1           10314 Staten Island                   Jewish/Kosher
2           11369        Queens                       American 
3           11234      Brooklyn                    Delicatessen
4           11219      Brooklyn                       American 
5           11226      Brooklyn Ice Cream, Gelato, Yogurt, Ices
                                                                                                          grades
1                                       1412553600, 1400544000, 1365033600, 1327363200, A, A, A, A, 9, 12, 12, 9
2                                      1416009600, 1398988800, 1362182400, 1328832000, Z, A, A, A, 38, 10, 7, 13
3 1401321600, 1389657600, 1375488000, 1342569600, 1331251200, 1318550400, A, A, A, A, A, A, 10, 10, 8, 10, 13, 9
4                   1405641600, 1375142400, 1360713600, 1345075200, 1313539200, A, A, A, A, A, 12, 12, 11, 2, 11
5                                        1405296000, 1373414400, 1341964800, 1329955200, A, A, A, A, 12, 8, 5, 8
                         name restaurant_id
1               Kosher Island      40356442
2     Brunos On The Boulevard      40356151
3          Wilken'S Fine Food      40356483
4             Regina Caterers      40356649
5 Taste The Tropics Ice Cream      40356731

7.2.2.4. Méthode distinct

Tout comme le .distinct() en MongoDB (plus de précisions ici), la méthode distinct() nous renvoie les valeurs distinctes d’un champ.

Exemple :

Affichez la liste des notes existant dans la base :

coll$distinct(key = "grades.grade")
  1. 'A'
  2. 'B'
  3. 'C'
  4. 'Not Yet Graded'
  5. 'P'
  6. 'Z'

Nous pouvons également noter l’équivalent en MongoDB :

coll$distinct(key = "grades.grade")
db.NYfood.distinct("grades.grade")

7.2.2.5. Sélectionner par date

Le traitement des dates avec mongolite mérite une attention particulière. En effet, lorsque l’on souhaite intéragir avec une collection sur un champ de type date, la syntaxe sera relativement différente de celle que l’on peut utiliser en MongoDB (plus de précisions ici) ou avec pymongo. En raison de la classe d’une requête en mongolite (chaîne de caractère), on ne pourra pas utiliser d’objet R spécifique aux dates (comme les datetimes en python par exemple). Ainsi, toutes les dates définies dans une requête en mongolite devront être spécifiées dans un format purement JSON intégrant un opérateur $date et une syntaxe UTC. Concrètement, ce format sera du type :

{ "$date" : "AAAA-MM-JJThh:mm:ssZ" }

Exemple :

Prenons un exemple en affichant la liste des 10 premiers restaurants ayant eu au moins une note postérieure au 20 janvier 2015 :

q <- '{"grades.date": {"$gte": {"$date": "2015-01-20T00:00:00Z"}}}'
data <- coll$find(q, limit=10)
print(data)
 Found 10 records...
 Imported 10 records. Simplifying into dataframe...
   address.building address.loc.type address.loc.coordinates   address.street
1               730            Point     -73.89955, 40.81644     Kelly Street
2               968            Point     -92.72302, 41.74614         6 Avenue
3              1285            Point     -73.92275, 40.83945  Shakespeare Ave
4               416            Point     -73.95136, 40.77249 East   80 Street
5               725            Point     -74.01381, 40.63368        65 Street
6           229-231            Point     -74.00155, 40.70780     Front Street
7            263-15            Point     -73.70923, 40.74883   Union Turnpike
8               353            Point     -73.98533, 40.76794 West   57 Street
9               413            Point     -73.86476, 40.90232 East  241 Street
10              590            Point     -73.92636, 40.81950  Grand Concourse
   address.zipcode   borough   cuisine
1            10455     Bronx American 
2            10018 Manhattan American 
3            10452     Bronx   Spanish
4            10075 Manhattan American 
5            11220  Brooklyn     Other
6            10038 Manhattan American 
7            11004    Queens American 
8            10019 Manhattan American 
9            10470     Bronx American 
10           10451     Bronx   African
                                                                                                                        grades
1                                                                                                1421712000, Not Yet Graded, 4
2  1421712000, 1396915200, 1380585600, 1360713600, 1345075200, 1330041600, Not Yet Graded, A, A, B, A, A, 17, 9, 13, 27, 9, 11
3                                                                                               1421712000, Not Yet Graded, 12
4                                                                                                1421712000, Not Yet Graded, 9
5                                                                                               1421712000, Not Yet Graded, NA
6                                                                                                1421712000, Not Yet Graded, 2
7                                                                                                1421712000, Not Yet Graded, 7
8                                                                                               1421712000, Not Yet Graded, 24
9                                          1421712000, 1383523200, 1367280000, 1318982400, Not Yet Graded, A, A, A, 4, 2, 9, 9
10                                                                                              1421712000, Not Yet Graded, 21
                                                 name restaurant_id
1                                    Ambassador Diner      40403946
2                                Herald Square Market      40984099
3                         El Nuevo Encuentro Bar Corp      41185949
4                    Caedmon School Winstead Caterers      41243754
5                               Swedish Football Club      41278206
6                                                Onda      41338998
7                                          Saint Anns      41433190
8                                          Good Units      41514954
9  Homestyle Food Services (St. Barnabas High School)      41519329
10                              Capitol International      41650465

Nous pouvons noter l’équivalent en MongoDB :

q <- '{"grades.date": {"$gte": {"$date": "2015-01-20T00:00:00Z"}}}'
data <- coll$find(q, limit=10)
data
date = new Date("2015-01-20")
db.NYfood.find({"grades.date":{$gte: date}}).limit(10)

7.2.3. Aggrégations

Nous nous proposons dans ce paragraphe de traiter de la méthode aggregate() de mongolite qui permet d’éxécuter, à l’instar de d’un .aggregate en MongoDB, une pipeline d’aggrégation qui n’est rien d’autre qu’une succession de plusieurs étapes d’aggrégation. La méthode aggregate() prend comme argument une liste de dictionnaires que l’on met entre simples quotes. La syntaxe a utiliser pour cette liste de dictionnaires est la même que pour le .aggregate de MongoDB (nous vous renvoyons au chapitre sur les requêtes d’aggrégations pour plus de précisions). Tout comme la méthode find(), la méthode aggregate renvoie un dataframe.

Exemple :

Prenons un exemple : dans la collection NYfood, le pipeline ci-dessous retourne le nombre de restaurants par arrondissement (borough).

req = '[{"$group":{"_id":"$borough","nb_restos":{"$sum":1}}}]' 
df <- coll$aggregate(pipeline=req) 
df 
 Found 6 records...
 Imported 6 records. Simplifying into dataframe...
A data.frame: 6 × 2
_idnb_restos
<chr><int>
1Missing 51
2Manhattan 10258
3Queens 5656
4Brooklyn 6085
5Bronx 2338
6Staten Island 969

Nous pouvons noter l’équivalent en MongoDB :

req = '[{"$group":{"_id":"$borough","nb_restos":{"$sum":1}}}]' 
df <- coll$aggregate(pipeline=req) 
df 
db.NYfood.aggregate([{$group:{"_id":"$borough", "nb_restos":{"$sum":1}}}])

Il se peut que le dataframe à retourner soit de taille importante (ce n’est pas le cas ici). Pour éviter tout problème de saturation de mémoire, il est préférable d’effectuer l’aggrégation avec l’option iterate=TRUE. Ainsi, la méthode aggregate() renvoie comme pour la méthode iterate() un Mongo iterator. Toutes les méthodes applicables à un Mongo iterator présentées précédemment s’appliqueront alors à l’objet renvoyé par la méthode aggregate(). Pour reprendre l’exemple ci-dessus :

req = '[{"$group":{"_id":"$borough","nb_restos":{"$sum":1}}}]' 
it <- coll$aggregate(pipeline=req, iterate=TRUE) 
it$page(2) 
A data.frame: 2 × 2
_idnb_restos
<chr><int>
1Manhattan10258
2Missing 51

7.2.4. Manipulation de données

7.2.4.1. Méthode insert

La méthode insert() permet, à l’instar du .insert en MongoDB (plus de précisions ici), d’ajouter des données à une collection. La méthode la plus simple, est d’insérer des données à partir d’un data frame R. Les colonnes du data frame seront automatiquement transformées en clées d’enregistrement JSON.

test <- mongo()
test$drop()
test$insert(iris)
List of 5
 $ nInserted  : num 150
 $ nMatched   : num 0
 $ nRemoved   : num 0
 $ nUpserted  : num 0
 $ writeErrors: list()

Remarque

En pratique, c’est l’inverse de mongo$find() qui converti la collection en Data Frame.

test$find(limit = 3)
A data.frame: 3 × 5
Sepal_LengthSepal_WidthPetal_LengthPetal_WidthSpecies
<dbl><dbl><dbl><dbl><chr>
15.13.51.40.2setosa
24.93.01.40.2setosa
34.73.21.30.2setosa

Il est également possible d’insérer directement des données à partir d’une chaîne de caractère JSON. Cette méthode nécessite un vecteur de caractères où chaque élément est une chaîne JSON valide.
A noter qu’ici la méthode insert() crée la collection “individus” car cette dernière n’existe pas.

individus <- mongo("individus")
str <- c('{"prenom" : "yolan"}' , '{"prenom": "paul", "age" : 22}', '{"prenom": "faisal"}')
individus$insert(str)
List of 6
 $ nInserted  : int 3
 $ nMatched   : int 0
 $ nModified  : int 0
 $ nRemoved   : int 0
 $ nUpserted  : int 0
 $ writeErrors: list()
individus$find(query = '{}', fields = '{}')
A data.frame: 3 × 3
_idprenomage
<chr><chr><int>
162309afeb1ebdb7a4570ec59yolan NA
262309afeb1ebdb7a4570ec5apaul 22
362309afeb1ebdb7a4570ec5bfaisalNA

Nous pouvons également noter l’équivalent en MongoDB :

individus <- mongo("individus")
individus$insert(c('{"prenom" : "yolan"}' , '{"prenom": "paul", "age" : 22}', '{"prenom": "faisal"}'))
db.createCollection("individus")
db.individus.insert([
{"prenom" : "yolan"}',
{"prenom": "paul", "age" : 22},
{"prenom": "faisal"}
])

7.2.4.2. Méthode remove

La même syntaxe que nous utilisons dans find() pour sélectionner les enregistrements à lire, peut également être utilisée pour sélectionner les enregistrements à supprimer :

test$count()
150
test$remove('{"Species" : "setosa"}')
test$count()
100

Nous pouvons noter l’équivalent en MongoDB :

test$remove('{"Species" : "setosa"}')
db.test.remove(
{"Species" : "setosa"}
)

Utilisez l’option just_one pour supprimer un seul enregistrement :

test$remove('{"Sepal_Length" : {"$lte" : 5}}', just_one = TRUE)
test$count()
99

Pour supprimer tous les documents de la collection (mais pas la collection elle-même) :

test$remove('{}')
test$count()
0

Nous pouvons noter l’équivalent en MongoDB :

test$remove('{}')
db.test.remove({})

La méthode drop() supprime une collection entière. Cela inclut toutes les documents, ainsi que les métadonnées telles que les index de la collection.

test$drop()

Nous pouvons noter l’équivalent en MongoDB :

test$drop()
db.test.drop()

7.2.4.3. Méthodes update/upsert

Pour modifier des enregistrements existants, utilisez l’opérateur update() :

Modification d’un document :

individus$find()
A data.frame: 3 × 2
prenomage
<chr><int>
1yolan NA
2paul 22
3faisalNA
individus$update('{"prenom":"yolan"}', '{"$set":{"age": 22}}')
List of 3
 $ modifiedCount: int 1
 $ matchedCount : int 1
 $ upsertedCount: int 0
individus$find()
A data.frame: 3 × 2
prenomage
<chr><int>
1yolan 22
2paul 22
3faisalNA

Nous pouvons noter l’équivalent en MongoDB :

individus$update('{"prenom":"yolan"}', '{"$set":{"age": 22}}')
db.individus.update(
{"prenom" : "yolan"},
{$set: {"age" : 22}}
)

Mise à jour de plusieurs documents :

Par défaut, la méthode update() met à jour un seul document. Pour mettre à jour plusieurs documents, utilisez l’option multi de la méthode update().

individus$update('{}', '{"$set":{"booleen_age": false}}', multiple = TRUE)
List of 3
 $ modifiedCount: int 3
 $ matchedCount : int 3
 $ upsertedCount: int 0
individus$update('{"age" : {"$gte" : 0}}', '{"$set":{"booleen_age": true}}', multiple = TRUE)
List of 3
 $ modifiedCount: int 2
 $ matchedCount : int 2
 $ upsertedCount: int 0
individus$find()
A data.frame: 3 × 3
prenomagebooleen_age
<chr><int><lgl>
1yolan 22 TRUE
2paul 22 TRUE
3faisalNAFALSE

Nous pouvons également noter l’équivalent en MongoDB :

individus$update('{}', '{"$set":{"booleen_age": false}}', multiple = TRUE)

individus$update('{"age" : {"$gte" : 0}}', '{"$set":{"booleen_age": true}}', multiple = TRUE)
db.individus.update(
{},
{$set: {"booleen_age": false}},
{multi: true}
)

db.individus.update(
{"age" : {"$gte" : 0}},
{$set: {"booleen_age": true}},
{multi: true}
)

Modification avec création d’un document si besoin :

Si aucun document ne correspond à la condition de mise à jour, le comportement par défaut de la méthode de mise à jour est de ne rien faire. En spécifiant l’option upsert à true, l’opération de mise à jour met à jour le ou les documents correspondants ou insère un nouveau document si aucun document correspondant n’existe.

individus$update('{"prenom":"malo"}', '{"$set":{"age": 22}}', upsert = TRUE)
List of 4
 $ modifiedCount: int 0
 $ matchedCount : int 0
 $ upsertedCount: int 1
 $ upsertedId   : chr "62309afee5a11a212c4cf65a"
individus$find()
A data.frame: 4 × 3
prenomagebooleen_age
<chr><int><lgl>
1yolan 22 TRUE
2paul 22 TRUE
3faisalNAFALSE
4malo 22 NA

Nous pouvons également noter l’équivalent en MongoDB :

individus$update('{"prenom":"malo"}', '{"$set":{"age": 22}}', upsert = TRUE)
db.individus.update(
{"prenom":"malo"},
{$set: {"age": 22}},
{upsert: true}
)

7.2.5. Import et export de données

7.2.5.1. La méthode export/import pour JSON

Le format par défaut JSON est celui d’une ligne par document.

individus$export(stdout())
{ "_id" : { "$oid" : "62309afeb1ebdb7a4570ec59" }, "prenom" : "yolan", "age" : 22, "booleen_age" : true }
{ "_id" : { "$oid" : "62309afeb1ebdb7a4570ec5a" }, "prenom" : "paul", "age" : 22, "booleen_age" : true }
{ "_id" : { "$oid" : "62309afeb1ebdb7a4570ec5b" }, "prenom" : "faisal", "booleen_age" : false }
{ "_id" : { "$oid" : "62309afee5a11a212c4cf65a" }, "prenom" : "malo", "age" : 22 }

En temps normal on exporte vers un fichier que l’on précise avec file() :

individus$export(file("individus.json"))

On peut faire l’essai de supprimer totalement la collection et de l’importer ensuite :

individus$drop()
individus$count()
0
individus$import(file("individus.json"))
individus$count()
4

7.2.5.2. Autres formats d’export (jsonlite/bjson/…)

Pour ces autres formats ou package concernant les méthodes import() et export(), nous vous renvoyons à la page de Jeroen Ooms.

7.2.6. Exercices

7.2.6.1. Consignes

Exercice 1 :

  1. A l’aide d’une requête d’aggrégation, récupérer le nombre de restaurants par quatier dans la collection NYfood.

  2. Réaliser un barplot pour visualiser le résultat.

Exercice 2 :

Cet exercice reprend l’exemple de carthographie avec leaflet de François-Xavier Jollois, disponible en cliquant ici.

  1. A l’aide d’une requête d’aggrégation, récupérer le nom, le quartier, la longitude et la latitude des restaurant new-yorkais de la collection NYfood.

  2. Afficher les différents restaurants sur la carte du monde. Que constatez vous ?

  3. Réaliser une carte des restaurants new-yorkais en ajoutant une couleur en fonction du quartier (coordonnées de New-York : long: -73.9, lat = 40.7).

7.2.6.2. Correction

Exercice 1 :

Question 1 :

req = '[{"$group":{"_id":"$borough","nb_restos":{"$sum":1}}}]' 
df <- coll$aggregate(pipeline=req) 
df

Question 2 :

library(tidyverse)

df %>%
  rename(Borough=`_id`,Nombre=nb_restos) %>%
  ggplot(aes(x=Borough, y=Nombre)) +
  geom_bar(stat="identity",aes(fill=Borough)) +
  geom_text(aes(label=Nombre), vjust=1.6, color="black", size=3.5)

Exercice 2 :

Question 1 :

restos.coord = coll$aggregate(
  '[
    { "$project": { 
        "name": 1, 
        "borough": 1, 
        "lng": { "$arrayElemAt": ["$address.loc.coordinates", 0]}, 
        "lat": { "$arrayElemAt": ["$address.loc.coordinates", 1]} 
    }}
]')

head(restos.coord)

Question 2 :

library(leaflet)

leaflet(restos.coord) %>%
  addTiles() %>%
  addCircles(lng = ~lng, lat = ~lat)

Question 3 :

pal = colorFactor("Accent", restos.coord$borough)
leaflet(restos.coord) %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lng  = -73.9,
          lat  =  40.7,
          zoom =  10) %>%
  addCircles(lng = ~lng, lat = ~lat, color = ~pal(borough)) %>%
  addLegend(pal = pal, values = ~borough, opacity = 1, 
            title = "Quartier")