- Mongo services
- ElasticSearch services
- Suggestions in map
- Error in town name corrected nb : index must be create and update before launching Application
Julien BONNEFONT Thomas LEGENDRE
Découverte de MongoDB et d'Elasticsearch, par la pratique !
Quelques rappels avant de démarrer :
- MongoDB est une base de donnnées NoSQL, orientée documents.
- Le format des documents est JSON.
- Les documents sont stockés dans des collections.
- Une base de données MongoDB peut contenir plusieurs collections de documents.
- Il n'est pas possible d'effectuer de jointures entre collections (et ce n'est pas la philosophie).
Dans tous les cas, n'hésitez pas à vous référer à la documentation officielle.
Téléchargez la dernière version stable de MongoDB sur mongodb.org/downloads. Ce workshop est basé sur la version 2.6.6 de MongoDB.
Dézippez le bundle dans le dossier de votre choix, par exemple $HOME/progz/mongodb-2.6.6
.
Les exécutables nécessaires au fonctionnement de MongoDB se trouvent dans le dossier $HOME/progz/mongodb-2.6.6/bin
.
Pour plus de facilités, vous pouvez ajouter ce dossier à votre PATH
, afin que les commandes mongod
et mongo
soient directement accessibles.
Par exemple sous Linux, ajoutez les lignes suivantes à votre fichier .profile
:
# Path to MongoDB binaries
PATH="$HOME/progz/mongodb-2.6.6/bin:$PATH"
export PATH
Par défaut, MongoDB stocke ses données dans le dossier /data/db
. Cela peut être modifié via le paramètre --dbpath
Vous pouvez donc créer un dossier spécifique pour stocker les données du workshop, par exemple $HOME/data/nosql-workshop
:
mkdir -p "$HOME/data/nosql-workshop"
Démarrez MongoDB à l'aide de la commande suivante :
mongod --dbpath="$HOME/data/nosql-workshop"
MongoDB propose un shell Javascript interactif permettant de se connecter à une instance (démarrée via la commande mongod
, comme précédemment).
Pour lancer le shell :
mongo
Par défaut, le shell se connecte à l'instance localhost
sur le port 27017
, sur la base test
:
MongoDB shell version: 2.6.6
connecting to: test
Le shell met à disposition un objet Javascript db
qui permet d'interagir avec la base de données. Par exemple pour obtenir de l'aide :
db.help()
Pour visualiser les bases disponibles :
show dbs
Pour changer de base de données, par exemple workshop
(MongoDB crée automatiquement la base si elle n'existe pas) :
use workshop
Pour insérer un document dans une collection (la collection est créée automatiquement si elle n'existe pas encore) :
db.personnes.insert({ "prenom" : "Jean", "nom" : "DUPONT" })
Pour afficher un document :
db.personnes.findOne()
MongoDB génère automtiquement un identifiant unique pour chaque document, dans l'attribut _id
. Cet identifiant peut être défini manuellement :
db.personnes.insert({ "_id" : "jdupont", "prenom" : "Jean", "nom" : "DUPONT" })
Pour voir la liste des collections d'une base de données :
show collections
La méthode find()
possède deux paramètres (optionnels) :
- le critère de recherche
- la projection (les attributs à retourner)
Par exemple, pour rechercher toutes les personnes se nommant "DUPONT" :
db.personnes.find({ "nom" : "DUPONT" })
Si vous ne souhaitez retourner que les noms et prénoms, sans l'identifiant :
db.personnes.find({ "nom" : "DUPONT" }, {"_id" : 0, "nom" : 1, "prenom" : 1})
Il est également possible de renommer un attribut dans la projection. Par exemple pour renomme le "nom" en "nom_de_famille" :
db.personnes.find({ "nom" : "DUPONT" }, {"_id" : 0, "nom_de_famille" : "$nom", "prenom" : 1})
L'insertion d'un document se fait via la méthode insert()
, comme vu précédemment lors de la prise en main du Shell.
Le Shell étant un interpréteur Javascript, il est possible d'insérer plusieurs documents à l'aide d'une boucle for
:
for (var i = 1 ; i <= 100 ; i++) {
db.personnes.insert({ "prenom" : "Prenom" + i, "nom" : "Nom" + i, "age" : (Math.floor(Math.random() * 50) + 20) })
}
La mise à jour de documents se fait via la méthode update()
, qui possède plusieurs paramètres :
- le filtre permettant de sélectionner les documents à mettre à jour
- la requête de mise à jour
- des options (par exemple :
{"multi" : true}
pour mettre à jour tous les documents correspondant au filtre)
Par exemple, pour répartir les personnes dans deux catégories ("Master" pour les plus de 40 ans, "Junior pour les autres") :
db.personnes.update({"age" : { "$gte" : 40 }}, {"$set" : {"categorie" : "Master"}}, {"multi" : true})
db.personnes.update({"age" : { "$lt" : 40 }}, {"$set" : {"categorie" : "Junior"}}, {"multi" : true})
Remarque : MongoDB a créé automatiquement l'attribut "categorie" qui n'existait pas auparavant !
La méthode remove()
permet de supprimer des documents étant donné un filtre :
db.personnes.remove({ "nom" : "DUPONT" })
Pour supprimer une collection :
db.personnes.drop()
Il est possible d'utiliser des tableaux dans les documents. Par exemple, on peut socker les compétences des personnes de la manière suivante :
{
"_id": "jdupont",
"prenom": "Jean",
"nom": "DUPONT",
"competences" : [
"Java",
"Javascript",
"HTML"
]
}
Pour rechercher les personnes possédant la compétence "Java" :
db.personnes.find({ "competences" : "Java" })
Pour ajouter une compétence :
db.personnes.update({ "_id" : "jdupont" }, {"$push" : {"competences" : "CSS"}})
Pour éviter les doublons :
db.personnes.update({ "_id" : "jdupont" }, {"$addToSet" : {"competences" : "CSS"}})
Pour enlever une compétence :
db.personnes.update({ "_id" : "jdupont" }, {"$pull" : {"competences" : "CSS"}})
Avant de démarrer :
- ElasticSearch est un moteur de recherches distribué
- Il s'appuie sur une base de données NoSQL orientée documents
- Le format des documents est JSON
- ElasticSearch est basé sur la bibliothèque Lucene
Vous pouvez à tout moment vous référer à la documentation officielle.
Téléchargez la dernière version d'ElasticSaerch sur download.elasticsearch.org, ce workshop est basé sur la version 1.4.3.
Dézippez l'archive dans le dossier de votre choix, par exemple $HOME/progz/elasticsearch-1.4.3
.
ElasticSearch étant basé sur le langage Java, veillez à disposer de Java 8 installé sur votre machine. Vous pouvez vérifier l'installation de Java à l'aide de la commande java -version
.
Les exécutables nécessaires au fonctionnement d'ElasticSearch se trouvent dans le dossier $HOME/progz/elasticsearch-1.4.3/bin
. elasticsearch permet de lancer le server et plugin permet d'installer des plugins.
Avant de démarrer, installez les plugins suivants :
- head (administration)
plugin -i mobz/elasticsearch-head
- marvel (supervision & boite à outils)
plugin -i elasticsearch/marvel/latest
Le fichier $HOME/progz/elasticsearch-1.4.3/config/elasticsearch.yml
, au format YAML permet de configurer ElasticSearch, activez (décommentez) l'option suivante. Pour le reste, la configuration par défaut nous suffit pour l'instant.
discovery.zen.ping.multicast.enabled: false
Vous pouvez à présent démarrer le serveur.
elasticsearch
Il est possible d'ajouter des options Java pour augmenter la mémoire allouée à ElasticSearch en passant directement les paramètres de la JVM à l'exécutable ElsasticSearch.
elasticsearch -Xmx=2G -Xms=2G
Pour vérifier le démarrage de votre noeud ElasticSearch,
http://localhost:9200/
Vous devriez obtenir une réponse qui ressemble à celle là :
{
"status" : 200,
"name" : "Spinnerette",
"cluster_name" : "elasticsearch",
"version" : {
"number" : "1.4.3",
"build_hash" : "36a29a7144cfde87a960ba039091d40856fcb9af",
"build_timestamp" : "2015-02-11T14:23:15Z",
"build_snapshot" : false,
"lucene_version" : "4.10.3"
},
"tagline" : "You Know, for Search"
}
Pour accéder aux plugins précédemment installés il vous suffit de consulter les URLs suivantes :
Un document est un élément unitaire, au format JSON, stocké dans ElasticSearch.
Un noeud est une instance d'ElasticSearch, une noeud appartien à un cluster.
Un cluster est composé d'un ou plusieurs noeuds ElasticSearch qui sont connectés entre eux et qui partagent le même nom. Un cluster comporte un noeud maître unique (master node). En cas de défaillance du master node, un nouveau master node est élu parmis les noeuds restants.
Un index est un regroupement logique d'un ensemble de documents. Un index est composé de shards. Tous les documents appartienent à un index.
Un type est un sous-ensemble d'un index qui permet de regrouper des documents. De la même manière que pour les index, les types permettent de configurer le stockage des documents. Tout document appartient à un type.
Un shard est un fragment d'un index. Ce sont les shards qui permettent de partitionner les index sur plusieurs noeuds. Ainsi, un index peut être partitionner sur autant de noeuds que cet index comporte de shards. Le nombre de shards par défaut est de 5.
Une réplique est une copie intégrale d'un index. Les répliques permettent d'augmenter la tolérance à la panne du système ainsi que la durabilité des données. Une réplique comporte autant de shards que l'index original. Le nombre de répliques par défaut est de 1.
ElasticSearch expose l'ensemble de ses APIs à l'aide d'un API REST, il est donc possible d'utiliser n'importe quel client HTTP pour manipuler ElasticSearch.
Le plugin Marvel que nous avons installé plus tôt propose parmis ces outils, le client Sense qui offre des fonctionnalités qui facilitent l'utilisation d'ElasticSearch.
http://localhost:9200/_plugin/marvel/sense/index.html
Votre client préféré, si vous en avez un, fera sans problèmes l'affaire :D.
L'API d'ElasticSearch est composé d'un ensemble d'APIs qui exposent des opérations spécialisées. Cette API est conforme aux standards REST. Elle est orientée ressources, s'appuie sur les verbes HTTP, les codes de retours HTTP...
D'une manière générale, les requêtes ressemblent à ça :
http://[host]:[port]/index/type/_action|id
Par exemple :
localhost:9200/heroes/person/_search
localhost:9200/heroes/person/_count
localhost:9200/heroes/person/ironman
Les actions sur les index permettent généralement d'effectuer l'opération sur plusieurs index simultanément. Par exemple, pour effectuer une requête sur les index index1 et index2, il est possible d'utiliser l'URL suivante :
localhost:9200/heroes,vilains/_search
Vous trouverez de nombreux exemples (inclusions, exclusions, jokers, ...) dans la documentation.
Pour insérer un document, on utilise la requête suivante.
curl -XPOST 'localhost:9200/heroes/person/ironman' -d '{
"firstName" : "Tony",
"lastName" : "Stark"
}'
Le verbe, POST, indique qu'on insert un document L'URL est construite de la manière suivante :
<host>:<port>/<index>/<type>/<id>
Si l'index n'existe pas au moment de la création du document, celui-ci est créé automatiquement.
Pour l'insertion de données, les verbes POST et PUT sont équivalents. Le verbe POST permet d'insérer des documents sans spéficier l'identifiant du document.
curl -XPOST 'localhost:9200/heroes/person/' -d '{
"firstName" : "Charles",
"lastName" : "Xavier"
}'
La réponse renvoyée contient l'identifiant généré par ElasticSearch.
{
"_index": "heroes",
"_type": "person",
"_id": "AUuFm0z0oSZRHss7_tP7",
"_version": 1,
"created": true
}
Pour extraire un document à l'aide de son identifiant, on utilise la requête suivante.
curl -XGET 'localhost:9200/heroes/person/ironman'
La réponse renvoyée est la suivante :
{
"_index": "heroes",
"_type": "person",
"_id": "ironman",
"_version": 11,
"found": true,
"_source": {
"firstName": "Tony",
"lastName": "Stark"
}
}
L'attribut found
indique que le document a bien été trouvé (true
dans notre cas, false
si le document n'a pas été trouvé). L'attribut _source
contient le document extrait.
Pour mettre à jour les données, il est possible d'utiliser les requêtes PUT et POST présentées ci-dessus. Cette méthode permet de mettre à jour l'ensemble du document.
Il est possible d'effectuer des mises à jour partielles en utilisant l'API _update
.
curl -XPOST 'localhost:9200/heroes/person/ironman/_update' -d '{
"doc" : {
"firstName" : "Tomy"
}
}'
Pour supprimer un document, on utilise le verbe DELETE
curl -XDELETE 'localhost:9200/heroes/person/ironman'
Il est possible, à l'aide du verbe HEAD de vérifier l'existance d'un document.
curl -XHEAD 'localhost:9200/heroes/person/ironman'
Les statut renvoyé :
- 200 indique que le document existe
- 404 indique que le document n'existe pas
Commençons par insérer quelques données ...
curl -XPOST 'http://localhost:9200/heroes/person/ironman' -d '{"firstName":"Tony","lastName":"Stark","aka":"Iron Man","team":"Avengers","age":45}'
curl -XPOST 'http://localhost:9200/heroes/person/thor' -d '{"firstName":"Thor","lastName":"Odinson","aka":"Thor","team":"Avengers","age":27}'
curl -XPOST 'http://localhost:9200/heroes/person/antman' -d '{"firstName":"Hank","lastName":"Pym","aka":"Ant-Man","team":"Avengers","age":41}'
curl -XPOST 'http://localhost:9200/heroes/person/wasp' -d '{"firstName":"Janet","lastName":"van Dyne","aka":"Wasp","team":"Avengers","age":32}'
curl -XPOST 'http://localhost:9200/heroes/person/hulk' -d '{"firstName":"Bruce","lastName":"Banner","aka":"Hulk","team":"Avengers","age":41}'
curl -XPOST 'http://localhost:9200/heroes/person/misterfantastic' -d '{"firstName":"Reed","lastName":"Richards","aka":"Mister Fantastic","team":"FantasticFour","age":45}'
curl -XPOST 'http://localhost:9200/heroes/person/invisiblewoman' -d '{"firstName":"Susan","lastName":"Storm","aka":"Invisible Woman","team":"FantasticFour","age":29}'
curl -XPOST 'http://localhost:9200/heroes/person/thehumantorch' -d '{"firstName":"Johnny","lastName":"Storm","aka":"The Human Torch","team":"FantasticFour","age":27}'
curl -XPOST 'http://localhost:9200/heroes/person/thething' -d '{"firstName":"Ben","lastName":"Grimm","aka":"The Thing","team":"FantasticFour","age":42}'
L'API _search
permet d'effectuer des recherches dans ElasticSearch.
La requête suivante lance une recherche sur l'ensemble des documents de type person
dans l'index heroes
(par défault, une recherche remonte 10 résultats) :
curl -XPOST 'http://localhost:9200/heroes/person/_search'
La requête suivante permet de rechercher tous ls documents qui ont un attribut lastName
dont la valeur est storm
:
curl -XPOST 'http://localhost:9200/heroes/person/_search' -d '{
"query": {
"match": {
"lastName": "storm"
}
}
}'
La requête suivante permet d'effectuer une recherche sur les documents dont le prénom commence par un t
:
curl -XPOST 'http://localhost:9200/heroes/person/_search' -d '{
"query": {
"wildcard": {
"firstName": {
"value": "t*"
}
}
}
}
Il est possible de faire des recherches à plusieurs niveaux :
- sur un type et un index donnés :
localhost:9200/heroes/person/_search
- sur l'ensemble des types d'un index donné :
localhost:9200/heroes/_search
- sur l'ensemble des index d'un cluster :
localhost:9200/_search
Les agrégations permettent d'agglomérer des données et d'effectuer des calculs à la volée sur documents contenus dans les index.
Bien qu'il soit possible de combiner recherche et aggrégations, nous ne nous intéressons pas ici aux recherches (d'où l'attribut "size": 0
...).
Obtenir la répartition des valeurs du terme team
dans les documents de type person
de l'index heroes
:
curl -XPOST 'http://localhost:9200/heroes/person/_search' -d '{
"size": 0,
"aggs" : {
"teams" : {
"terms": {
"field": "team"
}
}
}
}
Il est possible de faire des sous-agrégations. Obtenir la répartition des valeurs du terme lastName
dans la répartion du terme team
dans les documents de type person
de l'index heroes
:
curl -XPOST 'http://localhost:9200/heroes/person/_search' -d '{
"size": 0,
"aggs" : {
"teams" : {
"terms": {
"field": "team"
},
"aggs" : {
"names" : {
"terms": {
"field": "lastName"
}
}
}
}
}
}
Il est possible de faire des calculs avec les aggrégations. L'âge moyen des membres de chaque équipe :
curl -XPOST 'http://localhost:9200/heroes/person/_search' -d '{
"size": 0,
"aggs" : {
"teams" : {
"terms": {
"field": "team",
"order" : { "avgAge" : "desc" }
},
"aggs" : {
"avgAge" : {
"avg" : {
"field" : "age"
}
}
}
}
}
}
L'objectif est de développer une application manipulants des données relatives aux installations sportives de la région Pays de la Loire.
Les données sont issues de http://data.paysdelaloire.fr.
Trois jeux de données vont particulièrement nous intéresser et sont disponibles dans le projet (format CSV) :
Des liens existent entre les trois jeux de données :
- une installation possède un ou plusieurs équipements
- une ou plusieurs activités peuvent être pratiquées sur un équipement donné.
La première tâche consiste à créer la collection des installations sportives à partir des trois fichiers CSV, en utilisant le driver MongoDB natif Java.
Pour cela, recherchez les TODO
dans le code du module batch
, complétez le code pour obtenir des documents de cette forme :
{
"_id": "440390003",
"nom": "La Pierre Tremblante",
"adresse": {
"numero": "",
"voie": "Chemin des rives",
"lieuDit": "",
"codePostal": "44640",
"commune": "Cheix-en-Retz"
},
"location": {
"type": "Point",
"coordinates": [
-1.816274,
47.181243
]
},
"multiCommune": false,
"nbPlacesParking": 0,
"nbPlacesParkingHandicapes": 0,
"dateMiseAJourFiche": ISODate("2014-06-18T00:00:00Z"),
"equipements": [
{
"numero": "191989",
"nom": "La Pierre tremblante",
"type": "Point d'embarquement et de débarquement isolé",
"famille": "Site d'activités aquatiques et nautiques",
"activites": [
"Canoë de randonnée",
"Pêche au coup en eau douce"
]
}
]
}
La seconde tâche consiste à implémenter les services Java utilisés par les pages web de l'application.
Pour cela, recherchez les TODO
dans le code du module application
et complétez le code manquant.
L'application web propose une page "API Checkup" permettant de vérifier que les services répondent correctement.
On peut par exemple positionner un index de type "text" sur le nom de l'installation et sa commune, en mettant un poids plus important pour la commune :
db.installations.ensureIndex(
{
"nom" : "text",
"adresse.commune" : "text"
},
{
"weights" : {
"nom" : 3,
"adresse.commune" : 10
},
"default_language" : "french"
}
)
Ensuite, on peut par exemple rechercher les "Ports" de la ville de "Carquefou", en triant par pertinance et en ne conservant que les 10 premiers résultats :
db.installations.find(
{
"$text": {
"$search": "Port Carquefou",
"$language" : "french"
}
},
{
"score": {"$meta": "textScore"}
}
)
.sort({"score": {"$meta": "textScore"}})
.limit(10)
#### Avec Elasticsearch
On crée d'abord le mapping. Dans cette première version, nous nous contenterons d'un mapping simple :
curl -XPOST 'http://localhost:9200/installations' -d '{
"mappings": {
"installation": {
"properties": {
"location": {
"properties": {
"coordinates": {
"type": "geo_point"
}
}
}
}
}
}
}'
Le job MongoDbToElasticsearch a pour objectif de gérer la copie des données de MongoDB à ElasticSearch. Nous ne cherchons pas ici à gérer une mise à jour incrémentale des données.
Nous souhaitons extraire l'ensemble des données de la collection installations
et les écrire dans l'index installations
(type installation
). Afin d'éviter pour le moment des problèmes de conversion de dates, nous filtrerons la propriété dateMiseAJourFiche
avant l'insersion dans ElasticSearch.
Une fois les documents indexés dans ElasticSearch, nous pouvons lancer recherche full text :
curl -XPOST 'http://localhost:9200/installations/installation/_search' -d '{
"query": {
"multi_match": {
"query": "Carquefou",
"fields": ["_all"]
}
}
}'
#### Avec MongoDB
Tout d'abord, il faut positionner un index géographique de type 2dsphere
sur l'attribut "location" de la collection des installations :
db.installations.ensureIndex( { "location" : "2dsphere" } )
Ensuite, si l'on souhaite rechercher les installations sportives autour de Carquefou (lat = 47.3, lon = -1.5), dans un rayon de 5km :
db.installations.find({ "location" :
{ $near :
{ $geometry :
{ type : "Point" ,
coordinates : [ -1.5 , 47.3 ]
},
$maxDistance : 5000
}
}
})
#### Avec Elasticsearch
Pour effectuer la même requête dans ElasticSearch :
curl -XPOST 'http://localhost:9200/installations/installation/_search' -d '{
"query" : {
"filtered" : {
"query" : {
"match_all" : {}
},
"filter" : {
"geo_distance" : {
"distance" : "5km",
"coordinates" : [ -1.5 , 47.3 ]
}
}
}
}
}'