Qu’est ce que Elasticsearch ? Pourquoi devrais-je l’intégrer à mon système ? Si vous vous posez ces questions, ce billet vous apportera une partie des réponses !

Ce sera le premier article d’une série, qui vous permettra d’obtenir une application fullstack de moteur de recherche musicale à.

Vous apprendrez ici à :

  • Mettre en place un cluster Elasticsearch basique
  • Créer un index Elasticsearch pour vos fichiers musicaux
  • Implémenter une recherche de type autocomplete sur les fichiers musicaux indexés

Qu’est ce que Elasticsearch ?

Elasticsearch, c’est à la fois un moteur d’analyse sémantique, de recherche en temps réel – permettant d’effectuer des recherches full-text ou une recherche auto-complétée, basé sur Apache Lucene. C’est également un système distribué et scalable.

Pourquoi l’utiliser ?

C’est en effet une question que vous êtes en droit de vous poser : à quoi me servirait un système aussi couteux en ressources ? C‘est ce que nous allons voir par la suite ! De l’importation de vos premières données dans la base à leur interprétation, en passant par leur analyse sémantique, jusqu’à leur affichage dans un dashboard, vous aurez toutes les billes pour commencer à comprendre ce merveilleux outil.  

La stack ELK

Commençons par découvrir les différents outils qui composent la stack ELK :

  • Elasticsearch, qui va être notre moteur de recherche, l’endroit où nos données sont stockées et où toute la magie opère
  • Logstash, qui permet d’analyser, d’agréger et de publier des fichiers journaux dans Elasticsearch
  • Kibana, l’interface graphique permettant d’administrer Elasticsearch : le cluster, la visualisation des données contenues, la création de requêtes, etc.

Nous nous intéresserons seulement à la partie Elasticsearch dans ce billet de blog.

Rentrons maintenant dans le vif du sujet. Admettons que je veuille créer un outil me permettant d’administrer ma banque de fichiers musicaux (absolument légaux bien évidemment), avec pour objectif de créer un moteur de recherche me permettant de retourner les titres les plus pertinents à partir de mots clés, afin de pouvoir les lire.

Commençons par installer localement Elasticsearch, en suivant ce lien. Je vais ici décrire la procédure d’installation pour un système Linux :

wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.9.2-linux-x86_64.tar.gz
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.9.2-linux-x86_64.tar.gz.sha512
shasum -a 512 -c elasticsearch-7.9.2-linux-x86_64.tar.gz.sha512 
tar -xzf elasticsearch-7.9.2-linux-x86_64.tar.gz
cd elasticsearch-7.9.2/ 

Vous pouvez ensuite lancer le serveur avec la commande suivante :

./bin/elasticsearch

Vous pouvez vérifier que l’installation s’est bien déroulée en accédant à http://localhost:9200 sur votre navigateur préféré. Vous devriez voir apparaître un document JSON ressemblant à :

