Indexation et recherche de documents MongoDB avec ElasticSearch

Après avoir alimenté une base MongoDB avec un job Spring Batch, et décrit les principes du moteur de recherche ElasticSearch, nous allons maintenant combiner le tout pour indexer les documents et faire des recherches.

Installation du plugin elasticsearch-river-mongodb

Afin d’indexer les documents stockés dans MongoDB, nous allons installer un autre plugin de type ‘river’. Les plugins de type ‘river’ permettent de faire des indexations automatiques à partir d’une source de données. Dans notre cas une base MongoDB. Une liste des rivers est disponible sur ce lien : http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/modules-plugins.html. On pourrait par exemple indexer des tweets, des fichiers csv, une base MySQL, …

En ce qui concerne notre use-case nous allons installer le plugin elasticsearch-river-mongodb. Attention les versions de MongoDB et ElasticSearch doivent correspondre avec la version du plugin. Pour ma part voici les versions utilisées :

  • MongoDB : 2.6.5
  • ElasticSearch : 1.4.1
  • Plugin : 2.0.4

Installation à faire sur les 2 instances d’ElasticSearch :

plugin.bat --install com.github.richardwilly98.elasticsearch/elasticsearch-river-mongodb/2.0.4

La petite console qui va bien après redémarrage : http://localhost:9200/_plugin/river-mongodb/ (où on ne voit rien pour le moment c’est normal)

Démarrage de MongoDB en mode replicaSet

Le plugin elasticsearch-river-mongodb ne fonctionnera que si Mongo est en mode Replica Set (même s’il n’y a qu’une seule instance en cours). En ligne de commande :

mongod --port 27017 --dbpath K:\Dev\dataMongoDB1 --replSet rs0

On a dans les logs la ligne suivante :

2014-12-13T15:10:53.784+0100 [rsStart] replSet can’t get local.system.replset
config from self or any seed (EMPTYCONFIG)

Pas d’inquiétude. Maintenant nous allons indiquer à MongoDB qu’il n’y a qu’une seule instance (celle qui tourne sur localhost:27017). Dans un shell mongo (commande mongo) on tape :

cfg = {
   "_id" : "rs0", 
   "version" : 1, 
   "members" :
      [ { "_id" : 0, 
          "host" : "localhost:27017" 
      } ] 
}
rs.initiate(cfg)

On attend que MongoDB ait fini l’initialisation en mode ReplicaSet :

2014-12-13T15:22:13.191+0100 [FileAllocator] done allocating datafile K:\Dev\
dataMongoDB1\local.9, size: 2047MB, took 32.377 secs
2014-12-13T15:22:13.308+0100 [conn1] ExtentManager took 32 seconds to open: K:
\Dev\dataMongoDB1\local.9
..
2014-12-13T15:22:28.064+0100 [rsStart] replSet I am localhost:27017
2014-12-13T15:22:28.173+0100 [rsStart] build index on: local.me properties:
{ v: 1, key: { _id: 1 }, name: "_id_", ns: "local.me" }
2014-12-13T15:22:28.173+0100 [rsStart] added index to empty collection
2014-12-13T15:22:28.173+0100 [rsStart] replSet STARTUP2
2014-12-13T15:22:28.204+0100 [rsSync] replSet SECONDARY
2014-12-13T15:22:28.221+0100 [rsMgr] replSet info electSelf 0
2014-12-13T15:22:29.179+0100 [rsMgr] replSet PRIMARY
...

Injection de données en base

J’utilise le batch développé dans mon précédent article, pour injecter une vingtaine de villes en base (base donnée : « testblog », collection : « city ») : mongoTest1

Configurer le plugin river pour indexer les villes

Nous allons créer l’indexe blog2 avec des documents de type city2, à partir de la base de données testblog et de la collection city. On utilise pour cela l’API REST :

curl -XPUT "localhost:9200/_river/blog2/_meta" -d '
{
  "type": "mongodb",
  "mongodb": {
     "servers": [
       { "host": "127.0.0.1", "port": 27017 }
     ],
    "options": { "secondary_read_preference": true},
    "db": "testblog",
    "collection": "city"
  },
 "index": {
   "name": "blog2",
   "type": "city2"
 }
}'

Dans les logs d’ElasticSearch on peut voir :

[INFO ][org.elasticsearch.river.mongodb.MongoDBRiver] Starting river blog2
[INFO ][org.elasticsearch.river.mongodb.MongoDBRiver] MongoDB River Plugin
- version[2.0.4]
[INFO ][org.elasticsearch.river.mongodb.MongoDBRiver] starting mongodb stream.
options: ...
db [testblog], collection [city], script [null], indexing to [blog2]/[city2]
[INFO ][river.mongodb.util ] setRiverStatus called with blog2 - RUNNING
[INFO ][river.mongodb ] [blog-02] Creating MongoClient for [[127.0.0.1:27017]]
[INFO ][org.elasticsearch.river.mongodb.MongoConfigProvider] MongoDB version
- 2.6.5
[INFO ][org.elasticsearch.river.mongodb.CollectionSlurper] MongoDBRiver is
beginning initial import of testblog.city
[INFO ][org.elasticsearch.river.mongodb.CollectionSlurper]
Number of documents indexed in initial import of testblog.city: 20

