Elasticsearch (ES) est un moteur de recherche qui vous permet d’indexer des documents (entendez, des données) très facilement. L’indexation s’appuie sur Lucene, une bibliothèque open source de la fondation Apache. Les échanges avec ES se font via une interface REST au format JSON.

Si votre sérializeur préserve correctement les types PHP, le type des champs de votre document sera directement détecté par ES et indexé comme tel. Ainsi, si vous envoyez des chaines, les mots seront analysés et indexés, si vous envoyez des entiers, vous avez accès aux éléments de calculs, comme pour faire une somme, une moyenne, etc… Attention toutefois, dans certaines conditions les nombres peuvent être confondu, comme un entier ou un nombre à virgule, ou les formats de dates. Dans ces cas-là, il faut indiquer à ES le format des champs du document auquel il doit s’attendre.

L’un des points forts d’ES est la recherche à facettes, c’est la capacité de filtrer une collection de données en choisissant un ou plusieurs critères (les facettes). Il n’est donc pas tant question de recherche que de filtrage (une recherche brute, taxonomique, pouvant être utilisée en complément). Une classification à facettes associe à chaque donnée de l’espace de recherche un certain nombre d’axes explicites de filtrage, par exemple des mots clés issus d’une analyse texte, des métadonnées stockées dans une base de données, etc. On trouve par exemple des recherches à facettes basées sur des catégories sur de nombreux sites de e-commerce1. Cette fonctionnalité s’appelait “Facets” avant la version 5 d’ES, depuis elle a été retirée au profit de l’agrégation et des filtres.

Fini les requêtes Doctrine imbuvables et lentes avec beaucoup de jointures et conditions !

Ajouter ES à votre projet

Avec Docker rien de plus simple, comme toujours. Pour les versions récentes d’ES, Elastic à choisi de quitter DockerHub et de proposer leur propre registre.

# docker-compose.yml
version: '2'

volumes:

  esdata: ~

services:

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:5.2.2
    environment:
      - http.host=0.0.0.0
      - transport.host=127.0.0.1
      - xpack.security.enabled=false
    volumes:
      - esdata:/usr/share/elasticsearch/data

Tout d’abord on déclare un volume pour les données d’indexation d’ES. Ainsi celles-ci et les documents ne seront pas perdus si le conteneur Docker est supprimé.

Ensuite on déclare un nouveau service elasticsearch basé sur l’image du registre d’Elastic docker.elastic.co/elasticsearch/elasticsearch.

Pour la configuration d’ES, vous pouvez :

  • utiliser des variables d’environnement pour surcharger des options spécifiques
  • partager un fichier de configuration elasticsearch.yml vers l’emplacement /usr/share/elasticsearch/config/elasticsearch.yml du conteneur

Ici, étant donné que la configuration basique convient, les variables d’environnement sont utilisées. Attention, pour une utilisation de l’image en production, vous devez prendre connaissance de la configuration recommandée.

L’option http.host permet de lier le module HTTP d’ES sur une IP spécifique, ici la valeur 0.0.0.0 le lie à n’importe quelle IP, pratique vu que les containers Docker ont des IP changeante.

Même chose pour transport.host qui configure le module Transport qui assure les liaisons d’un cluster (même si nous n’en avons pas ici).

X-Pack est une extension payante pour ES, préinstallée dans l’image, qui ajoute les fonctionnalités suivantes :

  • sécurité (ex : droits d’accès)
  • alertes
  • monitoring
  • reporting
  • graphiques

Elle n’est pas nécessaire dans un environnement basique de développement local c’est pourquoi l’option xpack.security.enabled désactive la sécurité. Par contre, elle est recommandée en production.

L’image expose le port 9200 auquel vous pourrez vous connecter pour effectuer les requêtes REST après avoir lancé la commande docker-compose up -d.

ES et Symfony

