4. Interrogation de bases NoSQL

Dans ce chapitre nous commençons à étudier la gestion de grands ensembles de documents organisés en bases de données. Nous commençons par le Web: ce n’est pas vraiment une base de données (même si beaucoup rêvent d’aller en ce sens) mais c’est un système distribué de documents, et un cas-type de Big Data s’il en est. De plus, il s’agit d’une source d’information essentielle pour collecter des données, les agréger et les analyser.

Le Web s’appuie sur des protocoles bien connus (HTTP) qui ont été repris pour la définition de services (Web) dits REST. Un premier système NoSQL (CouchDB) est introduit pour illustrer l’organisation et la manipulation de documents basées sur REST.

Nous continuons ensuite notre exploration de Cassandra et de MongoDB.

S1: HTTP, REST, et CouchDB

Le Web est la plus grande base de documents ayant jamais existé! Même s’il est essentiellement constitué de documents très peu structurés et donc difficilement exploitables par une application informatique, les méthodes utilisées sont très instructives et se retrouvent dans des systèmes plus organisés. Dans cette section, l’accent est mis sur le protocole REST que nous retrouverons très fréquemment en étudiant les systèmes NoSQL. L’un de ces systèmes (CouchDB) qui s’appuie entièrement sur REST, est d’ailleurs brièvement introduit en fin de section.

Web = ressources + URL + HTTP

Rappelons les principales caractéristiques du Web, vu comme un gigantesque système d’information orienté documents. Distinguons d’abord l’Internet, réseau connectant des machines, et le Web qui constitue une collection distribuée de ressources hébergés sur ces machines.

Le Web est (techniquement) essentiellement caractérisé par trois choses: la notion de ressource, l’adressage par des URL et le protocole HTTP.

Ressources

La notion de ressource est assez générale/générique. Elle désigne toute entité disposant d’une adresse sur le réseau, et fournissant des services. Un document est une forme de ressource: le service est le contenu du document lui-même. D’autres ressources fournissent des services au sens plus calculatoire du terme en effectuant sur demande des opérations.

Il faut essentiellement voir une ressource comme un point adressable sur l’Internet avec lequel on peut échanger des messages. L’adressage se fait par une URL, l’échange par HTTP.

URLs

L’adresse d’une ressource est une URL, pour Universal Resource Location. C’est une chaîne de caractères qui encode toute l’information nécessaire pour trouver la ressource et lui envoyer des messages.

Note

Certaines ressources n’ont pas d’adresse sur le réseau, mais sont quand même identifiables par des URI (Universal Resource identifier).

Cet encodage prend la forme d’une chaîne de caractères formée selon des règles précises illustrées par l’URL fictive suivante:

https://www.example.com:443/chemin/vers/doc?nom=b3d&type=json#fragment

Ici, https est le protocole qui indique la méthode d’accès à la ressource. Le seul protocole que nous verrons est HTTP (le s indique une variante de HTTP comprenant un encryptage des échanges). L’hôte (hostname) est www.example.com. Un des services du Web (le DNS) va convertir ce nom d’hôte en adresse IP, ce qui permettra d’identifier la machine serveur qui héberge la ressource.

Note

Quand on développe une application, on la teste souvent localement en utilisant sa propre machine de développement comme serveur. Le nom de l’hôte est alors localhost, qui correspond à l’IP 127.0.0.1.

La machine serveur communique avec le réseau sur un ensemble de ports, chacun correspondant à l’un des services gérés par le serveur. Pour le service HTTP, le port est par défaut 80, mais on peut le préciser, comme sur l’exemple précédent, où il vaut 443. On trouve ensuite le chemin d’accès à la ressource, qui suit la syntaxe d’un chemin d’accès à un fichier dans un système de fichiers. Dans les sites simples, “statiques”, ce chemin correspond de fait à un emplacement physique vers le fichier contenant la ressource. Dans des applications plus sophistiquées, les chemins sont virtuels et conçus pour refléter l’organisation logique des ressources offertes par l’application.

Après le point d’interrogation, on trouve la liste des paramètres (query string) éventuellement transmis à la ressource. Enfin, le fragment désigne une sous-partie du contenu de la ressource. Ces éléments sont optionnels.

Le protocole HTTP

HTTP, pour HyperText Transfer Protocol, est un protocole extrêmement simple, basé sur TCP/IP, initialement conçu pour échanger des documents hypertextes. HTTP définit le format des requêtes et des réponses. Voici par exemple une requête envoyée à un serveur Web:

GET /myResource HTTP/1.1
Host: www.example.com

Elle demande une ressource nommée myResource au serveur www.example.com. Voici une possible réponse à cette requête:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8

<html>
  <head><title>myResource</title></head>
  <body><p>Bonjour à tous!</p></body>
</html>

Un message HTTP est constitué de deux parties: l’entête et le corps, séparées par une ligne blanche. La réponse montre que l’entête contient des informations qualifiant le message. La première ligne par exemple indique qu’il s’agit d’un message codé selon la norme 1.1 de HTTP, et que le serveur a pu correctement répondre à la requête (code de retour 200). La seconde ligne de l’entête indique que le corps du message est un document HTML encodé en UTF-8.

Le programme client qui reçoit cette réponse traite le corps du message en fonction des informations contenues dans l’entête. Si le code HTTP est 200 par exemple, il procède à l’affichage. Un code 404 indique une ressource manquante, une code 500 indique une erreur sévère au niveau du serveur. Voir http://en.wikipedia.org/wiki/List_of_HTTP_status_codes pour une liste complète.

Le Web est initialement conçu comme un système d’échange de documents hypertextes se référençant les uns les autres, codés avec le langage HTML (ou XHTML dans le meilleur des cas). Ces documents s’affichent dans des navigateurs et sont donc conçus pour des utilisateurs humains. En revanche, ils sont très difficiles à traiter par des applications en raison de leur manque de structure et de l’abondance d’instructions relatives à l’affichage et pas à la description du contenu. Examinez une page HTML provenant de n’importe quel site un peu soigné et vous verrez que la part du contenu est négligeable par rapport à tous les CSS, javascript, images et instructions de mise en forme.

Une évolution importante du Web a donc consisté à étendre la notion de ressource à des services recevant et émettant des documents structurés transmis dans le corps du message HTTP. Vous connaissez déjà les formats utilisés pour représenter cette structure: XML, principalement, et JSON, qui est (peut-être) en voie de le remplacer.

C’est cet aspect sur lequel nous allons nous concentrer: le Web des services est véritablement une forme de très grande base de documents structurés, présentant quelques fonctionnalités (rudimentaires) de gestion de données comparable aux opérations d’un SGBD classique. Les services basés sur l’architecture REST, présentée ci-dessous, sont la forme la plus courante rencontrée dans ce contexte.

L’architecture REST

REST est une forme de service Web (l’autre, beaucoup plus complexe, est SOAP) dont le parti pris est de s’appuyer sur HTTP, ses opérations, la notion de ressource et l’adressage par URL. REST est donc très proche du Web, la principale distinction étant que REST est orienté vers l’appel à des services à base d’échanges par documents structurés, et se prête donc à des échanges de données entre applications. La figure Architecture REST: client, serveur, ressources, URL (domainne + chemin) donne une vision des éléments essentiels d’une architecture REST.

_images/REST.png

Fig. 4.1 Architecture REST: client, serveur, ressources, URL (domainne + chemin)

Avec HTTP, il est possible d’envoyer quatre types de messages, ou méthodes, à une ressource web:

  • GET est une lecture de la ressource (ou plus précisément de sa représentation publique);
  • PUT est la création d’une ressource;
  • POST est l’envoi d’un message à une ressource existante;
  • DELETE la destruction d’une ressource.

REST s’appuie sur un usage strict (le plus possible) de ces quatre méthodes. Ceux qui ont déjà pratiqué la programmation Web admettront par exemple qu’un développeur ne se pose pas toujours nettement la question, en créant un formulaire, de la méthode GET ou POST à employer. De plus le PUT (qui n’est pas connu des formulaires Web) est ignoré et le DELETE jamais utilisé.

La définition d’un service REST se doit d’être plus rigoureuse.

  • le GET, en tant que lecture, ne doit jamais modifier l’état de la ressource (pas “d’effet de bord”); autrement dit, en l’absence d’autres opérations, des messages GET envoyés répétitivement à une même ressource ramèneront toujours le même document, et n’auront aucun effet sur l’environnement de la ressource ;
  • le PUT est une création, et l’URL a laquelle un message PUT est transmis ne doit pas exister au préalable; dans une interprétation un peu plus souple, le PUT crée ou remplace la ressource éventuellement existante par la nouvelle ressource transmise par le message;
  • inversement, POST doit s’adresser à une ressource existante associée à l’URL désignée par le message; cette méthode correspond à l’envoi d’un message à la ressource (vue comme un service) pour exécuter une action, avec potentiellement un changement d’état (par exemple la création d’une nouvelle ressource).

Les messages sont transmis en HTTP (voir ci-dessus) ce qui offre, entre autres avantages, de ne pas avoir à redéfinir un nouveau protocole (jetez un œil à SOAP si vous voulez apprécier vraiment cet avantage!). Le contenu du message est une information codée en XML ou en JSON (le plus souvent), soit ce que nous avons appelé jusqu’à présent un document.

  • quand le client émet une requête REST, le document contient les paramètres d’accès au service (par exemple les valeurs de la ressources à créer);
  • que la ressource répond au client, le document contient l’information produite par la ressource.

Important

En toute rigueur, il faut bien distinguer la ressource et le document qui représente une information produite par la ressource.

On peut faire appel à un service REST avec n’importe quel client HTTP, et notamment avec votre navigateur préféré: copiez l’URL dans la fenêtre de navigation et consultez le résultat. Le navigateur a cependant l’inconvénient, avec cette méthode, de ne transmettre que des messages GET. Un outil plus général, s’utilisant en ligne de commande, est cURL. S’il n’est pas déjà installé dans votre environnement, il est fortement conseillé de le faire dès maintenant: le site de référence est http://curl.haxx.se/.

Voici quelques exemples d’utilisation de cURL pour parler le HTTP avec un service REST. Ici nous nous adressons à l’API REST de Open Weather Map, un service fournissant des informations météorologiques.

Pour connaître la météo sur Paris (en JSON):

curl -X GET api.openweathermap.org/data/2.5/weather?q=Paris

Et on obtient la réponse suivante (qui varie en fonction de la météo, évidemment).

{
   "coord":{
     "lon":2.35,
     "lat":48.85
   },
   "weather":[
     {
      "id":800,
      "main":"Clear",
      "description":"Sky is Clear",
      "icon":"01d"
     }
   ],
   "base":"cmc stations",
   "main":{
    "temp":271.139,
    "temp_min":271.139,
    "temp_max":271.139,
    "pressure":1021.17,
    "sea_level":1034.14,
    "grnd_level":1021.17,
    "humidity":87
   },
   "name":"Paris"
}

Même chose, mais en demandant une réponse codée en XML. Notez l’option -v qui permet d’afficher le détail des échanges de messages HTTP gérés par cURL.

curl -X GET -v api.openweathermap.org/data/2.5/weather?q=Paris&mode=xml

Nous verrons ultérieurement des exemples de PUT et de POST pour créer des ressources et leur envoyer des messages avec cURL.

