Menu
Comment bien débuter avec Elasticsearch

Comment bien débuter avec Elasticsearch

Par Sophie DEWAILLY

07.12.2022

Elasticsearch est un outil de base de données NoSQL couplé à un moteur de recherche. J'ai eu l'occasion de beaucoup l'utiliser dans mes missions, et je suis convaincue par l'architecture et les fonctionnalités qu'il propose. Si vous voulez un aperçu de tout ce qu'il est possible de faire, cet article vous proposera de nombreux exemples et j'espère qu'il vous aidera à faire le saut (à l'élastique ?).

 

Introduction

 

ELK (Elastic stack)

Nous allons nous focaliser sur Elasticsearch et les possibilités de stockage et de recherche qu'il offre, mais j'ouvre une parenthèse sur le fait que l'elastic stack, ou ELK, c'est aussi les outils Logstash et Kibana.

  • Logstash est un outil ETL, il permet de récolter des données provenant de nombreuses sources, et de les transformer pour pouvoir les insérer dans Elastic. C'est une porte d'entrée pratique pour recevoir des données diverses en temps réel, par exemple pour du monitoring (capteurs de températures d'un parc d'équipements,...).
  •  Kibana, d'un autre côté, est une interface utilisateur ultra pratique pour consulter, visualiser, transformer les données, ou bien surveiller l'état de santé du cluster Elasticsearch. La manière basique de communiquer avec Elasticsearch, c'est des requêtes REST avec des contenus JSON qui sont souvent complexes. Kibana propose des interfaces graphiques pour de nombreuses fonctionnalités Elasticsearch, ce qui permet une exploration très libre des features disponibles (gestion des indexes, cycle de vie...) et une visualisation très rapide des données stockées. Kibana propose par exemple une console avec autocomplétion (Dev Tools) pour écrire et exécuter les requêtes REST, ou encore des outils de création de graphiques branchés directement sur les données source.

 

Quickstart

Si vous avez envie de tester les produits par vous-mêmes, c'est très facile de créer une instance locale d'Elasticsearch et de Kibana sur votre ordinateur, grâce aux images Docker. Avec le docker-compose.yml suivant

    services:
  elasticsearch:
    container_name: es-container
    image: docker.elastic.co/elasticsearch/elasticsearch:7.11.0
    environment:
      - xpack.security.enabled=false
      - "discovery.type=single-node"
    networks:
      - es-net
    ports:
      - 9200:9200
  kibana:
    container_name: kb-container
    image: docker.elastic.co/kibana/kibana:7.11.0
    environment:
      - ELASTICSEARCH_HOSTS=http://es-container:9200
    networks:
      - es-net
    depends_on:
      - elasticsearch
    ports:
      - 5601:5601
networks:
  es-net:
    driver: bridge

et la commande docker-compose up -d, vous aurez, à l'adressehttp://localhost:5601, un Kibana prêt à l'usage. Pour les commandes proposées dans cet article, naviguez vers le lien "Dev tools" présent sur la page d'accueil.

 

Insérer les premières données

Elasticsearch stocke ses données dans des index, qui peuvent être comparés très superficiellement aux tables SQL. En revanche, le format des données est le JSON, et rien n'est plus simple que d'indexer un document JSON:
    POST hello_world/_doc
{
  "language": "Scala",
  "age": 18
}
Cette commande va créer un index nommé hello_world contenant le document. Lors de la prochaine commande pour un autre langage de programmation, le document sera directement inséré comme entrée supplémentaire dans hello_world. Elasticsearch n'a pas besoin de savoir quel champ est de quel type, il peut déterminer le type automatiquement lors de l'indexation.

Pour rechercher, on peut exécuter la commande GET hello_world/_search.

Je vais rajouter quelques documents dans l'index, et nous pourrons ensuite regarder comment effectuer des recherches.
    POST hello_world/_doc
{ "language": "C++", "age": 37 }

POST hello_world/_doc
{ "language": "C#", "age": 22 }

Consulter les données

Recherche

Vous savez maintenant comment insérer des données dans vos indexes et les consulter. Voici un tour d'horizon des features disponibles pour mettre en place des filtres et calculs sur vos données, avec une attention portée sur les écueils classiques et les pièges que rencontrent souvent les débutants confrontés à la complexité cachée d'Elasticsearch.

Comme les documents stockés sous forme de JSON, les requêtes sont également sous forme de JSON. Elles peuvent paraître intimidantes au premier abord et sont parfois lourdes à écrire, mais avec l'auto-complétion de la console Kibana et une connaissance des briques de base, vous deviendrez vite familier de leur format. Je vous montre celles que j'utilise le plus régulièrement :

  • Range : données (numériques ou date) entre deux bornes
      GET hello_world/_search
  {
    "query": {
      "range": {
        "age": {
          "gte": 10,
          "lte": 20
        }
      }
    }
  }
  ```
- Match : filtrer sur la valeur d'un champ 

  {
    "query": {
      "match": {
        "language": "C++"
      }
    }
  }

 

  • Match sur des champs textes: Elasticsearch propose plusieurs types de recherche, tels que regexp (expression régulière), match_phrase_prefix (pour trouver des champs avec un certain préfixe)...

Vous avez maintenant les clés en main pour écrire vos propres requêtes... mais il s'est passé des choses étranges avec la query match ! Cela est dû au type de la donnée.

Mapping

Chaque index elastic possède ce que l'on appelle un mapping, qui définit le type de chaque champ. C'est en maîtrisant ce mapping et les différents types de données que vous aurez la possibilité de faire des recherches puissantes et complexes. Pour consulter le mapping d'un index, on utilise GET hello_world/_mapping .Pour configurer le type d'un champ dans un index à l'avance, on peut utiliser après avoir créé un index vide (PUT new_index/) la syntaxe:

    PUT /new_index/_mapping
{
  "properties": {
    "age": {
      "type": "double"
    }
  }
}


Voici des exemples de types importants concernant les champs textes, et leurs cas d'usages.

  • Le type text, qui est le type qu'Elasticsearch assigne par défaut lorsqu'il reçoit des string, permet de faire des recherches textuelles déjà analysées : Elasticsearch découpe la chaîne de caractères en "mots" en utilisant des délimiteurs (tels que ` `, `-`, `)`...), et quand un match est fait sur ce champ, Elasticsearch compare les "mots" de la recherche avec les "mots" de l'index. C'est de la recherche fuzzy. C'est à cause du type texte par défaut que, lorsqu'on a fait la recherche sur C++, on a retrouvé le document contenant C# : car les "mots" du match étaient en fait juste C.
  • Si vous avez besoin d'indexer des champs textes de manière à pouvoir faire des recherches exactes simplement (identifiants, catégories...), il faut utiliser un champ de type keyword. Une requête match sur ces champs fera un filtre exact.
  • Si vous voulez que le champ puisse dans certains cas être recherché de manière exacte, et dans d'autres avec le découpage en mots, vous pouvez utiliser un multi-field, en le déclarant de la manière suivante :

 

      PUT /new_index/_mapping
  {
    "mappings": {
      "properties": {
        "city": {
          "type": "text",
          "fields": {
            "keyword": { 
              "type":  "keyword"
            }}}}}}

Il sera possible de faire des requêtes match exactes sur le champ "city.keyword" et des requêtes match textuelles sur le champ "city" (par exemple pour trouver toutes les city nommées "New x").

  •  Pour faire de la recherche textuelle pointue sur les champs de type text, il est possible de définir un analyzer avec lequel analyser la valeur des champs. Un analyzer contient un tokenizer, qui va découper le texte en entrée en un ensemble de tokens (l'exemple le plus simple est le mot, mais on peut également gérer la casse minuscule/majuscule, ou créer toutes les chaînes de caractères de taille N inclues dans chaque mot afin de faire de la recherche dynamique...). Un analyzer contient également un filter, qui permet d'ignorer certains mots (par exemple des stopwords tel que "la", "et", "the"...), ou bien de configurer une liste de synonymes pour ce champ, synonymes qui seront utilisés pour les futures recherches.

Si vous indexez vos documents sans vous soucier du mapping, Elasticsearch choisira par lui-même un type par défaut. Cela peut donner de mauvaises surprises. Par exemple, avec la manipulation suivante


    POST no_config_index/_doc
{"value": 0}

POST no_config_index/_doc
{"value": 3.2}

GET no_config_index/_search
{"query": {"match": {"value": 3.2}}}

Le résultat du search sera vide, car Elasticsearch a mappé le champ value en type "long" : la première valeur qu'il a reçu, 0, était de type "long". Le 3.2 est donc interprété en 3 et ne ressort pas lors de la recherche.

De plus, une fois que le type d'un champ est défini dans un index, ce n'est pas possible de le changer sans passer par une réindexation des données, qui peut être un processus coûteux dès que le volume de données est conséquent. Assurez-vous donc que chaque champ est typé pour être cohérent avec les utilisations que vous en ferez.

Un exemple d'utilisation très pratique d'Elasticsearch est les agrégations.

 

Agrégations

En plus d'extraire les documents correspondant à une requête, Elasticsearch permet d'effectuer des agrégations sur les documents. Le terme englobe de nombreuses possibilités: GROUP BY à la manière SQL, calcul de métriques et statistiques, histogrammes... Encore une fois, je vous montre celles que j'ai le plus utilisées, et également la manière de les combiner pour calculer, par exemple, la moyenne du prix de la PS5 par jour et par vendeur.

Une agrégation peut s'utiliser avec une query, voici un squelette de requête utilisant les deux :
    GET data/_search
{"query": {...},
"aggs": {...}
}

  • Les aggrégations "metrics" permettent de faire des statistiques sur vos données. Voici un exemple d'aggrégation avec le max :
      GET data/_search
  {
  "aggs": {
    "myOwnMaxValue": {
      "max": {
        "field": "value"
      }
    }
  }
  }

  •  Les "bucket" aggrégations servent à regrouper les documents dans des buckets, par exemple selon :
    • des intervalles (numériques ou dates) : "range", "date_histogram". La recherche suivante compte le nombre de documents par mois, et crée un bucket par mois, bucket sur lequel d'autres aggrégations peuvent être faites (cf exemple final).
        {
      "aggs": {
        "myDateHisto": {
          "date_histogram": {
            "field": "date",
            "interval": "month"
          }
        }
      }
    }

  • la catégorie d'un champ de type keyword (un peu comme un GROUP BY): "terms". Par exemple, pour trouver le nombre de documents par city (city doit être de type keyword)
        {
      "aggs": {
        "myTerms": {
          "terms": {
            "field": "city"
          }
        }
      }
    }
Et finalement, voici comment imbriquer des aggrégations, avec l'exemple de la moyenne du prix de la PS5 par jour et par vendeur (on suppose qu'on collecte les données par jour pour différents sites de vente en ligne).
    GET _search
{
  "aggs": {
    "parJour": {
      "date_histogram": {
        "field": "date",
        "interval": "day"
      },
      "aggs": {
        "parVendeur": {
          "terms": {
            "field": "website"
          },
          "aggs": {
            "prixMoyen": {
              "avg": {
                "field": "price"
              }}}}}}}}

Pour aller plus loin

Nous avons exploré comment manipuler les données grâce à Elasticsearch. Vous avez des solides bases pour créer vos propres indexes et rechercher vos données de manière pointue. Je vais maintenant prendre le temps de vous expliquer, de manière un peu plus théorique, comment Elasticsearch peut vous aider à gérer le "Big Data" grâce à la scalability des données.

Avec nos trois indexes et 10 documents, pas besoin de s'inquiéter. Mais pour gérer des données réelles, il est courant de faire face à des Téras de données. Elasticsearch nous propose plusieurs outils pour gérer le stockage et la vie de ces données. Voici un tour d'horizon des concepts que j'ai eu l'occasion d'appliquer.

 

  • Le système de stockage de données d'Elasticsearch est clair et manipulable. Un cluster Elasticsearch possède un ou plusieurs nœuds. Chaque index est découpé en une ou plusieurs shards: chaque shard contient donc une partie des données de l'index, et est assignée de manière indifférente à un certain nœud. Et on peut contrôler lors de la création d'un index sur combien de shards il est distribué, ainsi que la taille maximale qu'une shard peut atteindre (la taille recommandée pour une shard est entre 10 et 50 Go). Un dernier concept : les replicas. Un index a des primary shards et peut posséder plusieurs replica shards, qui sont stockées sur des nœuds différents de la primary qui leur correspond. C'est donc un mécanisme de backup intégré, et on peut ajouter ou supprimer des replicas à tout moment sur un index.
  •  Un alias représente un groupe d'indexes (un GET myalias/_search renverra des documents de tous les indexes sur lesquels l'alias pointe). On peut créer un index nommé test-0001 associé à l'alias test, et si on rollover l'alias, l'index test-0002 sera créé. L'alias test pointe en écriture sur le deuxième index et en lecture sur l'index 1 et 2.
  • Pour gérer la vie de la donnée, Elasticsearch propose plusieurs outils sous le nom d'ILM : index lifecyle management. On peut définir une ILM policy sur des indexes. Cette policy définit des phases et des transitions. Une phase (hot, warm, cold, delete) définit le mode écriture ou lecture seule, la manière dont sont stockées les données et donc la performance de la recherche sur ces données. Une transition peut se faire selon plusieurs critères (âge de l'index, taille de l'index, taille des réplicas...). En phase "hot", les indexes peuvent avoir du rollover. En combinant toutes ces notions, on peut donc créer un index tous les jours, et demander à Elasticsearch de supprimer les indexes au bout de 30 jours.
  • Si vous voulez supprimer l'index mais garder une vue plus générale des données supprimées, c'est également possible grâce à un transform. Voici un petit cas d'usage : on possède une API quelconque avec de nombreux endpoints et utilisateurs. Chaque appel à cette API est loggé dans un alias Elasticsearch avec la date et l'heure, l'utilisateur, et l'endpoint concerné. Pour éviter de garder trop de données unitaires, on supprime les indexes après 3 mois. Mais on a également demandé à Elasticsearch d'appliquer un transform agrégeant le nombre total d'appels à chaque endpoint par jour et par utilisateur, et de le stocker dans un index à part. Ainsi, on contrôle la volumétrie de ce nouvel index et on peut automatiser la sauvegarde des statistiques tout en supprimant les données source.

C'est tout pour cet aperçu des features de gestion des données, j'espère vous avoir donné les pistes pour comprendre que l'on peut vraiment mettre en place de nombreux outils pour contrôler le stockage et le volume des données indexées dans Elasticsearch.

 

Le mot de la fin


Elasticsearch propose donc un stockage de données et un moteur de recherche très puissant et modulaire, une fois que les "conditions d'utilisation" ont été comprises pour en tirer le maximum de performance. Tous les exemples que je vous ai montrés constituent une bonne base pour commencer à profiter des possibilités quasi-infinies de cet outil : le reste est entre vos mains, et n'oubliez pas :

Elastic, c'est fantastique !

 

Retour aux articles

C'est à lire...