Afin de vous interfacer avec ES, il existe plusieurs clients PHP. Dont un qui sera utilisé dans les exemples : Elastica. La documentation n’est pas formidable, mais les tests sont très complets et donnent les indications pour réaliser les requêtes.

Pour l’installer :

composer require ruflin/elastica

Le détail de la création d’un index, de son mapping et de l’ajout de documents ne sera pas abordé dans cet article. Si vous souhaitez avoir des exemples et des indications, vous pouvez consulter le bundle FOSElasticaBundle. Attention avec ce dernier, il pousse a indexer les entités telles qu’elles sont alors qu’il est souvent plus profitable de dénormaliser les données pour la recherche.

Visualiser les données indexées

Elastic propose un logiciel, Kibana, qui est une UI permettant d’effectuer des requêtes dans les index et de mettre en forme les résultats. C’est le “PHPMyAdmin” d’ES ayant, en plus, des possibilités de mise en forme des données comme des histogrammes, des camemberts, etc…

On peut l’ajouter au projet avec Docker :

# docker-compose.yml
version: '2'

services:

  kibana:
    image: docker.elastic.co/kibana/kibana:5.2.2
    environment:
      - server.host=0.0.0.0
      - elasticsearch.username=
      - elasticsearch.password=
      - xpack.security.enabled=false

L’image expose le port 5601 auquel vous pourrez vous connecter avec votre navigateur après avoir lancé la commande docker-compose up -d.

Lors de la première connexion, Kibana va demander de configurer un index. Si l’index à ajouter contient un champ ayant une valeur temporelle, cochez la case Index contains time-based events puis indiquez le nom de l’index dans le champ Index name or pattern. Après avoir recherché les informations de mapping, un menu déroulant Time-field name fera son apparation pour pouvoir sélectionner le champ avec la donnée temporelle primaire. Il suffira ensuite de valider les informations en cliquant sur create. Vous pourrez alors voir les informations de votre index, notamment la liste des champs et leur type.

Création de l'index
Les informations à saisir lors de la création de l’index

Dans le menu, si vous cliquez sur discover vous aurez alors accès à une barre de recherche vous permettant d’effectuer une recherche basique dans l’index et de consulter les résultats.

Les résultats
Page de résultats, pour les données temporelles attention à la sélection de la période en haut à gauche

Pour faire des recherches plus complexes, notamment avec l’agrégation de résultats, ou obtenir une représentation particulière, telle qu’un camembert, sélectionnez visualize dans le menu. Plusieurs formulaires sont alors présentés pour faire les différentes sélections.

Une requête avec agrégation avec Elastica

En admettant que j’ai un index activity avec un document contenant les champs network, stock et tyre, voici comment faire l’équivalent des requêtes SQL suivantes (en une seule requête pour ES !) :

  • SELECT COUNT(1), SUM(stock) FROM activity WHERE allfields LIKE ‘%texte%’ AND networkId = 2 AND tyre = ‘mytest’
  • SELECT * FROM activity WHERE allfields LIKE ‘%texte%’ networkId = 2 AND tyre = ‘mytest’ LIMIT 40, 20

Bien sûr allfields n’existe pas en SQL, mais admettons qu’il s’agisse d’un moyen d’utiliser tous les champs :)

<?php
 
use Elastica\Aggregation;
use Elastica\Query;

$index = /* Your Elastica\Index instance */;
$search = 'text';
$network = 2;
$tyre = 'mytest';
$size = 20;
$from = 40;

$queryPart = new Query\BoolQuery();

// Match the searched sentence
$queryPart->addMust((new Query\Match())->setFieldQuery('_all', $search));

// Filter on one network
$queryPart->addMust(new Query\Term(['networkId' => ['value' => $network]]));
$queryPart->addMust(new Query\Term(['tyre' => ['value' => $tyre]]));

$query = Query::create($queryPart);

// Sum all the time spent
$termAgg = (new Aggregation\Sum('stock'))
    ->setField('stockSum')