Note

La méthode GET est utilisée par défaut par cURL, on peut donc l’omettre.

Nous nous en tenons là pour les principes essentiels de REST, qu’il faudrait compléter de nombreux détails mais qui nous suffiront à comprendre les interfaces (ou API) REST que nous allons rencontrer.

Important

Les méthodes d’accès aux documents sont représentatives des opérations de type dictionnaire: toutes les données ont une adresse, on peut accéder à la donnée par son adresse (get), insérer une donnée à une adresse (put), détruire la donnée à une adresse donnée (delete). De nombreux systèmes NoSQL se contentent de ces opérations qui peuvent s’implanter très efficacement.

Pour être concret et rentrer au plus vite dans le cœur du sujet, nous présentons l’API de CouchDB qui est conçu comme un serveur de documents (JSON) basé sur REST.

L’API REST de CouchDB

Faisons connaissance avec CouchDB, un système NoSQL qui gère des collections de documents JSON. Les quelques manipulations ci-dessous sont centrées sur l’utilisation de CouchDB via son interface REST, mais rien ne vous empêche d’explorer le système en profondeur pour en comprendre le fonctionnement.

CouchDB est essentiellement un serveur Web étendu à la gestion de documents JSON. Comme tout serveur Web, il parle le HTTP et manipule des ressources (figure Architecture (simplifiée) de CouchDB).

_images/CouchDB.png

Fig. 4.2 Architecture (simplifiée) de CouchDB

Vous pouvez installer CouchDB sur votre machine avec Docker, ou directement en suivant les instructions du site http://couchdb.apache.org. Dans ce qui suit, on suppose que le serveur est accessible à l’adresse http://localhost:5984. Une première requête REST permet de vérifier la disponibilité de ce serveur.

curl -X GET http://localhost:5984

{"couchdb":"Welcome","version":"1.6.0"}

