ElasticSearch est une base de données documentaire libre se basant sur le serveur Apache Lucene. Les requêtes se font via le protocole HTTP et l'interface REST. Le requête PUT permet d'ajouter/modifier une entrée tandis que GET permet de la récupérer. L'échange de données se faisant via le format JSON.
Ce tutoriel a pour but de montrer comment installer basiquement le service ElasticSearch et réaliser des requêtes simples. Les paramètres techniques détaillées et les concepts d'architecture ou de modèle documentaire ne seront pas abordés ici. Pour obtenir des informations plus poussées, veuillez vous référer aux liens en fin de pages ou chercher des sites plus spécialisés.
Installez le paquet elasticsearch.
sudo apt-get install elasticsearch
Pour vérifier que le service est installé, rechercher le via :
service --status-all
Vous devriez voir une ligne elasticsearch :
[ - ] elasticsearch
En effet, si vous saisissez cette commande :
sudo service elasticsearch status
Alors vous devriez voir un active (exited) qui indique la commande de lancement a été exécutée mais que l'on est pas sur du status du service :
● elasticsearch.service - LSB: Starts elasticsearch Loaded: loaded (/etc/init.d/elasticsearch; bad; vendor preset: enabled) Active: active (exited) since dim. 2016-09-18 12:57:38 CEST; 15s ago Docs: man:systemd-sysv-generator(8) sept. 18 12:57:38 lubuntu-DEV systemd[1]: Starting LSB: Starts elasticsearch... sept. 18 12:57:38 lubuntu-DEV systemd[1]: Started LSB: Starts elasticsearch
Pour remédier à cela, il faut modifier le script "init.d" :
sudo nano /etc/init.d/elasticsearch
D'abord, trouver la ligne :
test "$START_DAEMON" = true || exit 0
et la commenter :
#test "$START_DAEMON" = true || exit 0
Puis rechercher cette ligne :
start-stop-daemon --start -b --user "$ES_USER" -c "$ES_USER" --pidfile "$PID_FILE" --exec $DAEMON -- $DAEMON_OPTS
et la modifier :
start-stop-daemon --start -b --user "$ES_USER" --pidfile "$PID_FILE" --exec $DAEMON -- $DAEMON_OPTS
D'un côté la variable START_DAEMON n'est pas utilisée dans la suite du script. Donc pourquoi forcer l'arret du script si elle n'existe pas ?
Ensuite, le groupe défini par la variable ES_USER ne semble pas le droit de lancer le démon alors que l'utilisateur référencer par la m^me variable lui le peut. Bizarre.
Maintenant le service doit pouvoir se lancer correctement. Pour le vérifier, exécutez :
sudo systemctl daemon-reload sudo service elasticsearch restart service elasticsearch status
Ce qui doit donner un active (running) qui indique la commande de lancement a été exécutée et que l'on a eu un retour positif:
● elasticsearch.service - LSB: Starts elasticsearch Loaded: loaded (/etc/init.d/elasticsearch; bad; vendor preset: enabled) Active: active (running) since dim. 2016-09-18 13:42:40 CEST; 18ms ago Docs: man:systemd-sysv-generator(8) Process: 7340 ExecStop=/etc/init.d/elasticsearch stop (code=exited, status=0/SUCCESS) Process: 7376 ExecStart=/etc/init.d/elasticsearch start (code=exited, status=0/SUCCESS) CGroup: /system.slice/elasticsearch.service ├─7151 /usr/lib/jvm/java-8-openjdk-i386/bin/java -Xms256m -Xmx1g -Djava.awt.headless=true -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupa └─7417 start-stop-daemon --start -b --user elasticsearch --pidfile /var/run/elasticsearch.pid --exec /usr/share/elasticsearch/bin/elasticsearch -- -d -p /var/run/elasticsearch.pid -Des.default.con sept. 18 13:42:40 lubuntu-DEV systemd[1]: Starting LSB: Starts elasticsearch... sept. 18 13:42:40 lubuntu-DEV elasticsearch[7376]: * Starting Elasticsearch Server sept. 18 13:42:40 lubuntu-DEV elasticsearch[7376]: ...done. sept. 18 13:42:40 lubuntu-DEV systemd[1]: Started LSB: Starts elasticsearch.
Il est maintenant possible d'interroger le serveur via la requête HTTP GET :
curl -X GET 'http://localhost:9200'
Qui va renvoyer un JSON :
{ "status" : 200, "name" : "Stature", "cluster_name" : "elasticsearch", "version" : { "number" : "1.7.3", "build_hash" : "NA", "build_timestamp" : "NA", "build_snapshot" : false, "lucene_version" : "4.10.4" }, "tagline" : "You Know, for Search" }
Il reste à créer la configuration minimale pour avoir un service opérationnel. Pour cela, ouvrir le fichier de configuration :
sudo nano /etc/elasticsearch/elasticsearch.yml
Dans la section Cluster, choisissez un nom pour le groupe. Par exemple :
cluster.name: elasticsearch
Dans la section Node, choisissez un nom pour le nœud. Par exemple :
node.name: "development"
Dans la section Index, paramétrez la répartition des données. Par exemple :
index.number_of_shards: 1 index.number_of_replicas: 0
Dans la section Network And HTTP, indiquez quelle plage du réseau est à écouter. Par exemple, pour tout écouter sans aucune restriction :
network.host: 0.0.0.0
Il ne reste plus qu'à relancer le service :
sudo service elasticsearch restart
Dans un base de données, il existe 4 opérations de base. Elle sont synthétisées sous l'acronyme CRUD :
De même, le protocole HTTP possède, entre autres, 4 méthodes :
Dans le cadre d'ElasticSearch, on peut donc faire le rapprochement :
Pour ajouter un enregistrement dans ElasticSearch, on peut prendre comme exemple un méthode PUT :
curl -XPUT "http://localhost:9200/movies/movie/1" -d' { "title": "The Godfather", "director": "Francis Ford Coppola", "year": 1972 }'
Avec :
movies
: Espace ou notre enregistrement sera stocké dans lequel sera (obligatoire)movie
: Pour affiner l'index (obligatoire). On pourra par exemple ajouter des réalisateurs à notre base de films1
: une étiquette unique associée à l'enregistrement (optionnel){}
On aura alors une réponse, elle aussi au format JSON, du type :
{"_index":"movies","_type":"movie","_id":"1","_version":1,"created":true}
Avec :
créé
à VRAI (évident pour une création)Avec la méthode POST, on peut faire l'opération similaire avec l'index 2 :
curl -XPOST "http://localhost:9200/movies/movie/2" -d' { "title": "Terminator", "director": "James Cameron", "year": 1984 }'
Pour avoir en retour :
{"_index":"movies","_type":"movie","_id":"2","_version":1,"created":true}
On peut créer un enregistrement avec la méthode POST sans spécifier d'ID :
curl -XPOST "http://localhost:9200/movies/movie" -d' { "title": "Star Wars", "director": "George Lucas", "year": 1977 }'
On reçoit donc un ID aléatoire (ici AVc-Cf49qZYpQV_XCKMq
) :
{"_index":"movies","_type":"movie","_id":"AVc-Cf49qZYpQV_XCKMq","_version":1,"created":true}
curl -XPUT "http://localhost:9200/movies/movie/" -d' { "title": "Alien", "director": " Ridley Scott", "year": 1979 }'
Va donner une erreur :
No handler found for uri [/movies/movie/] and method [PUT]
Il est possible de mettre à jour un enregistrement existant. Il faut alors refaire le même type de requête que pour une création, mais en utilisant un ID existant. Par exemple avec PUT, on peut ajouter les genres à un film :
curl -XPUT "http://localhost:9200/movies/movie/1" -d' { "title": "The Godfather", "director": "Francis Ford Coppola", "year": 1972, "genres": ["Crime", "Drama"] }'
On reçoit en réponse quelque chose de similaire à la création, mais :
créé
à FAUX (l'enregistrement existé déjà){"_index":"movies","_type":"movie","_id":"1","_version":2,"created":false}
Et avec POST :
curl -XPOST "http://localhost:9200/movies/movie/AVc-Cf49qZYpQV_XCKMq" -d' { "title": "Star Wars", "director": "George Lucas", "year": 1977, "genres": ["Action", "Adventure", "Fantasy", "Sci-Fi"] }'
On reçoit en réponse qui suit le même principe :
{"_index":"movies","_type":"movie","_id":"AVc-Cf49qZYpQV_XCKMq","_version":2,"created":false}
Maintenant que nous avons créé et modifié des enregistrements, il est facilement possible de les récupérer via la méthode GET en utilisant uniquement les IDs. Par exemple, pour récupérer notre premier film :
curl -XGET "http://localhost:9200/movies/movie/1"
On reçoit en réponse quelque chose de similaire à l'indexation, mais :
found
à VRAI (l'enregistrement existe){"_index":"movies","_type":"movie","_id":"1","_version":2,"found":true,"_source": { "title": "The Godfather", "director": "Francis Ford Coppola", "year": 1972, "genres": ["Crime", "Drama"] }}
Si l'on demande à récupérer un enregistrement inexistant :
curl -XGET "http://localhost:9200/movies/movie/19"
On reçoit en réponse quelque chose de similaire à l'indexation, mais :
found
à FAUX (l'enregistrement n'existe pas){"_index":"movies","_type":"movie","_id":"19","found":false}
Pour effacer un enregistrement, il suffit de connaitre son ID et d'utiliser la méthode DELETE. On a donc une commande assez proche de la lecture:
curl -XDELETE "http://localhost:9200/movies/movie/2"
On reçoit en réponse quelque chose de similaire à l'indexation, mais :
found
à VRAI (l'enregistrement existe){"found":true,"_index":"movies","_type":"movie","_id":"2","_version":2}
Du coup, si l'on refait une lecture de l'enregistrement :
curl -XGET "http://localhost:9200/movies/movie/2"
On remarque qu'il ne peut plus être trouvé :
{"_index":"movies","_type":"movie","_id":"2","found":false}
Donc, si on redemande à supprimer cette enregistrement une 2nd foix:
curl -XDELETE "http://localhost:9200/movies/movie/2"
On reçoit en réponse quelque chose de similaire à la 1ère suppression, mais :
found
à FAUX (l'enregistrement n'existe pas){"found":false,"_index":"movies","_type":"movie","_id":"2","_version":3}
Avant d'aller plus loin, il est nécessaire d'alimenter la base de données :
curl -XPUT "http://localhost:9200/movies/movie/1" -d' { "title": "The Godfather", "director": "Francis Ford Coppola", "year": 1972, "genres": ["Crime", "Drama"] }' curl -XPUT "http://localhost:9200/movies/movie/2" -d' { "title": "Lawrence of Arabia", "director": "David Lean", "year": 1962, "genres": ["Adventure", "Biography", "Drama"] }' curl -XPUT "http://localhost:9200/movies/movie/3" -d' { "title": "To Kill a Mockingbird", "director": "Robert Mulligan", "year": 1962, "genres": ["Crime", "Drama", "Mystery"] }' curl -XPUT "http://localhost:9200/movies/movie/4" -d' { "title": "Apocalypse Now", "director": "Francis Ford Coppola", "year": 1979, "genres": ["Drama", "War"] }' curl -XPUT "http://localhost:9200/movies/movie/5" -d' { "title": "Kill Bill: Vol. 1", "director": "Quentin Tarantino", "year": 2003, "genres": ["Action", "Crime", "Thriller"] }' curl -XPUT "http://localhost:9200/movies/movie/6" -d' { "title": "The Assassination of Jesse James by the Coward Robert Ford", "director": "Andrew Dominik", "year": 2007, "genres": ["Biography", "Crime", "Drama"] }'
On peut vérifier que tout est bien intégré :
{"_index":"movies","_type":"movie","_id":"1","_version":3,"created":false} {"_index":"movies","_type":"movie","_id":"2","_version":1,"created":true} {"_index":"movies","_type":"movie","_id":"3","_version":1,"created":true} {"_index":"movies","_type":"movie","_id":"4","_version":1,"created":true} {"_index":"movies","_type":"movie","_id":"5","_version":1,"created":true} {"_index":"movies","_type":"movie","_id":"6","_version":1,"created":true}
Le mot clé _search se place à la fin d'un chemin pour rechercher tous les enregistrements au niveau du chemin. Il est possible de rechercher respectivement dans tous :
curl -XGET "http://localhost:9200/_search"
curl -XGET "http://localhost:9200/movies/_search"
curl -XGET "http://localhost:9200/movies/movie/_search"
Comme nous n'avons qu'un seuls type et un seul index, le résultat sera sensiblement le même :
{"took":43,"timed_out":false,"_shards":{"total":1,"successful":1,"failed":0},"hits":{"total":7,"max_score":1.0,"hits":[{"_index":"movies","_type":"movie","_id":"AVc-Cf49qZYpQV_XCKMq","_score":1.0,"_source": { "title": "Star Wars", "director": "George Lucas", "year": 1977, "genres": ["Action", "Adventure", "Fantasy", "Sci-Fi"] }},{"_index":"movies","_type":"movie","_id":"1","_score":1.0,"_source": { "title": "The Godfather", "director": "Francis Ford Coppola", "year": 1972, "genres": ["Crime", "Drama"] }},{"_index":"movies","_type":"movie","_id":"2","_score":1.0,"_source": { "title": "Lawrence of Arabia", "director": "David Lean", "year": 1962, "genres": ["Adventure", "Biography", "Drama"] }},{"_index":"movies","_type":"movie","_id":"3","_score":1.0,"_source": { "title": "To Kill a Mockingbird", "director": "Robert Mulligan", "year": 1962, "genres": ["Crime", "Drama", "Mystery"] }},{"_index":"movies","_type":"movie","_id":"4","_score":1.0,"_source": { "title": "Apocalypse Now", "director": "Francis Ford Coppola", "year": 1979, "genres": ["Drama", "War"] }},{"_index":"movies","_type":"movie","_id":"5","_score":1.0,"_source": { "title": "Kill Bill: Vol. 1", "director": "Quentin Tarantino", "year": 2003, "genres": ["Action", "Crime", "Thriller"] }},{"_index":"movies","_type":"movie","_id":"6","_score":1.0,"_source": { "title": "The Assassination of Jesse James by the Coward Robert Ford", "director": "Andrew Dominik", "year": 2007, "genres": ["Biography", "Crime", "Drama"] }}]}}
Il est possible d'adjoindre une requète au format JSON après le mot-clé _search pour affiner les résultats. Il faut placer dans la requète les mots clés :
query
pour indiquer que l'on passe une requètequery_string
pour indiquer que l'on recherche du textequery : <MOTS CLES>
pour indiquer que l'on recherche tout les textes contenant les mots clésPar exemple, pour rechercher tous les films contenant le mot "kill", il faut faire :
curl -XPOST "http://localhost:9200/_search" -d' { "query": { "query_string": { "query": "kill" } } }'
Ce qui va donner :
{"took":49,"timed_out":false,"_shards":{"total":1,"successful":1,"failed":0},"hits":{"total":2,"max_score":0.5772806,"hits":[{"_index":"movies","_type":"movie","_id":"3","_score":0.5772806,"_source": { "title": "To Kill a Mockingbird", "director": "Robert Mulligan", "year": 1962, "genres": ["Crime", "Drama", "Mystery"] }},{"_index":"movies","_type":"movie","_id":"5","_score":0.5772806,"_source": { "title": "Kill Bill: Vol. 1", "director": "Quentin Tarantino", "year": 2003, "genres": ["Action", "Crime", "Thriller"] }}]}}
Le résultat peut-être décomposé en 3 parties :
"_shards":{"total":1,"successful":1,"failed":0}
"hits":{"total":2,"max_score":0.5772806,"hits":[...]
hits
. Ici, le 1er résultat : {"_index":"movies","_type":"movie","_id":"3","_score":0.5772806,"_source": { "title": "To Kill a Mockingbird", "director": "Robert Mulligan", "year": 1962, "genres": ["Crime", "Drama", "Mystery"] }}
En ajoutant la balise fields : <NOM DU CHAMP>
, il est possible d'avoir tous le texte mais venant d'un champ précis.
Ainsi, pour avoir tous les titres contenant le mot "ford" :
curl -XPOST "http://localhost:9200/_search" -d' { "query": { "query_string": { "query": "ford", "fields": ["title"] } } }'
Ce qui donne :
{"took":3,"timed_out":false,"_shards":{"total":1,"successful":1,"failed":0},"hits":{"total":1,"max_score":0.70398843,"hits":[{"_index":"movies","_type":"movie","_id":"6","_score":0.70398843,"_source": { "title": "The Assassination of Jesse James by the Coward Robert Ford", "director": "Andrew Dominik", "year": 2007, "genres": ["Biography", "Crime", "Drama"] }}]}
Le filtrage permet de restreindre une recherche à certaines conditions. Seuls les résultats respectant ces contraintes seront remontés. Pour cela, il faut ajouter à la fin de la requête les mots clés :
filter
pour indiquer un filtreterm
pour indiquer les termes des conditions"<CHAMP>" : <VALEUR>
pour indiquer les valeurs des champs à filtrerAinsi, pour avoir tous les film marqués comme des drames sortis en 1962, on aura :
curl -XPOST "http://localhost:9200/_search" -d' { "query": { "filtered": { "query": { "query_string": { "query": "drama" } }, "filter": { "term": { "year": 1962 } } } } }'
Ce qui va renvoyés 2 résultats au format déjà connu :
{"took":5,"timed_out":false,"_shards":{"total":1,"successful":1,"failed":0},"hits":{"total":2,"max_score":0.36067212,"hits":[{"_index":"movies","_type":"movie","_id":"2","_score":0.36067212,"_source": { "title": "Lawrence of Arabia", "director": "David Lean", "year": 1962, "genres": ["Adventure", "Biography", "Drama"] }},{"_index":"movies","_type":"movie","_id":"3","_score":0.36067212,"_source": { "title": "To Kill a Mockingbird", "director": "Robert Mulligan", "year": 1962, "genres": ["Crime", "Drama", "Mystery"] }}]}}
Il est possible d'avoir un filtre sans requête explicite. Par exemple, on peut ressortir l'ensemble des films datés de 1962 :
curl -XPOST "http://localhost:9200/_search" -d' { "query": { "constant_score": { "filter": { "term": { "year": 1962 } } } } }'
Ce qui donne :
{"took":3,"timed_out":false,"_shards":{"total":1,"successful":1,"failed":0},"hits":{"total":2,"max_score":1.0,"hits":[{"_index":"movies","_type":"movie","_id":"2","_score":1.0,"_source": { "title": "Lawrence of Arabia", "director": "David Lean", "year": 1962, "genres": ["Adventure", "Biography", "Drama"] }},{"_index":"movies","_type":"movie","_id":"3","_score":1.0,"_source": { "title": "To Kill a Mockingbird", "director": "Robert Mulligan", "year": 1962, "genres": ["Crime", "Drama", "Mystery"] }}]}}
curl -XPOST "http://localhost:9200/_search" -d' { "query": { "query_string": { "query": "1962", "fields": ["year"] } } }'
Il faut donc bien réfléchir à la façon dont seront faites les recherches et concevoir les requêtes les plus adaptés au contexte.
Pour plus de détails : Discussion FR et Discussion EN
Si l'on tente de rechercher tous les films de Francis Ford Coppola :
curl -XPOST "http://localhost:9200/_search" -d' { "query": { "constant_score": { "filter": { "term": { "director": "Francis Ford Coppola" } } } } }'
Aucun résultat ne sera retourné :
{"took":2,"timed_out":false,"_shards":{"total":1,"successful":1,"failed":0},"hits":{"total":0,"max_score":null,"hits":[]}}
En effet, ElasticSearch indexe les champs en utilisant le service Apache Lucene. Lucene décompose le champ en une série de mots. Ici, on a les 3 mots "francis", "ford" et "coppola". Le champ original "Francis Ford Coppola" est conservée sous la forme d'une _source
mais qui n'est pas indexée. Or, comme seules les données indexée sont recherchée par ElasticSearch, il faudrait avoir la requête :
curl -XPOST "http://localhost:9200/_search" -d' { "query": { "constant_score": { "filter": { "term": { "director": ["francis", "ford", "coppola"] } } } } }'
Pour obtenir :
{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"failed":0},"hits":{"total":2,"max_score":1.0,"hits":[{"_index":"movies","_type":"movie","_id":"1","_score":1.0,"_source": { "title": "The Godfather", "director": "Francis Ford Coppola", "year": 1972, "genres": ["Crime", "Drama"] }},{"_index":"movies","_type":"movie","_id":"4","_score":1.0,"_source": { "title": "Apocalypse Now", "director": "Francis Ford Coppola", "year": 1979, "genres": ["Drama", "War"] }}]}}
POST
_mapping
une nouvelle forme d'indexationnot_analyzed
: pour indiquer une champ sourceAinsi, pour les directeurs de film, on aura :
curl -XPUT "http://localhost:9200/movies/movie/_mapping" -d' { "movie": { "properties": { "director": { "type": "string", "index": "not_analyzed" } } } }'
Toutefois, ElasticSearch supporte mal la modification des indexes créés par defaut :
{"error":"MergeMappingException[Merge failed with failures {[mapper [director] has different index values, mapper [director] has different tokenize values, mapper [director] has different index_analyzer]}]","status":400} # new mapping creation
Il vaut mieux étendre l'index existant. Pour cela, on va utiliser :
type
d'extension (ici multi-field)original
)Ce qui donne la commande :
curl -XPOST "http://localhost:9200/movies/movie/_mapping" -d' { "movie": { "properties": { "director": { "type": "multi_field", "fields": { "director": {"type": "string"}, "original": {"type" : "string", "index" : "not_analyzed"} } } } } }'
On peut vérifier que l'exécution est faite sans erreur :
{"acknowledged":true}
Ce qui permet de réécrire finallement la requête voulue :
curl -XPOST "http://localhost:9200/_search" -d' { "query": { "constant_score": { "filter": { "term": { "director.original": "Francis Ford Coppola" } } } } }'
Qui va renvoyer le bon résultat :
{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"failed":0},"hits":{"total":2,"max_score":1.0,"hits":[{"_index":"movies","_type":"movie","_id":"1","_score":1.0,"_source": { "title": "The Godfather", "director": "Francis Ford Coppola", "year": 1972, "genres": ["Crime", "Drama"] }},{"_index":"movies","_type":"movie","_id":"4","_score":1.0,"_source": { "title": "Apocalypse Now", "director": "Francis Ford Coppola", "year": 1979, "genres": ["Drama", "War"] }}]}}