;
$query->addAggregation($termAgg);

$query->setSize($size);
$query->setFrom($from);

$resultSet = $index->search($query);

Voici la requête correspondante au code ci dessus, qui sera envoyé à ES, sachant qu’on recherche huile pour le networkId 4 et le tyre gomme :

{
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "_all": {
                            "query": "huile"
                        }
                    }
                },
                {
                    "term": {
                        "networkId": {
                            "value": 4
                        }
                    }
                },
                {
                    "term": {
                        "tyre": {
                            "value": "gomme"
                        }
                    }
                }
            ]
        }
    },
    "aggs": {
        "stockSum": {
            "sum": {
                "field": "stock"
            }
        }
    },
    "size": 19,
    "from": 0
}

Et voilà, $resultSet est un objet de classe \Elastica\ResultSet qui contiendra :

  • 20 documents ayant un des champs ayant le texte text, le champ networkId à la valeur exacte 2, le champ tyre à la valeur mytest à partir du 40° document correspondant à ces conditions
  • la somme de tous les champs stock de tous les documents correspondants aux conditions de la recherche (pas uniquement les 20 documents sélectionnés) qui sera appelée stockSum
  • le nombre total des documents correspondant à la recherche

Voici comment accéder aux informations :

<?php

$resultSet->getAggregation('stockSum');
$resultSet->getTotalHits();
$resultSet->getResults(); // Return Result[], Result with mixed ES data & the document, you may want to use $result->getSource() to get the document

BoolQuery est une classe incontournable qui vous permet de préparer les conditions de sélection de la recherche. Elle propose notamment ces trois méthodes qui vous permettront de faire une recherche usuelle :

  • addShould() : ajoute une condition qui fera monter dans les résultats un document qui la respecte
  • addMust() : tous les documents des résultats doivent impérativement correspondre à la condition
  • addMustNot() : tous les documents qui respectent la condition seront exclus des résultats

Une fois la partie conditionnelle réalisée, vous devez créer la requête qui sera envoyée à ES via la méthode statique Query::create($queryPart);. Si vous souhaitez rechercher rapidement une chaine et remonter tous les documents correspondants (soit une requête MatchAll() de ES), vous pouvez passer la chaine directement ainsi : Query::create('ma chaine recherchée');.

Ensuite on peut ajouter les agrégations. Dans l’exemple il s’agit de faire la somme de tous les champs stock, donc Aggregation\Sum(), mais il en existe d’autres pour les nombres tels que Avg, Min, Max, Percentiles, etc… Mais aussi pour la géolocalisation et les dates, vous pouvez les retrouver dans la documentation.

Dans l’exemple, on spécifie des informations de pagination, size et from pour ne sélectionner qu’une fraction des résultats. Étant donné que les agrégations ne sont pas directement calculées à partir des résultats sélectionnés, mais la totalité des résultats correspondant à la recherche, si vous n’êtes intéressé que par l’agrégation, vous pouvez très bien spécifier une size à zéro.

Le futur et pour aller plus loin

Des nouvelles fonctionnalités sont régulièrement ajoutées à ES et une annonce récente est particulièrement intéressante. Il sera bientôt possible d’effectuer des recherches à partir d’une requête SQL (source : Elastic sur Twitter). Ca simplifiera la prise en main du moteur de recherche car ça retire la complexité initiale d’apprentissage de la “Search API” d’ES pour obtenir les premiers résultats de recherche.

Elastic propose également un logiciel très pratique pour collecter vos logs : Logstash. Il se marie avec ES et Kibana pour vous proposer une consultation pratique de vos logs.

JoliCode as publié une dizaine d’article autour d’ES, je vous invite à les découvrir.

Références

1. Article “Recherche à facettes” sur Wikipedia [https://fr.wikipedia.org/wiki/Recherche_%C3%A0facettes](https://fr.wikipedia.org/wiki/Recherche%C3%A0_facettes)