Et dans la console MongoDB river (http://localhost:9200/_plugin/river-mongodb/) nous avons :

Console MongoDB River
Console MongoDB River

Si on supprime/modifie/ajoute un élément, l’indexe est automatiquement mis à jour.

Exemples de recherche

 

Chercher la ville « Visanseny » (recherche « full »)

curl -XGET 'http://localhost:9200/blog2/city2/_search?q=Visanseny&pretty=true'

Une réponse :

{
   "took":2,
   "timed_out":false,
   "_shards":{
      "total":5,
      "successful":5,
      "failed":0
   },
   "hits":{
      "total":1,
      "max_score":0.2635247,
      "hits":[
         {
            "_index":"blog2",
            "_type":"city2",
            "_id":"3038827",
            "_score":0.2635247,
            "_source":{
               "admin2Code":"",
               "timezone":"Europe/Andorra",
               "latitude":42.56667,
               "admin1Code":"0",
               "dem":1913,
               "population":"0",
               "admin3Code":"",
               "alternateNames":[
                  "Visanceny",
                  "Visanseny"
               ],
               "modificationDate":"0023-07-02T23:00:00.000Z",
               "feature":{
                  "featureCode":"LCTY",
                  "featureClass":"L"
               },
               "countryCode":"AD",
               "name":"Visanseny",
               "asciiname":"Visanseny",
               "_id":3038827,
               "_class":"bean.City",
               "alternateCountryCodes":[
                  "AD"
               ],
               "admin4Code":"",
               "longitude":1.61667
            }
         }
      ]
   }
}

On remarque que cette ville a une liste de noms alternatifs. Nous allons donc chercher son autre orthographe Visanceny :

curl -XGET 'http://localhost:9200/blog2/city2/_search?q=Visanceny&pretty=true'

Une réponse également ce qui montre qu’ElasticSearch permet de rechercher dans des champs simples (String) et des champs plus complexes (liste de String).

Recherche approximative

Si on ne connaît que le début du nom de la ville :

curl -XGET 'http://localhost:9200/blog2/city2/_search?q=Visan*&pretty=true'

Une réponse également !

Recherche sur un champ précis

On cherche toutes les villes dont le featureCode (contenu dans feature) est LCTY :

curl -XGET 'http://localhost:9200/blog2/city2/_search?q=feature.featureCode:LCTY'

Une réponse également !

Recherche avancée

Il est possible de faire des requêtes beaucoup plus complexes.

Exemple : je veux rechercher toutes les villes qui contiennent « vista », mais je veux exclure celles dont le nom contient « obaga ».

Le plugin ElasticSearch Head permet de construire facilement ce genre de requête. On passe alors par une requête REST en POST :

Exemple Recherche Complexe

Recherche par wildcard ou regex

On peut vouloir chercher les villes par wildcard en utilisant les caractères :

  • * : correspond à zéro ou plusieurs caractères, y compris le caractère espace
  • ? : correspond à un seul caractère

Exemple :

{
   "query":{
      "wildcard":{
         "name":"Co?l de *"
      }
   }
}

La recherche par regex permet d’être encore plus précis. On peut rechercher par exemple toutes les villes dont le featureCode commence par LCT et finit par une lettre entre A et F :

{
   "query":{
      "regexp":{
         "feature.featureCode":"LCT[A-F]"
      }
   }
}

Utilisation des « facets » ou « aggregations »

Avec 19 villes indexées il est facile de trouver rapidement celle que l’on cherche. Mais avec 9 millions, cela sera différent.

Je vais donc, lors de mes requêtes, utiliser les « aggregations » pour regrouper mes résultats. Les requêtes par « facet » ont été remplacées par les aggregations dans ElasticSearch.

Dans cet exemple je recherche « vista » et je regroupe par feature.featureCode :

{
   "query":{
      "bool":{
         "must":[
            {
               "query_string":{
                  "default_field":"_all",
                  "query":"vista"
               }
            }
         ],
         "must_not":[

         ],
         "should":[

         ]
      }
   },
   "from":0,
   "size":10,
   "sort":[

   ],
   "aggs":{
      "featureCodes":{
         "terms":{
            "field":"feature.featureCode"
         }
      }
   }
}

La réponse remonte 4 villes ainsi que les résultats de l’agrégation :

{
   "aggregations":{
      "featureCodes":{
         "doc_count_error_upper_bound":0,
         "sum_other_doc_count":0,
         "buckets":[
            {
               "key":"pass",
               "doc_count":1
            },
            {
               "key":"rk",
               "doc_count":1
            },
            {
               "key":"slp",
               "doc_count":1
            },
            {
               "key":"spur",
               "doc_count":1
            }
         ]
      }
   }
}

On sait donc qu’il y a une ville pour chaque code : pass, rk, slp, spur.

Pour avoir le document correspondant, il suffirait de refaire la recherche avec ce nouveau critère. C’est un procédé très utilisé sur les sites de commerces où les produits sont regroupés par types, par marque, …

Dans cet exemple j’ai agrégé des donnés sur un terme. Mais on peut le faire sur une période, sur des coordonnées géographiques, etc.

D’autres exemples sont disponibles sur le site d’ElasticSearch : http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-aggregations.html

Performances

Pouvoir indexer quelques documents sans une ligne de code et faire des recherches complexes est déjà une bonne chose. Mais si on peut le faire sur des millions de documents c’est encore mieux.

J’ai donc dans un premier temps enregistrer en base MongoDB 9 millions de villes dans une nouvelles base testperf1.

Puis j’ai configuré une « river » pour indexer cette nouvelle base dans un nouvel index blog3 :

curl -XPUT "localhost:9200/_river/blog3/_meta" -d '
{
  "type": "mongodb",
  "mongodb": {
     "servers": [
       { "host": "127.0.0.1", "port": 27017 }
     ],
    "options": { "secondary_read_preference": true},
    "db": "testperf1",
    "collection": "city"
  },
 "index": {
   "name": "blog3",
   "type": "city3"
 }
}'

Résultats

[2014-12-14 17:12:47,105]... MongoDBRiver is beginning initial import of testperf1.city
[2014-12-14 18:26:17,971]... Number of documents indexed in initial import of testperf1.city: 8975385

En un peu plus d’une heure, toute la base a été indexée.

Le plugin BigDesk, montre que les documents ont été uniformément indexés et répliqués dans l’indexe blog3 :

BigDesk Indexation Totale

En faisant une recherche sur Paris en ajoutant 2 agrégations (une sur la timezone et l’autre sur featureCode), la première requête prends entre 1 et 4s.

Un peu long, mais ElasticSearch met en cache un certain nombre d’informations.

En faisant la même requête en changeant « Paris » en « Vienne », la requête met 13ms et remonte 96 résultats, ce qui est plus que raisonnable !

"took": 13,
"timed_out": false,
"_shards": {
  "total": 5,
  "successful": 5,
  "failed": 0
},
"hits": {
"total": 96,

Autres pistes à creuser

Il y aurait encore beaucoup à dire sur ElasticSearch ! Si cet outil vous intéresse les sujets suivants pourraient vous être utiles :

Mapping

Par défaut lorsqu’on indexe un document, ElasticSearch définit un mapping par défaut :

  • Les types de champs : String, date, …
  • Si ces champs doivent servir à la recherche ou juste être stockés

Vous pouvez accéder au mapping de vos données avec la requête :

curl -XGET 'http://localhost:9200/blog3/city3/_mapping?pretty=true'

On peut modifier ce mapping pour améliorer la recherche : http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html

Analysers

La meilleure manière d’avoir une recherche qualitative est d’utiliser les bons « analysers » lors de l’indexation et lors de la recherche.

Si mes documents avait contenu beaucoup de texte en français, il aurait été conseillé d’utiliser le « french analyser » :

  • Les articles français ne sont pas indexés (le, la, …) car ils ne servent à rien
  • La recherche avec ou sans accent fonctionne correctement

La liste des analysers est ici : http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/analysis-analyzers.html

Pour tester comment fonctionne les différents analysers, on peut utiliser l’API analyse :

curl 'http://localhost:9200/blog3/_analyze?tokenizer=letter&filters=asciifolding,lowercase&pretty=true' -d "l'élève"

Qui donne :

{
 "tokens" : [ {
   "token" : "l",
   "start_offset" : 0,
   "end_offset" : 1,
   "type" : "word",
   "position" : 1
 }, {
   "token" : "eleve",
   "start_offset" : 2,
   "end_offset" : 7,
   "type" : "word",
   "position" : 2
 } ]
}

Si on cherche « eleve » dans un document qui contient « l’élève », ce document remontera bien car nous avons correctement configuré les analysers.

Alias

Définir un alias pour un indexe permet de facilement « switcher » d’indexe sans interruption de service.

C’est donc une bonne pratique à adopter : http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-aliases.html

Kibana et Loggly

Ces 2 outils utilisent ElasticSearch pour indexer et permettre la recherche dans… des logs !

Kibana : http://www.elasticsearch.org/overview/kibana/

Loggly : https://www.loggly.com/

Conclusion

Après quelques tests, Elasticsearch s’avère très performant dans le domaine de l’indexation et de la recherche. Que ce soit au niveau des temps de réponse ou de la qualité de réponse.

Il faut aussi mettre en avant la facilité de mise en place pour une indexation standard.

Un niveau plus avancé sera requis pour calibrer l’indexeur aux documents à structure plus complexes tels que des logs de serveurs, des textes en japonais ou encore un catalogue d’albums de musique.

ElasticSearch est pour moi un outil incontournable à l’heure du BigData !

2 réflexions au sujet de « Indexation et recherche de documents MongoDB avec ElasticSearch »

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *