Archives pour la catégorie NoSQL

MongoDB : un exemple de map-reduce

Le concept de map-reduce (on lit parfois les termes de paradigme de programmation ou de design pattern associés à map-reduce) n’a rien de nouveau : il s’inspire grandement des principes de base de la programmation fonctionnelle (i.e, par usage de fonctions mathématiques).

Comme son nom l’indique, map-reduce se compose de deux fonctions :

  • map, qui passe à reduce une clé et des données à traiter
  • reduce qui, pour chaque clé donnée par map, va opérer une réduction sur les données en provenance de map selon une logique que vous allez devoir écrire.

Map-reduce a connu un renouveau avec l’apparition des data stores de très grande capacité (on parle ici en pétaoctets) sur le web et doit en grande partie son retour sous les feux de la rampe à Google qui s’en est servi (et s’en sert toujours) pour distribuer/paralléliser les traitements, sur des clusters de machines. Le principe est simple: découper un gros problème en plusieurs petits, résolus par distribution à des nœuds fils (réducteurs) qui ensuite remontent le résultat de ces opérations aux nœuds de niveau supérieur, et ce récursivement jusqu’à remonter au la racine de l’arbre, qui a initié le calcul et en récupère donc le résultat final.

Ici nous n’aurons par cette notion de master nodes / worker nodes car nous travaillons sur une seule instance de MongoDB, située sur un seul serveur (ma machine) et non sur une architecture basée sur des shards ou des replica sets.

Image provenant de http://www.pal-blog.de/

Image provenant de PAL-Blog [http://www.pal-blog.de]


Map traverse tous les documents de votre collection MongoDB et va pousser des infos à reduce selon une clé définie. Passons à la pratique sans plus tarder…

Notre collection de test

Nous allons constituer une collection dénommée commandes et qui va contenir les commandes passées sur notre site web. Ces commandes seront évidemment des documents dont la structure est la suivante :

  • userid : l’identifiant de l’utilisateur à l’origine de la commande
  • date : la date de passage (ou de validation, pourquoi pas) de la commande
  • codepost : le code postal de la ville de résidence de l’utilisateur
  • articles : la liste des articles commandés (il y en a autant que souhaité)
  • totalttc : le prix TTC de la totalité de la commande
  • totalht : le prix HT de la totalité de la commande
  • tva : le montant de TVA applicable à la commande
sebastien.ferrandez@sebastien:~$ mongo
 MongoDB shell version: 2.0.6
 connecting to: test
>db.createCollection('commandes');
 { "ok" : 1 }
>show collections;
 commandes
 system.indexes

>db.commandes.insert({userid: 54845, date: new Date("Apr 28, 2013"), codepost:13100, articles:[{id:1,nom:'livre',prix:29.90}, {id:9, nom:'eponge', prix:2.90}], totalttc:32.80, tva:5.38, totalht:27.42});

>db.commandes.insert({userid: 54846, date: new Date("Apr 29, 2013"),codepost:13290, articles:[{id:45,nom:'robinet',prix:69.90}, {id:9, nom:'laitx6', prix:9.90}], totalttc:79.80, tva:13.08, totalht:66.72});

>db.commandes.insert({userid: 54847, date: new Date("Apr 30, 2013"),codepost:13008, articles:[{id:76,nom:'clavier',prix:49.90}, {id:2, nom:'fromage', prix:1.50}], totalttc:51.40, tva:8.42, totalht:42.98});

>db.commandes.insert({userid: 54848, date: new Date("Apr 28, 2013"),codepost:13600, articles:[{id:2987,nom:'presse',prix:2}], totalttc:2, tva:0.33, totalht:1.67});

>db.commandes.insert({userid: 54848, date: new Date("Apr 29, 2013"),codepost:13600, articles:[{id:2988,nom:'presse',prix:5.90}], totalttc:5.90, tva:0.97, totalht:4.93});

>db.commandes.insert({userid: 54848, date: new Date("Apr 30, 2013"),codepost:13600, articles:[{id:3989,nom:'presse',prix:1.20}], totalttc:1.20, tva:0.20, totalht:1});

>db.commandes.insert({userid: 54847, date: new Date("Apr 25, 2013"),codepost:13008, articles:[{id:2987,nom:'presse',prix:2}], totalttc:2, tva:0.33, totalht:1.67});

Structure du JSON

Voici la structure de l’un de nos documents :

{
    "_id": ObjectId("517fb463b53bb7169584f3c7"),
    "userid": 54847,
    "date": ISODate("2013-04-29T22:00:00Z"),
    "codepost": 13008,
    "articles": [
        {
            "id": 76,
            "nom": "clavier",
            "prix": 49.9
        },
        {
            "id": 2,
            "nom": "fromage",
            "prix": 1.5
        }
    ],
    "totalttc": 51.4,
    "tva": 8.42,
    "totalht": 42.98
}

Le JavaScript de notre map-reduce

Maintenant, nous allons écrire le map-reduce qui va opérer sur ces données; notre but est d’afficher, par code postal, le chiffre d’affaire généré par notre site en ligne. Créons donc un fichier mapred.js qui contiendra notre MR :

use commandes;

map = function() {
	emit(this['codepost'], {totalttc: this['totalttc']});
	};

reduce = function(cle, valeur) {
			var s = {somme : 0};
			valeur.forEach(function(article) {
				s.somme += article.totalttc;
			});
			return s;
		};

db.commandes.mapReduce(map, reduce, {out:'total_cmdes_par_ville'});

db.total_cmdes_par_ville.find();

And…action !

Nous injectons notre fichier JS dans Mongo

sebastien.ferrandez@sebastien:~$ mongo < /tmp/mapred.js

Et voici le résultat produit :

MongoDB shell version: 2.0.6
connecting to: test
function () {
    emit(this.codepost, {totalttc:this.totalttc});
}
function (cle, valeur) {
    var s = {somme:0};
    valeur.forEach(function (article) {s.somme += article.totalttc;});
    return s;
}
{
        "result" : "total_cmdes_par_ville",
        "timeMillis" : 33,
        "counts" : {
                "input" : 7,
                "emit" : 7,
                "reduce" : 2,
                "output" : 4
        },
        "ok" : 1,
}
{ "_id" : 13008, "value" : { "somme" : 53.4 } }
{ "_id" : 13100, "value" : { "totalttc" : 32.8 } }
{ "_id" : 13290, "value" : { "totalttc" : 79.8 } }
{ "_id" : 13600, "value" : { "somme" : 9.1 } }
bye

La fonction map va émettre (emit) des paires clé/valeur sur lesquelles reduce va travailler. Pour faire simple, reduce va faire la somme de tous les montants TTC des commandes pour un code postal donné. Une collection est créée pour abriter le résultat de l’exécution de map-reduce : nous avons décider de la nommer total_cmdes_par_ville. Elle est persistante, elle ne sera pas détruite une fois que vous vous déconnectez du shell MongoDB.

MongoDB : les bases pour bien débuter (3/3)

Revenons sur la collection MongoDB qui nous sert d’exemple depuis le début et effectuons quelques opérations de mise à jour élémentaires. Voici donc pour rappel à quoi ressemble notre collection :

sebastien.ferrandez@sebastien:~$ mongo
MongoDB shell version: 2.0.6
connecting to: test
> show dbs;
admin   0.203125GB
gens    0.203125GB
local   (empty)
> use gens;
switched to db gens
> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

mongo-db-logo

Opérations de mise à jour

Avec inc

Supposons qu’il nous faille mettre à jour l’âge d’une personne : aujourd’hui c’est l’anniversaire de Maïa, elle a 7 ans ! Voici plusieurs façons de faire : tout d’abord nous incrémentons la clé age de nos documents pour lesquels age est supérieur à 5.

db.gens.update({ age:{$gt: 5}}, {$inc: {age: 1}});

Avec set

Nous ciblons uniquement le prénom

db.gens.update({ prenom: "maïa"}, {$set: {age: 7}});

Nous faisons un mélange des deux précédentes requêtes :

db.gens.update({ age:{$gt: 5}}, {$set: {age: 7}});

Opérations de suppression

Suppression par ObjectId

> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517e33cf4937f8d068f9e9aa"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 7 }
> db.gens.remove( {"_id": ObjectId("517e33cf4937f8d068f9e9aa")});
> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }

Suppression par champ quelconque

Supprimons tous les gens qui s’appellent « ferrandez » :

> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
> db.gens.remove({nom: 'ferrandez'});
> db.gens.find();

Notez que cette fois-ci je n’ai pas mis de guillemets autour de la clé (nom) et j’ai mis des apostrophes autour du nom pour que vous voyez bien qu’il n’est pas obligatoire de mettre tout ça entre guillemets systématiquement !

Suppression de la première occurrence seulement

En mettant justOne à 1 (le premier paramètre), seul le premier document satisfaisant aux critères sera supprimé :

> db.gens.find();
{ "_id" : ObjectId("517e36c14937f8d068f9e9ab"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 7 }
{ "_id" : ObjectId("517e36e54937f8d068f9e9ac"), "nom" : "ferrandez", "prenom" : "sébastien" }
> db.gens.remove({nom: 'ferrandez'}, 1);
> db.gens.find();
{ "_id" : ObjectId("517e36e54937f8d068f9e9ac"), "nom" : "ferrandez", "prenom" : "sébastien" }

Suppression de l’intégralité des documents d’une collection

> db.gens.remove();

Voilà ! Pour ceux d’entre vous qui ont déjà des connaissances en langage SQL, vous avez les bases pour débuter avec MongoDB. Nous allons bientôt rentrer en détail dans le fonctionnement de MongoDB et en particulier nous attarder sur l’aspect dénormalisation !

MongoDB : les bases pour bien débuter (2/3)

Dans le billet précédent, nous avons vu quelques unes des requêtes DDL nous permettant de manipuler les structures élémentaires de MongoDB, comme les bases de données, les collections et les documents. L’heure est maintenant venue de requêter sur nos données !

Le kit MongoDB, qu'on reçoit à toutes les confs (source http://xenodesystems.blogspot.mx/)

Le kit MongoDB, qu’on reçoit à toutes les confs (source http://xenodesystems.blogspot.mx/)

Notre jeu de données de démarrage

Il est très simple :

> db.gens.insert({"nom":"ferrandez"});
> db.gens.insert({"nom":"ferrandez", prenom: "léo"});
> db.gens.insert({"nom":"ferrandez", prenom: "maïa", age:6});
> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

MongoDB est schema-less (prononcez « skimaless »), nous pouvons donc très bien insérer des documents qui n’ont pas la même structure, regardez bien nos trois documents, ils ne comportent pas le même nombre de champs !

Poser des index

Vous le savez, pour que vos requêtes aient un temps d’exécution satisfaisant (surtout si elle sont appelées fréquemment), il est absolument nécessaire qu’elles s’appuient sur des index. MongoDB dispose d’index au niveau des collections (l’équivalent des tables en SQL). Le champ _id est indexé par défaut et il est impossible de le supprimer.

Sur un seul champ

Pour poser un index sur le champ nom dans l’ordre ascendant (1, ce sera -1 pour DESC) :

db.gens.ensureIndex({nom:1});

Les index composés

Pour poser un index sur les champs nom ET prenom (ASC tous les deux) :

db.gens.ensureIndex({nom:1, prenom:1});

Comme en SQL, si vos requêtes se basent sur prenom, l’index ne sera pas utilisé !
Si vous requêtez sur le nom ou sur le nom ET le prénom, celui-ci sera sollicité.

Les index uniques

Rien de plus simple :

db.gens.ensureIndex({nom:1}, {unique: true});

Ici en réalité la pose de cet index va échouer car notre champ nom contient les mêmes valeurs :

> db.gens.ensureIndex({nom:1}, {unique: true});
E11000 duplicate key error index: gens.gens.$nom_1  dup key: { : "ferrandez" }

Voir les index d’une collection

db.gens.getIndexes();
[
        {
                "v" : 1,
                "key" : {
                        "_id" : 1
                },
                "ns" : "gens.gens",
                "name" : "_id_"
        },
        {
                "v" : 1,
                "key" : {
                        "nom" : 1
                },
                "ns" : "gens.gens",
                "name" : "nom_1"
        },
        {
                "v" : 1,
                "key" : {
                        "nom" : 1,
                        "prenom" : 1
                },
                "ns" : "gens.gens",
                "name" : "nom_1_prenom_1"
        }
]

Supprimer un index

On va supprimer l’index nommé ‘nom_1_prenom_1’ :

> db.gens.dropIndex('nom_1_prenom_1')
{ "nIndexesWas" : 3, "ok" : 1 }
> db.gens.getIndexes();
[
        {
                "v" : 1,
                "key" : {
                        "_id" : 1
                },
                "ns" : "gens.gens",
                "name" : "_id_"
        },
        {
                "v" : 1,
                "key" : {
                        "nom" : 1
                },
                "ns" : "gens.gens",
                "name" : "nom_1"
        }
]

Nous voyons qu’il a été effectivement supprimé (et nous voyons aussi au passage qu’un index est posé par défaut par MongoDB sur _id).

Les requêtes

Sur une seule valeur

Cherchons toutes les personnes qui ont 6 ans :

> db.gens.find({age:6});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

Cherchons toutes les personnes qui s’appellent « ferrandez » :

> db.gens.find({nom:"ferrandez"});
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

Sur deux valeurs

Cherchons toutes les personnes qui ont 6 ans et qui s’appellent « ferrandez » :

> db.gens.find({nom:"ferrandez", age:6});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

Avec des opérateurs de comparaison

Cherchons tous les « ferrandez » qui ont plus de 6 ans (gt = greater than)

> db.gens.find({nom:"ferrandez", age: {$gt: 6}});

Cherchons tous les « ferrandez » qui ont moins de 6 ans (lt = less than)

> db.gens.find({nom:"ferrandez", age: {$lt: 6}});

Ces deux requêtes ne ramènent aucun résultat, ce qui n’est pas le cas des suivantes avec l’égalité…

> db.gens.find({nom:"ferrandez", age: {$lte: 6}});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }
> db.gens.find({nom:"ferrandez", age: {$gte: 6}});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

L’équivalent du IN SQL

Les gens dont l’âge est 6, 7 ou 8 :

> db.gens.find({age: {$in: [6, 7, 8]}});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

Les expressions régulières (regexp)

Les gens dont le nom commence par la lettre f :

> db.gens.find({nom:/^f/});
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

MongoDB : les bases pour bien débuter (1/3)

Le but de ce billet est d’effectuer une première incursion dans l’univers MongoDB en voyant les commandes de bases dans le shell mongo et en faisant un parallèle avec ce qui existe dans le monde du SQL. Nous allons commencer en manipulant les bases : bases de données, collections et documents. Le but n’est pas de se noyer d’emblée dans l’architecture interne de Mongo (le sharding, les replica sets, etc.) mais d’attaquer d’emblée des choses concrètes !

mongo_logo

L’emblème de MongoDB

Installation de MongoDB sur GNU Linux Debian
Nous allons faire les choses proprement au lieu d’insérer à la sauvage dans le fichier sources.list. Vous devez évidemment être sudoer pour effectuer ceci :

echo 'deb http://downloads-distro.mongodb.org/repo.fr.ian-sysvinit dist 10gen' > /tmp/mongodb.list
sudo cp /tmp/mongodb.list /etc/apt/sources.list.d/
rm /tmp/mongodb.list

sudo apt-get update
sudo apt-get install mongodb-10gen

A l’issue de l’installation, le serveur MongoDB doit être démarré automatiquement (le daemon mongod pour être plus précis).

Les principales commandes

Lancer le shell

Tout part de là ! En tapant « mongo » vous devriez y accéder car /bin doit être dans votre PATH. Si vous rencontrez des problèmes, lancez

/usr/bin/mongo

Quitter le shell

Rien de plus simple: faites donc Ctrl+C (ce bon vieux SIGINT !) ou tapez exit à l’invite, vous sortirez du shell.

Obtenir le numéro de version

Les données concernant votre installation apparaissent sous la forme d’un objet JSON, la commande a exécuter est en gras :

> db.runCommand({buildinfo: 1});
{
        "version" : "2.0.6",
        "gitVersion" : "nogitversion",
        "sysInfo" : "Linux z6 3.8-trunk-amd64 #1 SMP Debian 3.8.3-1~experimental.1 x86_64 BOOST_LIB_VERSION=1_49",
        "versionArray" : [
                2,
                0,
                6,
                0
        ],
        "bits" : 64,
        "debug" : false,
        "maxBsonObjectSize" : 16777216,
        "ok" : 1
}

Lister les bases de données

La commande permettant de faire ça est show dbs.

sebastien.ferrandez@sebastien:~$ mongo
MongoDB shell version: 2.0.6
connecting to: test
> show dbs;
local   (empty)

Comme je viens d’installer MongoDB, rien d’étonnant à ce qu’aucune base de données n’apparaisse ! Il est possible d’obtenir davantage d’informations en utilisant la base de données admin dont l’usage est réservé comme son nom l’indique à l’admin.

> use admin
switched to db admin
> db.runCommand({listDatabases: 1});
{
        "databases" : [
                {
                        "name" : "local",
                        "sizeOnDisk" : 1,
                        "empty" : true
                }
        ],
        "totalSize" : 0,
        "ok" : 1
}

Créer une collection

La collection en langage MongoDB est l’équivalent de la table en relationnel, on les crée soit en y insérant le tout premier document (l’équivalent du tuple en relationnel) soit en exécutant la commande suivante :

> db.createCollection('gens');
{ "ok" : 1 }

Insérer un document dans une collection

Notre collection s’appelle gens, insérons-y un premier document :

db.gens.insert( { nom: "ferrandez", prenom:"sebastien", age: 35 } );

Lister les documents contenus dans une collection

Pour faire l’équivalent d’un SELECT * FROM gens, j’utilise la commande find sans paramètres :

> db.gens.find();
{ "_id" : ObjectId("517941f3b12e1948c04f6d5e"), "nom" : "ferrandez", "prenom" : "sebastien", "age" : 35 }

Insérons un nouveau document et observons le changement dans le find :

> db.gens.insert( { nom: "ferrandez", prenom:"sandrine", age: 34, sexe:"F" } );
> db.gens.find();
{ "_id" : ObjectId("517941f3b12e1948c04f6d5e"), "nom" : "ferrandez", "prenom" : "sebastien", "age" : 35 }
{ "_id" : ObjectId("51794334b12e1948c04f6d5f"), "nom" : "ferrandez", "prenom" : "sandrine", "age" : 34, "sexe" : "F" }

Notez que, contrairement à une table, le nombre de champs n’est pas contraint par l’intention du schéma, en effet mon deuxième document a un champ de plus que le premier : sexe.
Dans les objets JSON que j’enregistre, un champ _id est déterminé par MongoDB. Il est évidemment tout à fait possible de forcer le sien :

> db.gens.insert( { _id: 10, nom: "ferrandez", prenom:"christophe", age: 40 } );
> db.gens.find();
{ "_id" : ObjectId("517941f3b12e1948c04f6d5e"), "nom" : "ferrandez", "prenom" : "sebastien", "age" : 35 }
{ "_id" : ObjectId("51794334b12e1948c04f6d5f"), "nom" : "ferrandez", "prenom" : "sandrine", "age" : 34, "sexe" : "F" }
{ "_id" : 10, "nom" : "ferrandez", "prenom" : "christophe", "age" : 40 }

Il est possible de manipuler des identifiants auto-incrémentés mais si vous insistez pour utiliser ce mécanisme offert par certains SGBDR comme MySQL c’est peut-être que vous avez une vision trop « relationnelle » de vos données ! Si toutefois vous persistiez à vouloir bénéficier de ce mécanisme, vous trouverez le lien à la fin de ce billet.

Supprimer une collection

Il vous faut utiliser drop, comme en SQL !

> db.gens.drop();
true

Supprimer une base de données

Deux manières de procéder, en faisant un use pour se connecter à la base de données à effacer :

> show dbs;
admin   0.203125GB
gens    0.203125GB
local   (empty)
> use gens;
switched to db gens
> db.runCommand({dropDatabase: 1});
{ "dropped" : "gens", "ok" : 1 }
> show dbs;
admin   0.203125GB
local   (empty)

ou bien en exécutant directement :

> db.gens.runCommand({dropDatabase: 1});
{ "dropped" : "gens", "ok" : 1 }

Pour aller plus loin…

Au sujet des identifiants auto-incrémentés : La documentation MongoDB (anglais)