Bien entendu, dans ce qui suit, utilisez l’adresse de votre propre serveur. Même si nous utilisons REST pour communiquer avec CouchDB par la suite, rien ne vous empêche de consulter en parallèle l’interface graphique qui est disponible à l’URL relative _utils (donc à l’adresse complète http://localhost:5984/_utils dans notre cas). La figure L’interface graphique de CouchDB montre l’aspect de cette interface graphique, très pratique.

_images/couch-futon.png

Fig. 4.3 L’interface graphique de CouchDB

Important

Si vous avez installé votre serveur chez un fournisseur (et pas sur votre machine locale) il faut transmettre le nom et le mot de passe de l’administrateur dans la commande REST. Ils se placent dans l’URL comme indiqué ci-dessous:

$nom et $passe correspondent au compte administrateur que vous avez défini.

CouchDB adopte délibérément les principes et protocoles du Web. Une base de données et ses documents sont vus comme des ressources et on dialogue avec eux en HTTP, conformément au protocole REST.

Un serveur CouchDB gère un ensemble de bases de données. Créer une nouvelle base se traduit, avec l’interface REST, par la création d’une nouvelle ressource. Voici donc la commande avec cURL pour créer une base films (notez la méthode PUT pour créer une ressource).

curl -X PUT http://localhost:5984/films
{"ok":true}

Maintenant la ressource est créée, et on peut obtenir sa représentation avec une requête GET.

curl -X GET http://localhost:5984/films

Cette requête renvoie un document JSON décrivant la nouvelle base.

{"update_seq":"0-g1AAAADfeJz6t",
 "db_name":"films",
 "sizes": {"file":17028,"external":0,"active":0},
 "purge_seq":0,
 "other":{"data_size":0},
 "doc_del_count":0,
 "doc_count":0,
 "disk_size":17028,
 "disk_format_version":6,
 "compact_running":false,
 "instance_start_time":"0"
}

Vous commencez sans doute à saisir la logique des interactions. Les entités gérées par le serveur (des bases de données, des documents, voire des fonctions) sont transposées sous forme de ressources Web auxquelles sont associées des URLs correspondant, autant que possible, à l’organisation logique de ces entités.

Pour insérer un nouveau document dans la base films, on envoie donc un message PUT à l’URL qui va représenter le document. Cette URL est de la forme http://localhost:5984/films/idDoc, où idDoc désigne l’identifiant de la nouvelle ressource.

$ curl -X PUT http://localhost::5984/films/doc1 \
       -d '{"key": "value"}'
  {"ok":true,"id":"doc1","rev":"1-25eca"}

Que faire si on veut insérer des documents placés dans des fichiers? Vous avez dû récupérer de la base Webscope des documents représentant des films en JSON. Le document movie_52.json par exemple représente le film Usual Suspects. Voici comment on l’insère dans la base en lui attribuant l’identifiant us.

curl -X PUT http://localhost:5984/films/us
     -d @movie_52.json -H "Content-Type: application/json"

Cette commande cURL est un peu plus compliquée car il faut créer un message HTTP un peu plus complexe que pour une simple lecture. On passe dans le corps du message HTTP le contenu du fichier movie_52.json avec l’option -d et le préfixe @, et on indique que le format du message est JSON avec l’option -H. Voici la réponse de CouchDB.

{
 "ok":true,
 "id":"us",
 "rev":"1-68d58b7e3904f702a75e0538d1c3015d"
}

Le nouveau document a un identifiant (celui que nous attribué par l’URL de la ressource) et un numéro de révision. L’identifiant doit être unique (pour une même base), et le numéro de révision indique le nombre de modifications apportées à la ressource depuis sa création.

Si vous essayez d’exécuter une seconde fois la création de la ressource, CouchDB proteste et renvoie un message d’erreur:

{
  "error":"conflict",
 "reason":"Document update conflict."
}

Un document existe déjà à cette URL.

Une autre façon d’insérer, intéressante pour illustrer les principes d’une API REST, et d’envoyer non pas un PUT pour créer une nouvelle ressource (ce qui impose de choisir l’identifiant) mais un POST à une ressource existante, ici la base de données, qui se charge alors de créer la nouvelle ressource représentant le film, et de lui attribuer un identifiant.

Voici cette seconde option à l’œuvre pour créer un nouveau document en déléguant la charge de la création à la ressource films.

curl -X POST http://localhost:5984/films
      -d @movie_52.json -H "Content-Type: application/json"

Voici la réponse de CouchDB:

{
 "ok":true,
 "id":"movie:52",
 "rev":"1-68d58b7e3904f702a75e0538d1c3015d"
}

CouchDB a trouvé l’identifiant (conventionnellement nommé id) dans le document JSON et l’utilise. Si aucun identifiant n’est trouvé, une valeur arbitraire (une longue et obscure chaîne de caractère) est engendrée.

CouchDB est un système multi-versions: une nouvelle version du document est créée à chaque insertion. Chaque document est donc identifié par une paire (id, revision): notez l’attribut rev dans le document ci-dessus. Dans certains cas, la version la plus récente est implicitement concernée par une requête. C’est le cas quand on veut obtenir la ressource avec un simple GET.

curl -X GET http://localhost:5984/films/us

En revanche, pour supprimer un document, il faut indiquer explicitement quelle est la version à détruire en précisant le numéro de révision.

curl -X DELETE http://localhost:5984/films/us?rev=1-68d58b7e3904f702a75e0538d1c3015d

Nous en restons là pour l’instant. Cette courte session illustre assez bien l’utilisation d’une API REST pour gérer une collection de document à l’aide de quelques opérations basiques: création, recherche, mise à jour, et destruction, implantées par l’une des 4 méthodes HTTP. La notion de ressource, existante (et on lui envoie des messages avec GET ou POST) ou à créer (avec un message PUT), associée à une URL correspondant à la logique de l’organisation des données, est aussi à retenir.

Exercices

Exercice Ex-S1-1: reproduisez les commandes REST avec votre serveur CouchDB

Il s’agit simplement de reproduire les commandes ci-dessus, en examinant éventuellement les requêtes HTTP engendrées par cURL pour bien comprendre les échanges effectués.

Vous pouvez tenter d’importer l’ensemble des documents en une seule commande avec l’API décrite ici: http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API. Une très légère modification du document JSON, extrait de Webscope et représentant l’ensemble des films, suffit à le rendre compatible avec la commande d’insertion en masse. Vous devriez avoir exporté un fichier movie.json avec le contenu suivant:

[
 {
 "_id": "movie:1",
 "title": "Vertigo",
 ...
 },
 {
 "_id": "movie:2",
 "title": "Alien",
 ...
 }
]

Effectuez la modification suivante, plaçant le tableau comme valeur d’une clé docs:

 {"docs":
  [
   {
    "_id": "movie:1",
    "title": "Vertigo",
    ...
   },
   {
    "_id": "movie:2",
    "title": "Alien",
    ...
   }
  ]
}

La commande d’insertion est alors la suivante:

curl -X POST  http://localhost:5984/films/_bulk_docs
         -d @movies.json -H "Content-Type: application/json"

Exercice Ex-S1-2: Documents et services Web

Soyons concret: vous construisez une application qui, pour une raison X ou Y, a besoin de données météo sur une région ou une ville donnée. Comment faire? la réponse est simple: trouver le service Web qui fournit ces données, et appeler ces services. Pour la première question, on peut par exemple prendrele site OpenweatherMap, dont les services sont décrits ici: http://openweathermap.org/api. Pour appeler ce service, comme vous pouvez le deviner, on passe par le protocole HTTP.

Application: utilisez les services de OpenWeatherMap pour récupérer les données météo pour Paris, Marseille, Lyon, ou toute ville de votre choix. Testez les formats JSON et XML.

Exercice Ex-S1-3: comprendre les services géographiques de Google.

Google fournit (librement, jusqu’à une certaine limite) des services dits de géolocalisation: récupérer une carte, calculer un itinéraire, etc. Vous devriez être en mesure de comprendre les explications données ici: https://developers.google.com/maps/documentation/webservices/?hl=FR (regardez en particulier les instructions pour traiter les documents JSON et XML retournés).

Pour vérifier que vous avez bien compris: créez un formulaire HTML avec deux champs dans lesquels on peut saisir les points de départ et d’arrivée d’un itinéraire (par exemple, Paris - Lyon). Quand on valide ce formulaire, afficher le JSON ou XML (mieux, donner le choix du format à l’utilisateur) retourné par le service Google (aide: il s’agit du service directions).

Exercice Ex-S1-4: explorer les services Web et l’Open data.

Le Web est une source immense de données structurées représentées en JSON ou en XML. Allez voir sur le sit http://programmableweb.com et regardez la liste de services. Vous voulez connaître le programme d’une station de radio, accéder à une entre Wikipedia sous forme structurée, gérer des calendriers? On trouve à peu près tout sous forme de services.

Explorez ces API pour commencer à vous faire une idée du type de projet qui vous intéresse. Récupérez des données et regardez leur format.

Autre source de données: les données publiques. Allez voir par exemple sur

Essayer d’imaginer une application qui combine plusieurs sources de données.

À titre d’exemple, voici comment récupérer toutes les heures les informations sur les Vélibs. Les commandes ci-desous supposent une machine Linux connectée à l’Internet. Je vous laisse transposer pour Windows. Il faut également demander une clé d’accès au service sur le site http://api.jcdecaux.com.

Premièrement, voici le code du script recup_velib.sh.

#!/bin/sh
# Récupération des données dans un fichier txt
wget 'https://api.jcdecaux.com/vls/v1/stations?contract=Paris&apiKey=efa96ce1fb1e3a694799818be 0b499f7ffeb09b2'
mv 'stations?contract=Paris&apiKey=efa96ce1fb1e3a694799818be0b499f7ffeb09b2' resultat-`date +% Y-%m-%d-%H-%M`.json

Chaque appel produit un fichier JSON nommé de la manière suivante:

resultat-2016-12-13-17-00.json : resultat-annee-mois-jour-HH-MM.json

Voici le format du contenu:

 {
    "number": int,
    "name": String,
    "address": String,
    "position":
    "banking":Boolean,
    "bonus":Boolean,
    "status": String,
    "contract_name": String,
    {"lat": float,
    "lng": float},
    "bike_stands": int,
    "available_bike_stands": int,
    "available_bikes": int,
    "last_update": float
}

Il faut automatiser l’exécution de ce script, avec cron sous Linux (https://doc.ubuntu-fr.org/cron par exemple). Voicil le contenu du fichier contrab pour exécuter la commande toutes les heures (faut-il préciser que le chemin d’accès au fichier est donné pour l’exemple?).

0 * * * * sh /home/philippe/recup_velib.sh

Et voilà, comptez environ 1500 documents JSON par heure, quelques centaines de MO par mois.

S2: requêtes Cassandra

Supports complémentaires

  • Présentation: à venir
  • Vidéo de la session CQL à venir

Cassandra propose un langage, nommé CQL, inspiré de SQL, mais fortement restreint par l’absence de jointure. De plus, d’autres types de restrictions s’appliquent, motivées par l’hypothèse qu’une base Cassandra est nécessairement une base à très grande échelle, et que les seules requêtes raisonnables sont celles pour lequelles la structuration des données permet des temps de réponse acceptables.

Note

Vous pouvez pratiquer les requêtes de cette session si vous disposez d’un serveur Cassandra, d’un client et de la base de données des films. Toutes les instructions ont été données dans le chapitre Modélisation de bases NoSQL. En résumé, vous devriez avoir:

  • une table artists avec la liste des artistes;
  • une table movies où chaque film contient des données imbriquées représentant le réalisateur du film et les acteurs.

CQL, un sous-ensemble de SQL

CQL ne permet d’interroger qu’une seule table. Cette restriction (forte) mise à part, le langage est délibérement conçu comme un sous-ensemble de SQL et de sa construction select from where.

Note

Toute requête CQL doit se terminer par un ‘;’.

Commençons par quelques exemples.

Sélectionnons tous les artistes.

select  * from artists;

Selon l’utilitaire que vous utilisez, vous devriez obtenir l’affichage des premiers artistes sous une forme ou sous une autre. Cassandra étant supposé gérer de très grandes bases de données, ces utilitaires vont souvent ajouter automatiquement une clause limitant le nombre de lignes retournées. Vous pouvez ajouter cette clause explicitement.

select  * from artists limit 20;

On peut obtenir le résultat encodé en JSON en ajoutant simplement le mot-clé JSON.

select JSON * from artists;

Bien entendu, le * peut être remplacé par la liste des attributs à conserver (projeter).

select title from movies;

Si une valeur v est un dictionnaire (objet en JSON), on peut accéder à l’un de ses composants c avec la notation v.c. Exemple pour le réalisateur du film.

select title, director.last_name from movies;

En revanche, quand la valeur est un ensemble ou une liste, on ne sait pas avec CQL accéder à son contenu. La tentative d’exécuter la requête:

select title, actors.last_name from movies;

devrait retourner une erreur. Il est vrai que l’on ne sait pas très bien à quoi devrait ressembler le résultat. D’autres langages (notamment XQuery, mais également le la,gage de script Pig que nous étudierons en fin de cours) proposent des solutions à ce problème. Il se peut que CQL évolue un jour pour proposer quelque chose de semblable.

On peut, dans la clause select, appliquer des fonctions. Cassandra permet la définition de fonctions utilisateur, et leur application aux données grâce à CQL. Quelques fonctions prédéfinies sont également disponible. Voici un exemple (sans intérêt) de conversion de l’année du film en texte (c’est un entier à l’origine).

select cast(year as text) as yearText from movies ;

Notez le renommage de la colonne avec le mot-clé as. Tout cela est directement emprunté à SQL. On peut également compter le nombre de lignes dans la table.

select count(*) from movies ;

On peut effectuer des filtrages avec la clause where. Par exemple:

from artists where id='artist:31';

Remarque importante: le critère de sélection porte ici sur la clé. On peut généraliser à plusieurs valeurs avec la clause in.

select  * from artists
where id in ('artist:31', 'artist:17', 'artist:65');

Tentons maintenant une recherche sur un attribut non-clé.

select  * from artists
where last_name='Cruise' ;

Vous devriez obtenir un rejet de cette requête avec le message suivant:

Unable to execute CQL script. Cannot execute this query as it might involve data
filtering and thus may have unpredictable performance. If you want
to execute this query despite the performance unpredictability,
use ALLOW FILTERING.

Nous avons atteint les limites de CQL en tant que clône de SQL.

Pourquoi CQL n’est pas SQL

Pourquoi un where sur un attribut non-clé est-il rejeté? Pour une raison qui tient à l’organisation des données:

  • Cassandra organise une table selon une structure (que nous étudierons ultérieurement) qui permet très rapidement de trouver un document par sa clé. La recherche par clé est donc autorisée.
  • Cette structure n’existe que pour la clé. Toute recherche sur un autre attribut n’a d’autre solution que de parcourir séquentiellement toute la table en effectuant le test sur le critère de recherche à chaque fois.

Comme déjà indiqué, Cassandra est conçu pour de très grandes bases de données, et le rejet de ces requêtes est une précaution. Le message indique clairement à l’utilisateur que sa requête est susceptible de prendre beaucoup de temps à s’exécuter.

À l’usage on décrouvre tout un ensemble de restrictions (par rapport à SQL) qui s’expliquent par cette volonté d’éviter l’exécution d’une requête qui impliquerait un parcours de tout ou partie de la table. Voyons quelques exemples, avec explications.

Note

Certaines des explications qui suivent sont volontairement brèves car elles impliquent une compréhension de la structure interne des données dans Cassandra ainsi que de la méthode de répartition dans un environnement distribué. Nous présenterons tout cela plus tard.

Tentons une requête sur la clé primaire, mais avec un critère d’inégalité.

select  * from artists
where id > 'hhh'

On obtient un rejet avec un message indiquant que seule l’égalité est autorisée sur la clé (et d’autres détails à éclaircir ultérieurement).

Peut-on trier les données avec la clause order by? Essayons.

select  * from artists order by id;
select  * from movies order by title;

Les deux requêtes sont rejetées. Le message nous dit (à peu près) que le tri est autorisé seulement quand on est assuré que les données à trier proviennent d’une seule partition. En (un peu plus) clair: Cassandra ne veut pas avoir à trier des données provenant de plusieurs serveurs, dans un environnement distribué avec répartition d’une table sur plusieurs nœuds.

Et voilà. Cassandra interdit tout usage de CQL qui amènerait à parcourir toute la base ou une partie non prédictible de la base pour constituer le résultat. Cette interdiction n’est cependant pas totale. Dans le cas de la clause where, l’utilisateur peut prendre explicitement ses responsabilités en ajoutant la clause allow filtering. Dans ce cas, on peut partir à la recherche de Tom Cruise.

select  * from artists
where last_name='Cruise' allow filtering;

Si la table contient des milliards de ligne (bon, c’est peu probable ici), il faudra certainement attendre longtemps et exploiter intensivement les ressources du système pour un résultat médiocre (on ne parle pas de Tom Cruise, mais du nombre de lignes ramenées). À utiliser à bon escient donc.

Il faut penser que le coût d’évaluation de cette requête est proportionnel à la taille de la base. Cassandra tente de limiter les requêtes à celles dont le coût est proportionnel à la taille du résultat.

Note

Cette remarque explique pourquoi la requête select * from artists;, qui parcourt toute la base, est autorisée.

À partir du moment où on autorise explicitement le filtrage, on peut combiner plusieurs critères de recherche, comme en SQL.

select  * from artists
where last_name='Cruise' and first_name='Tom' allow filtering;

Mais, si c’est pour faire du SQL, autant choisir une base relationnelle. Les restrictions de Cassandra doivent s’interpréter dans un contexte Big Data où l’accès aux données doit prendre en compte leur volumétrie (et notamment le fait que cette volumétrie impose une répartition des données dans un système distribué).

Une autre possibilité est de créer un index secondaire sur les attributs auxquels on souhaite appliquer des critères de recherche.

create index on movies(year);

Cassandra autorise alors de requêtes avec la clause where portant sur les attributs indexés.

select * from movies where year = 1992;

En présence d’un index, il n’est plus nécessaire de parcourir toute la collection. Cette option est cependant à utiliser avec prudence. En premier lieu, un index peut être coûteux à maintenir. Mais surtout sa sélectivité n’est pas toujours assurée. Ici, par exemple, un index sur l’année est probablement une très mauvaise idée. On peut estimer qu’un film sur 100 a été tourné en 1992, et à l’échelle du Big Data, ça laisse beaucoup de films à trouver, même avec l’index, et une requête qui peut ne pas être performante du tout.

Exercices

Exercice Ex-S2-1: expérimentez CQL

À vous de jouer: reproduisez les requêtes ci-dessus sur votre base Cassandra. Nous verrons

Exercice Ex-S2-2: données imbriquées

Peut-on exprimer des critères sur les données imbriquées? Peut-on par exemple trouver tous les films mis en scène par Tarantino? À vous de chercher la solution (si elle existe) dans la documentation Cassandra.

Exercice Ex-S2-3: sujet d’étude, les vues matérialisées

Depuis la version 3, Cassandra propose un mécanisme de vue matérialisé. Etudiez la documentation à ce sujet, et montrez comment ce mécanisme peut permettre de répondre à des requêtes comme celle de l’exercice précédent.

S3: requêtes avec MongoDB

Précisons tout d’abord que le langage de requête sur des collections est spécifique à MongoDB. Essentiellement, c’est un langage de recherche dit “par motif” (pattern). Il consiste à interroger une collection en donnant un objet (le “motif/pattern”, en JSON) dont chaque attribut est interprété comme une contrainte sur la structure des objets à rechercher. Voici des exemples, plus parlants que de longues explications. Nous travaillons sur la base contenant les films complets, sans référence (donc, celle nommée nfe204 si vous avez suivi les instructions précédentes).

Sélections

Commençons par la base: on veut parcourir toute une collection. On utilise alors find() dans argument.

db.movies.find ()

Si’il y a des millions de documents, cela risque de prendre du temps... D’ailleurs, comment savoir combien de documents comprend le résultat?

db.movies.count ()

Comme en SQL (étendu), les options skip et limit permettent de “paginer” le résultat. La requête suivante affiche 12 documents à partir du dixième inclus.

db.movies.find ().skip(9).limit(12)

Implicitement, cela suppose qu’il existe un ordre sur le parcours des documents. Par défaut, cet ordre est dicté par le stockage physique: MongoDB fournit les documents dans l’ordre où il les trouve (dans les fichiers). On peut trier explicitement, ce qui rend le résultat plus déterministe. La requête suivante trie les documents sur le titre du film, puis pagine le résultat.

db.movies.find ().sort({"title": 1}).skip(9).limit(12)

La spécification du tri repose sur un objet JSON, et ne prend en compte que les noms d’attribut sur lesquels s’effectue le tri. La valeur (ici, celle du titre) ne sert qu’à indiquer si on trie de manière ascendante (valeur 1) ou descendante (valeur -1).

Attention, trier n’est pas anodin. En particulier, tout tri implique que le système constitue l’intégralité du résultat au préalable, ce qui induit une latence (temps de réponse) potentiellement élevée. Sans tri, le système peut délivrer les documents au fur et à mesure qu’il les trouve.

Critères de recherche

Si on connaît l’identifiant, on effectue la recherche ainsi.

db.movies.find ({"_id": "movie:2"})

Une requête sur l’identifiant ramène (au plus) un seul document. Dans un tel cas, on peut utiliser findOne.

db.movies.findOne ({"_id": "movie:2"})

Cette fonction renvoie toujours un document (au plus), alors que la fonction find renvoie un curseur sur un ensemble de documents (même si c’est un singleton). La différence est surtout importante quand on utilise une API pour accéder à MongoDB avec un langage de programmation.

Sur le même modèle, on peut interroger n’importe quel attribut.

db.movies.find ({"title": "Alien"})

Ca marche bien pour des attributs atomiques (une seule valeur), mais comment faire pour interroger des objets ou des tableaux imbriqués? On utilise dans ce cas des chemins, un peu à la XPath, mais avec une syntaxe plus “orienté-objet”. Voici comment on recherche les films de Quentin Tarantino.

db.movies.find ({"director.last_name": "Tarantino"})

Et pour les acteurs, qui sont eux-mêmes dans un tableau? Ca fonctionne de la même manière.

db.movies.find ({"actors.last_name": "Tarantino"})

La requête s’interprète donc comme: “Tous les films dont l’un des acteurs se nomme Tarantino”.

Conformément aux principes du semi-structuré, on accepte sans protester la référence à des attributs ou des chemins qui n’existent pas. En fait, dire “ce chemin n’existe pas” n’a pas grand sens puisqu’il n’y a pas de schéma, pas de contrainte sur la structure des objets, et que donc tout chemin existe potentiellement: il suffit de le créer. La requête suivante ne ramène rien, mais ne génére pas d’erreur.

db.movies.find ({"actor.last_name": "Tarantino"})

Important

Contrairement à une base relationnelle, une base semi-structurée ne proteste pas quand on fait une faute de frappe sur des noms d’attributs.

Quelques raffinements permettent de dépasser la limite sur le prédicat d’égalité implicitement utilisé ici pour comparer les critères donnés et les objets de la base. Pour les chaînes de caractères, on peut introduire des expressions régulières. Tous les films dont le titre commence par Re? Voici:

db.movies.find ({"title": /^Re/}, {"actors": null, "summary": 0} )

Pas d’apostrophes autour de l’expression régulière. On peut aussi effectuer des recherches par intervalle.

db.movies.find( {"year": { $gte: "2000", $lte: "2005" } }, {"title": 1} )

Projections

Jusqu’à présent, les requêtes ramènent l’intégralité des objets satisfaisant les critères de recherche. On peut aussi faire des projections, en passant un second argument à la fonction find().

db.movies.find ({"actors.last_name": "Tarantino"}, {"title": true, "actors": 'j'} )

Le second argument est un objet JSON dont les attributs sont ceux à conserver dans le résultat. La valeur des attributs dans cet objet-projection ne prend que deux interprétations. Toute valeur autre que 0 ou null indique que l’attribut doit être conservé. Si on choisit au contraire d’indiquer les attributs à exclure, on leur donne la valeur 0 ou null. Par exemple, la requête suivante retourne les films sans les acteurs et sans le résumé.

db.movies.find ({"actors.last_name": "Tarantino"}, {"actors": null, "summary": 0})

Opérateurs ensemblistes

Les opérateurs du langage SQL in, not in, any et all se retrouvent dans le langage d’interrogation. La différence, notable, est que SQL applique ces opérateurs à des relations (elles-mêmes obtenues par des requêtes) alors que dans le cas de MongoDB, ce sont des tableaux JSON. MongoDB ne permet pas d’imbriquer des requêtes.

Voici un premier exemple: on cherche les films dans lesquels joue au moins un des artistes dans une liste (on suppose que l’on connaît l’identifiant).

db.artists.find({"actors._id": {$in: ["artist:34","artist:98","artist:1"]}})

Gardez cette recherche en mémoire: elle s’avèrera utile pour contourner l’absence de jointure en MongoDB. Le in exprime le fait que l’une des valeurs du premier tableau (actors._id) doit être égale à l’une des valeurs de l’autre. Il correspond implicitement, en SQL, à la clause ANY. Pour exprimer le fait que toutes les valeurs de premier tableau se retrouvent dans le second (en d’autres termes, une inclusion), on utilise la clause all.

db.movies.find({"actors._id": {$all: ["artist:23","artist:147"]}})

Le not in correspond à l’opérateur $nin.

db.artists.find({"_id": {$nin: ["artist:34","artist:98","artist:1"]}})

Comment trouver les films qui n’ont pas d’attribut summary?

db.movies.find({"summary": {$exists: false}}, {"title": 1})

Opérateurs Booléens

Par défaut, quand on exprime plusieurs critères, c’est une conjonction (and) qui est appliquée. On peut l’indiquer explicitement. Voici la syntaxe (les films tournés avec Leonardo DiCaprio en 1997):

db.movies.find({$and : [{"year": "1997"}, {actors.last_name: "DiCaprio"]} )

L’opérateur and s’applique à un tableau de conditions. Bien entendu il existe un opérateur or avec la même syntaxe. Les films parus en 1997 ou avec Leonardo DiCaprio.

db.movies.find({$or : [{"year": "1997"}, {actors.last_name: "DiCaprio"]} )

Voici pour l’essentiel en ce qui concerne les recherches portant sur une collection et consistant à sélectionner des documents. Grosso modo, on obtient la même expressivité que pour SQL dans ce cas. Que faire quand on doit croiser des informations présentes dans plusieurs collections? En relationnel, on effectue des jointures. Avec Mongo, il faut bricoler.

Jointures

La jointure, au sens de: associer des objets distincts, provenant en général de plusieurs collections, pour appliquer des critères de recherche croisés, n’existe pas en MongoDB. C’est une limitation très importante du point de vue de la gestion de données. On peut considérer qu’elle est cohérente avec une approche documentaire dans laquelle les documents sont supposés indépendants les uns des autres, avec une description interne suffisamment riche pour que toute recherche porte sur le contenu du document lui-même. Cela étant, on peut imaginer toutes sortes de situations où une jointure est nécessaire dans une aplication de traitement de données.

Le serveur ne sachant pas effectuer de jointures, on en est réduit à les faire côté client, comme illustré sur la figure Jointure côté serveur et côté client. Cela revient essentiellement à appliquer l’algorithme de jointures par boucle imbriquées en stockant des données temporaires dans des structures de données sur le client, et en effectuant des échanges réseaux entre le client et le serveur, ce qui dans l’ensemble est inefficace.

_images/jointure-serveur-client.png

Fig. 4.4 Jointure côté serveur et côté client

Comme l’interpréteur mongo permet de programmer en Javascript, nous pouvons en fait illustrer la méthode assez simplement. Considérons la requête: “Donnez tous les films dont le directeur est Clint Eastwood”.

Note

Nous travaillons sur la base moviesref dans laquelle un film ne contient que la référence au metteur en scène, ce qui évite les redondances, mais complique la reconstitution de l’information.

La première étape dans la jointure côté client consiste à chercher l’artiste Clint Eastwood et à le stocker dans l’espace mémoire du client (dans une variable, pour dire les choses simplement).

eastwood = db.artists.findOne({"first_name": "Clint", "last_name": "Eastwood"})

On dispose maintenant d’un objet eastwood. Une seconde requête va récupérer les films dirigés par cet artiste.

db.movies.find({"director._id": eastwood['_id']}, {"title": 1})

Voilà le principe. Voyons maintenant plus généralement comment on effectue l’équivalent des jointures en SQL. Prenons la requête suivante:

select m.titre, a.*  from Movie m, Artist a
where m.id_director = a.id

On veut donc les titres des films et le réalisateur. On va devoir coder, du côté client, un algorithme de jointure par boucles imbriquées. Le voici, sous le shell de MongoDB (et donc en programmation javascript).

var lesFilms = db.movies.find()
while (lesFilms.hasNext()) {
  var film = lesFilms.next();
  var mes = db.artists.findOne({"_id": film.director._id});
  printjson(film.title);
  printjson(mes);
}

On a donc une boucle, et une requête imbriquée, exécutée autant de fois qu’il y a de films. C’est exactement la méthode qui serait utilisée par le serveur si ce dernier implantait les jointures. L’exécuter du côté client induit un surcoût en programmation, et en échanges réseau entre le client et le serveur.

Exercices

Exercice Ex-S3-1: requêtes sur la base des films.

Sur votre base movies,

  • tous les titres;
  • tous les titres des films parus après 2000;
  • le résumé de Spider-Man;
  • qui est le metteur en scène de Gladiator?
  • titre des films avec Kirsten Dunst;
  • quels films ont un résumé?
  • les films qui ne sont ni des drames ni des comédies.
  • affichez les titres des films et les noms des acteurs.
  • dans quels films Clint Eastwood est-il acteur mais pas réalisateur (aide: utilisez l’opérateur de comparaison $ne).
  • Difficile: Comment chercher les films dont le metteur en scène est aussi un acteur? Pas sûr que ce soit possible sans recourir à une auto-jointure, côté client...

Correction

  • db.movies.find({}, {"title": 1})

  • db.movies.find({"year": {$gt: "2000"}}, {"title": 1, "year": 1})

  • db.movies.find({"title": "Spider-Man"}, {"summary": 1})

  • db.movies.find({"title": "Gladiator"}, {"director": 1})

  • db.movies.find({"actors.last_name": "Dunst"}, {"title": 1})

  • db.movies.find({"summary": {$exists: true}}, {"title": 1})

    NB: cette fonction regarde si le champ existe, pas s’il est vide ou non. Dans la base, il existe des films avec un résumé ayant pour valeur null. Afin de ne récupérer que les films ayant réellement un résumé, on peut ajouter $ne:null

    db.movies.find({"summary": {$exists: true, $ne:null}}, {"title": 1})

    donne les films dont le champ résumé existe et dont la valeur du champ est différente de null.

  • db.movies.find({"genre": {$nin: ["Drame", "Comédie"]}}, {"title": 1, "genre": 1})

  • db.movies.find({}, {"title": 1, "actors.first_name": 1, "actors.last_name": 1})

  • db.movies.find({"actors.last_name": "Eastwood", "director.last_name": {$ne: "Eastwood"}}, {"title": 1})

Exercice Ex-S3-2: requêtes sur votre base

Si vous avez constitué une base de documents JSON (bureautique, météo, données géolocalisées Google), je vous encourage à l’importer dans MongoDB et à tester des requêtes qui vous semblent typique d’une application basée sur ces données.