====== Ajout d'une base de données ======
===== Idée =====
La première application consiste à réaliser un aperçu de cartographie rapide en se basant sur les 500 sites disponibles dans la partie "top sites" de l'interface web. En revanche, les données capitalisées sont disponibles uniquement sur la base d'un fichier CSV, qui est peu pratique à utiliser. De plus, mis à part la problématique du fichier, on a également pu remarquer précédemment que les données sont brutes et nécessitent un filtrage pour être pleinement exploitables. L'utilisation d'une base de données à ce stade de l'étude devenait donc nécessaire afin d'organiser les données récupérées de façon plus efficace.
===== Sauvegarde des données =====
Pour capitaliser les données du CSV de manière plus efficace, le choix s'est porté sur [[https://www.mongodb.com/fr|MongoDB]]. Ce système de gestion de base de données orientée documents est très pratique pour la sauvegarde et le traitement de données hétérogènes ou non-encore hiérarchisées. De plus, son fonctionnement sur la base d'objets JSON permet de pouvoir exploiter la base de données très facilement depuis du code JavaScript.
Toutes les données sont sauvegardées dans une unique collection afin de faciliter le traitement.
Gestion BDD (MongoDB) > BDD (webmap) > Collection (websites)
En effet, il n'était pas nécessaire d'utiliser un modèle relationnel vu la simplicité des données à sauvegarder : j'ai alors choisi d'ajouter un tableau d'objets imbriqués dans le schéma de stockage, afin de stocker les upstreams et downstreams pour un site donné. Les objets de ces tableaux référencent d'autres objets de la collection "websites". Le schéma de stockage est décrit dans le code JavaScript ci-dessous.
const mongoose = require('mongoose');
require('mongoose-double')(mongoose);
let websiteSchema = new mongoose.Schema({
url: String,
globalRank: Number,
country: String,
countryRank: Number,
bounceRate: mongoose.Schema.Types.Double,
dailyPageviewsPerVisitor: mongoose.Schema.Types.Double,
trafficFromSearchEngines: mongoose.Schema.Types.Double,
totalSitesLinkingIn: Number,
upstreams: [{
url: String,
percentOfUniqueVisits: mongoose.Schema.Types.Double
}],
downstreams: [{
url: String,
percentOfUniqueVisits: mongoose.Schema.Types.Double
}]
});
let Website = mongoose.model('Website', websiteSchema);
module.exports = Website;
Le morceau de programme qui s'occupe de réaliser le transfert de données vers MongoDB fonctionne sur la même base que celui utilisé pour streamer le CSV vers Gephi. L'algorithme se déroule selon les étapes suivantes :
- Ouverture du fichier CSV à l'aide d'un objet de type stream (pour éviter des problèmes de mémoire dans le cas où le fichier est très imposant)
- La lecture du stream ligne par ligne est envoyé dans un second objet stream qui s'occupe de parser le formatage CSV puis renvoi, pour chaque ligne, un tableau à une dimension avec les données.
- Chaque ligne est ensuite traité par une fonction asynchrone qui se décompose en trois étapes :
- On créé un objet représentant le site (ou nœud) de la ligne en cours de traitement selon le schéma défini plus tôt
- On vérifie que ses attributs, une fois castés dans le bon type soient complets, et que l'objet ne contienne pas de "NaN" ou de "null"
- Si tel est le cas, on vérifie que l'objet n'existe pas encore et on l'ajoute à la base de données MongoDB, en prenant soin de le garder en mémoire
- Sur la ligne parsée, il y a aussi un lien upstream ou un lien dowstream qu'on s'occupe d'ajouter dans le tableau correspondant de l'objet précédemment gardé en mémoire, puis on le sauvegarde à nouveau dans la base de données MongoDB
- Enfin, on effectue une dernière requête pour ajouter aussi une entrée à la collection correspondant au lien upstream ou dowstream ajouté précédemment
A la fin, la collection possède un document par site internet (qu'il fasse parti du top 500 ou qu'il soit un upstream ou dowstream). Chaque document possède (ou non) des données additionnelles décrites précédemment que l'on retrouve par exemple sur le document associé au site "bloomberg.com" présenté ci-dessous (les tableaux upstreams et downstreams ont été volontairement réduits à deux entrées).
{
"_id": "5a9d34e788263f6974f2e2ea",
"url": "bloomberg.com",
"__v": 20,
"bounceRate": 0.656,
"country": "united states",
"countryRank": 155,
"dailyPageviewsPerVisitor": 1.68,
"downstreams": [{
"url": "drudgereport.com",
"percentOfUniqueVisits": 0.014,
"_id": "5a9d34f188263f6974f33257"
}, {
"url": "google.co.in",
"percentOfUniqueVisits": 0.019,
"_id": "5a9d34fa88263f6974f3783f"
}, {...}],
"globalRank": 434,
"totalSitesLinkingIn": 97935,
"trafficFromSearchEngines": 0.28,
"upstreams": [{
"url": "google.co.in",
"percentOfUniqueVisits": 0.021,
"_id": "5a9d34f188263f6974f33261"
}, {
"url": "yahoo.com",
"percentOfUniqueVisits": 0.02,
"_id": "5a9d350188263f6974f39087"
}, {...}]
}
===== Transfert des données dans Gephi =====
Pour réaliser la cartographie avec le logiciel Gephi des données disponibles dans la base MongoDB, j'ai à nouveau utilisé le [[https://github.com/gephi/gephi/wiki/GraphStreaming|plugin Streaming API]] afin de garder mes fonctions helpers écrites pour le premier concept d'application de stream depuis le CSV.
L'algorithme mis en place ici est bien plus simple que précédemment car les données sont déjà en place, filtrées et correctement formatées dans la collection MongoDB. Ainsi, le stream vers Gephi se déroule de la manière suivante à travers une fonction asynchrone :
- On réalise une unique requête permettant de récupérer l'ensemble des documents de la collection
- Une première boucle est effectuée sur les documents de la collection afin d'ajouter tous les nœuds du graphe dans Gephi.
- Une seconde boucle est effectuée sur les documents afin de réaliser deux boucles :
- Une boucle sur les upstreams afin d'ajouter les liens correspondants sur le graphe
- Une boucle sur les downstreams afin d'ajouter les liens correspondants sur le graphe
Attention toutefois, Gephi utilise un type strict pour la déclaration des données dans ses colonnes et cela impose une limitation importante, qui est **non contournable avec le JavaScript** ! En effet, en JavaScript les nombres de type Float ou Int sont tous de type Number et un nombre comme "3.00" sera toujours ramené à "3". Or cette simplification du fonctionnement du langage est problématique pour la colonne //dailyPageViewsPerVisitors// : la plupart des données sont des float, mais la valeur est entière sur quelques entrées, qui au final seront ignorées lors du stream... Pour contourner cette limitation, cette colonne est castée sous forme de String avant d'être streamée vers Gephi.
Après le stream, on se retrouve avec un graphe basique noir et blanc{{ :txs:map18p:post_stream.png?150|Graphe brut}}, spatialisé de manière aléatoire, comprenant (pour un scrap le 20/02/2018) un ensemble de 1290 nœuds et 9463 liens. Ce graphe est identique qu'à celui réalisé à partir du CSV en apparence, mais possède l'avantage d'avoir des données de meilleure qualité et est plus facilement exploitable dans Gephi : les colonnes sont pour les plupart de type "nombres" et sont utilisables à travers les filtres (sauf pour la colonne //dailyPageViewsPerVisitors// à cause de la limitation expliquée précédemment). Une possible solution serait de réécrire le programme avec un langage tel que le C++ qui possède un typage plus strict.