{
  "name" : "servermcserverface",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "EYTAqvD4T_KcGEYDdo7o5w",
  "version" : {
    "number" : "7.9.2",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "d34da0ea4a966c4e49417f2da2f244e3e97b4e6e",
    "build_date" : "2020-09-23T00:45:33.626720Z",
    "build_snapshot" : false,
    "lucene_version" : "8.6.2",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

Félicitations, vous avez un cluster Elasticsearch d’un noeud fonctionnel ! Comme vous avez pu le voir grâce à l’exemple précédent, Elasticsearch expose une API REST.  Utilisons la pour créer notre premier index, avec notre premier Titre :

curl -XPOST "http://elasticsearch:9200/tracks/_doc/291b5a9f-0f15-48ae-8dab-47b054a265ca" -H 'Content-Type: application/json' -d 
'{
  "id": "291b5a9f-0f15-48ae-8dab-47b054a265ca",
  "title": "Addict",
  "albumName": "LUN",
  "artistName": "Destiny Potato",
  "length": 458,
  "path": "/the/path/to/the/track/some track.flac",
  "genres": "",
  "format": "FLAC 16 bits",
  "extension": "flac"
}'

Cet appel CURL nous indique que nous voulons indexer le document fourni en JSON dans l’index tracks.  Ce concept d’index est omniprésent dans Elasticsearch, puisqu’il remplit plusieurs fonctions au sein du cluster. Il sert d’une part à stocker des documents d’un même format dans l’équivalent d’une collection, et d’autre part à les analyser à chaque indexation, ce qui rend le moteur de recherche réactif.

Elasticsearch est doté de valeurs par défaut permettant d’analyser sans autre configuration particulière un document qu’on lui fournit. On peut voir comment il a interprété nos données en interrogeant l’endpoint tracks/_mapping :

curl -XGET "http://elasticsearch:9200/tracks/_mapping"

Ce qui nous donne en retour :

{
  "tracks" : {
    "mappings" : {
      "properties" : {
        ...
        "artistName" : {
          "type" : "text", // type principal du champ
          "fields" : { // champs secondaires rattachés au champ
            "keyword" : {  // texte non analysé du champ : artistName.keyword
              "type" : "keyword", // propriétés du champs secondaire
              "ignore_above" : 256
            }
          }
        },
       ...
        "length" : {
          "type" : "long"
        },
  	...
      }
    }
  }
}

On retrouve dans cet objet JSON plein d’informations intéressantes sur la façon dont le moteur d’Elasticsearch ainterprtété les données qu’on lui a fournies en entrée. Par exemple, le champ artistName a été identifié comme étant un champ textuel. Il faut savoir qu’Elasticsearch gère de deux façons distinctes ce genre de champ : une partie analysée (type: text), et une partie non analysée : l’objet keyword.

Le champ principal (text) indique qu’Elasticsearch va analyser le texte grâce à l’analyseur standard, et pourra être utilisé par la suite pour effectuer des recherches dans notre index. Le deuxième champ, de type keyword, contient le texte tel quel, et sera utilisé pour rechercher exactement la chaîne de caractère dans son intégralité.

Afin de pouvoir créer un moteur de recherche sur notre Index, nous allons devoir modifier ce mapping afin d’inclure des mots clés spécifiques à indexer et à rechercher :

{
  "mappings": {
    "properties": {
      "suggest": {
        "type": "completion"
      },
      "album": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "albumName": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "artist": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "artistName": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "cover": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "id": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "length": {
        "type": "integer"
      },
      "path": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "title": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      }
    }
  }
}

Dans ce nouveau mapping, nous avons ajouté un champ de type completion, qui nous permettra d’avoir de l’auto complétion, dans une barre de recherche par exemple. Mettons à jour notre mapping, après avoir supprimé l’index créé précédemment, étant donné qu’il n’est pas possible de changer le mapping d’un index déjà créé :

curl -XDELETE "http://elasticsearch:9200/tracks"

curl -XPUT "http://elasticsearch:9200/tracks"
-H 'Content-Type: application/json' 
-d @path/to/TracksMapping.json

Ce PUT permet à la fois de créer un index – appelé tracks – et de lui affecter un mapping  personnalisé. Il est donc possible de spécifier à Elasticsearch la façon dont il est supposé analyser chaque champ. Une fois ce mapping créé, nous pouvons indexer un morceau de musique avec des mots clés en minuscule dans ce champ nouvellement créé :

{
  "id": "291b5a9f-0f15-48ae-8dab-47b054a265ca",
  "title": "Addict",
  "albumName": "LUN",
  "artistName": "Destiny Potato",
  "length": 458,
  "path": "/the/path/to/the/track/some track.flac",
  "suggest": [
    "lun",
    "destiny potato",
    "addict"
  ],
  "genres": "",
  "format": "FLAC 16 bits",
  "extension": "flac"
}

Une fois un certain nombre de titres  ajoutés dans cet index, avec les mêmes champs remplis, il est possible d’effectuer une recherche d’autocomplétion sur notre index tracks :

curl -XPOST "http://elasticsearch:9200/yashmss_tracks/_search" -H 'Content-Type: application/json' -d '
{
  "suggest": {
    "title-suggest": {
      "prefix": "fire",
      "completion": {
        "field": "suggest"
      }
    }
  }
}'

Nous retrouvons en retour une liste des morceaux ayant leur artiste, titre ou nom d’album commençant par « fire » : 

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "suggest" : {
    "title-suggest" : [
      {
        "text" : "fire",
        "offset" : 0,
        "length" : 4,
        "options" : [
          {
            "text" : "fire",
            "_index" : "yashmss_tracks",
            "_type" : "_doc",
            "_id" : "868bf980-47d5-4074-9e10-5807c8a15d5f",
            "_score" : 1.0,
            "_source" : {
              "id" : "868bf980-47d5-4074-9e10-5807c8a15d5f",
              "title" : "Fire",
              "cover" : null,
              "albumName" : "Classicks",
              "artistName" : "Alice Cooper",
              "length" : 182,
              "path" : "/path/to/musicAlice Cooper/1995 Classicks/15-Fire.mp3",
              "suggest" : [
                "classicks",
                "alice cooper",
                "fire"
              ],
              "genres" : "Hard Rock",
              "format" : "MPEG-1 Layer 3",
              "extension" : "mp3"
            }
          },
          {
            "text" : "fire",
            "_index" : "yashmss_tracks",
            "_type" : "_doc",
            "_id" : "40e57c4d-09bc-48e1-8b42-76b28f7b1dd4",
            "_score" : 1.0,
            "_source" : {
              "id" : "40e57c4d-09bc-48e1-8b42-76b28f7b1dd4",
              "title" : "Fire",
              "cover" : null,
              "albumName" : "The Ultimate Experience",
              "artistName" : "Jimi Hendrix",
              "length" : 157,
              "path" : "/path/to/musicJimi Hendrix/1992 The Ultimate Experience/17-Fire.mp3",
              "suggest" : [
                "the ultimate experience",
                "jimi hendrix",
                "fire"
              ],
              "genres" : "Hard Rock",
              "format" : "MPEG-1 Layer 3",
              "extension" : "mp3"
            }
          },
          {
            "text" : "fire",
            "_index" : "yashmss_tracks",
            "_type" : "_doc",
            "_id" : "e74095b9-67b8-4029-9e51-4be5a3b6f003",
            "_score" : 1.0,
            "_source" : {
              "id" : "e74095b9-67b8-4029-9e51-4be5a3b6f003",
              "title" : "Fire",
              "cover" : null,
              "albumName" : "What hits!?",
              "artistName" : "Red hot chili peppers",
              "length" : 121,
              "path" : "/path/to/musicRed Hot Chili Peppers/1992 What hits!_/07-Fire.mp3",
              "suggest" : [
                "what hits!?",
                "red hot chili peppers",
                "fire"
              ],
              "genres" : "Rock",
              "format" : "MPEG-1 Layer 3",
              "extension" : "mp3"
            }
          },
          {
            "text" : "fire",
            "_index" : "yashmss_tracks",
            "_type" : "_doc",
            "_id" : "22e95583-48ca-4236-a4fd-3b907b7f459c",
            "_score" : 1.0,
            "_source" : {
              "id" : "22e95583-48ca-4236-a4fd-3b907b7f459c",
              "title" : "Fire",
              "cover" : null,
              "albumName" : "Mother's Milk",
              "artistName" : "Red Hot Chili Peppers",
              "length" : 123,
              "path" : "/path/to/musicRed Hot Chili Peppers/1989 Mother's Milk/09-Fire.mp3",
              "suggest" : [
                "mother's milk",
                "red hot chili peppers",
                "fire"
              ],
              "genres" : "Rock",
              "format" : "MPEG-1 Layer 3",
              "extension" : "mp3"
            }
          },
          {
            "text" : "fire",
            "_index" : "yashmss_tracks",
            "_type" : "_doc",
            "_id" : "3f5c6d50-7ada-499b-b443-d52a626ca91b",
            "_score" : 1.0,
            "_source" : {
              "id" : "3f5c6d50-7ada-499b-b443-d52a626ca91b",
              "title" : "Fire",
              "cover" : null,
              "albumName" : "Trilogy",
              "artistName" : "Yngwie Malmsteen",
              "length" : 252,
              "path" : "/path/to/musicYngwie Malmsteen (rising force)/1986 Trilogy/06-Fire.mp3",
              "suggest" : [
                "trilogy",
                "yngwie malmsteen",
                "fire"
              ],
              "genres" : "Hard Rock",
              "format" : "MPEG-1 Layer 3",
              "extension" : "mp3"
            }
          }
        ]
      }
    ]
  }
}

Le but de cette requête est d’être exécutée à chaque fois que l’utilisateur entre un nouveau caractère dans une barre de recherche, comme ci-dessous :

Et voilà, vous avez pu manipuler et mettre un pied dans le monde merveilleux d’Elasticsearch, en créant votre premier moteur de recherche ! Comme répété à plusieurs reprises le long de cet article, nous allons continuer l’aventure dans une deuxième et troisième partie, qui nous amèneront à contruire un back-office à notre application, qui nous permettra d’indexer automatiquement des titres musicaux, de les rechercher et de les écouter ! Le tout sur une stack Kotlin – VueJS.

La suite

Nous allons continuer l’aventure dans d’autres parties, qui nous amèneront à construire un back-office à notre application, ce qui nous permettra d’indexer automatiquement des titres musicaux, de les rechercher et de les écouter !