Cas Utilisateur

Elasticsearch pour trouver des commerçants nantais : une histoire de small data

Escale est une agence d’innovation web nantaise. Notre activité principale est d’aider startups, PME et grandes entreprises à identifier leurs problématiques et à les résoudre via la réalisation de solutions web. Nous aimons les défis techniques, les projets bienveillants et ambitieux, l’écosystème nantais et les licornes.

Alors quand l’équipe de SoNantes nous a sollicité pour travailler sur un sujet qui reprenait tous ces points (sauf les licornes é_è), nous savions que nous nous embarquions pour un beau projet !

SoNantes est une monnaie complémentaire à l’euro qui vise à accélérer et favoriser les échanges économiques locaux de la région nantaise. L’idée est de permettre aux professionnels d'accroître leur visibilité, de valoriser leur image et d’augmenter leurs fonds de roulement. Les particuliers peuvent, quant à eux, faire leurs achats en SoNantes et s’assurer que l’argent dépensé sera réutilisé dans la région nantaise en favorisant les circuits courts et les commerçants locaux.

Vous l’aurez deviné, la réussite d’un tel projet dépend en grande partie de l’adoption de la monnaie à la fois par les particuliers et les professionnels. Pour favoriser cette traction, SoNantes mise sur la valeur ajoutée d’outils et de services à l’ergonomie soignée gravitant autour de la monnaie. C’est pour la réalisation de l’un de ces outils, l’annuaire, que nous avons collaboré.

escale-sonantes-logos.png

Un annuaire pour les montrer tous, un annuaire pour les trouver, et dans les bons plans les lier

Nous voilà donc chargés de développer un annuaire des professionnels adhérant à notre monnaie nantaise préférée. Ce projet peut être décomposé en plusieurs modules :

  • Une API HTTP pour rechercher et obtenir des informations sur les adhérents
  • Une API HTTP pour obtenir des informations sur les bons plans proposés par des adhérents
  • Un site web qui exploite les mêmes données que ces APIs et permet une visualisation confortable (sur une carte, par exemple)
  • Des widgets pouvant être intégrés sur des sites tiers et utilisant les données des APIs

L’annuaire doit s’insérer dans le système d’information déjà existant. L’interconnexion entre les deux entités est matérialisée par un axccès en lecture-seule à une base de données MySQL qui contient toutes les informations dont nous avons besoin. Nous avons choisi de développer notre applicatif en PHP grâce au framework web Laravel.

Pour cet article, nous n’allons considérer que le premier module : l’API HTTP de recherche des adhérents. L’annuaire doit permettre de trouver un professionnel en fournissant dans un champ unique des informations pouvant correspondre à plusieurs attributs : nom, commune, catégorie et tags. On veut favoriser certains attributs : si le texte saisi correspond en partie au nom du professionnel pour un résultat donné, ce résultat doit être mieux classé que si la correspondance est sur la commune. On veut aussi obtenir des résultats pertinents même si on fait une faute de frappe. On a donc besoin de fonctionnalités avancées de recherche en plein texte (ou full text search).

L’annuaire doit également pouvoir filtrer les résultats pour ne garder que ceux qui sont géographiquement proches, dans le cas où l’utilisateur s’est géolocalisé et a spécifié un rayon maximum.

Toutes ces contraintes ne pouvant être satisfaites par MySQL seul, nous avons donc décidé de mettre à l’épreuve les fonctionnalités d’Elasticsearch !

technologies-sonantes.png

Elasticsearch, l'index pointé vers le futur

Étant donné ce contexte, la faible taille des données (moins de 1 Mo) et leur fréquence de changement occasionnelle, nous avons choisi de conserver MySQL comme stockage maître, et d’introduire Elasticsearch comme un index externe. La base MySQL contient donc les données de référence, et Elasticsearch contient une copie d’une partie de ces données qui lui permet de renvoyer une liste ordonnée d'identifiants lorsqu’on lui soumet une recherche.

Pour garder les données à jour dans Elasticsearch, nous avons développé une tâche qui s’exécute de manière régulière. Cette tâche réalise les opérations suivantes :

  1. Création d’un nouvel index au nom unique
  2. Envoi à cet index d’un mapping correspondant à nos données
  3. Récupération des données dans la base MySQL, mise sous la forme d’un document JSON respectant le mapping (on fait correspondre le champ _id du document avec notre clé primaire dans MySQL), et envoi dans l’index
  4. Mise à jour de l’alias nommé sonantes pour qu’il pointe vers le nouvel index 
  5. Suppression de l’ancien index vers lequel pointait l’alias

sonantes-process.png

Le point le plus intéressant dans ce processus est l’utilisation des alias. Cette fonctionnalité d’Elasticsearch permet d’associer un ou plusieurs noms supplémentaires à un index existant, un peu à la manière d’un lien symbolique Unix. Ainsi, notre application peut utiliser l’alias sonantes, sans avoir besoin de connaître le nom de l’index cible de cet alias. Changer la cible d’un alias est une opération atomique, ce qui offre les avantages suivants :

  • Si le processus d’indexation échoue, l’alias n’a pas été modifié et pointe toujours vers l’ancien index ;
  • Le changement de l’index actif se fait sans période d’indisponibilité.

Les interactions entre notre application PHP Laravel et Elasticsearch passent par le client PHP officiel. Nous utilisons également le paquet laravel-elasticsearch qui fournit une intégration propre du client PHP officiel d’Elasticsearch dans l'environnement de Laravel : cela permet par exemple de configurer les paramètres de connexion à Elasticsearch d’une manière similaire à la configuration des bases de données dans Laravel.

Voici un exemple de code PHP utilisant ces abstractions pour changer la cible de l’alias sonantes :

<?php
Elasticsearch::indices()->updateAliases([
    'body' => [
        'actions' => [
            [
                'add' => [
                    'index' => $newIndex,
                    'alias' => 'sonantes'
                ],
            ],
            [
                'remove' => [
                    'index' => $oldIndex,
                    'alias' => 'sonantes'
                ]
            ],
        ],
    ],
]);

Lance-requêtes téléguidé

En ce qui concerne l’exploitation de cet index Elasticsearch, notre application procède de la manière suivante :

  1. Création d’une requête Elasticsearch en fonction des données reçues dans la requête HTTP
  2. Récupération des résultats, et extraction de leurs identifiants
  3. Envoi d’une requête à MySQL pour obtenir toutes les données associées à ces identifiants, en conservant leur ordre
  4. Envoi d’une réponse HTTP contenant le résultat

On va s’intéresser à la requête qui est envoyée à Elasticsearch, dont voici un exemple :

<?php
Elasticsearch::search([
    'index' => 'sonantes',
    'type' => 'company',
    'body' => [
        'filter' => [
            'bool' => [
                'must' => [
                    [
                        'term' => [
                            'active' => 1,
                        ],
                    ],
                    [
                        'geo_distance_range' => [
                            'from' => $from.'km',
                            'to' => $to.'km',
                            'location' => $lon.','.$lat,
                        ],
                    ],
                ],
            ],
        ],
        'query' => [
            'bool' => [
                'must' => [
                    [
                        'multi_match' => [
                            'query' => $text,
                            'fields' => ['name^2', 'city', 'category^5', 'tags^3'],
                            'type' => 'best_fields',
                            'fuzziness' => 3,
                        ],
                    ],
                ],
            ],
        ]
    ],
]);

Il y a trois choses intéressantes à observer : 

  • On cible l’index sonantes qui est en fait un alias, comme vu dans la section précédente (mais ça ne change rien car Elasticsearch va résoudre ce nom pour que la requête atteigne le vrai index)
  • Une section filter dans la requête spécifie des critères d’exclusion de données du résultat de la requête
  • Une section query définit ce qu’on cherche, et comment ordonner les résultats

La section de filtrage comporte deux conditions qui doivent être respectées pour qu’un document puisse être présent dans les résultats. La première indique que le champ booléen active du document doit être à vrai. La seconde utilise une Geo Distance Range Query pour garantir que le professionnel modélisé par le document se situe géographiquement dans une couronne définie par un point central et deux rayons. Cette seconde condition est facile à exprimer car le mapping de notre index comprend un champ de type Geo-point, supporté nativement par Elasticsearch. 

La section de requête, quant à elle, comporte une Multi Match Query. C’est une requête très intéressante pour la recherche en plein texte car elle est capable de rechercher à travers plusieurs champs des documents. Ces champs sont indiqués dans le paramètre fields, et peuvent être associés à un poids (appelé boost). Avec ces informations et un mode de fonctionnement (ici best_fields, le mode par défaut), Elasticsearch est capable d’ordonner les résultats dans un ordre en cohérence avec nos contraintes métier. Dans notre exemple, on souhaite donner plus de points à la catégorie d’un professionnel qu’à sa ville. Ainsi, on obtient des premiers résultats plus pertinents que sans les boosts.

Elasticsearch, notre base de données complémentaire

Sur ce projet, notre usage d’Elasticsearch n’était pas celui qui est le plus souvent mis en avant. Nous n’avons pas utilisé les fonctionnalités de type “temps réel”, ni celles liées à la haute disponibilité puisque nous utilisons un unique nœud. Nous n’avons utilisé Elasticsearch comme moyen de stockage primaire pour aucune donnée, c’est à dire que les données que nous avons placées dans Elasticsearch ne sont jamais considérées comme source de vérité.

Pour autant, utiliser Elasticsearch a du sens. Il sert à indexer des données provenant d’une base de données relationnelle, et nous offre des fonctionnalités avancées de recherche en plein texte.

Nous avons vu quelques exemples de ces fonctionnalités : le filtrage par géocode, le boost de score de certains champs dans les résultats de recherche et la capacité à ignorer les erreurs de frappe légères (fuzziness). Il en existe bien d’autres !

Comme expliqué en introduction, SoNantes se différencie en proposant des services et des outils de qualité. C’est avec cet objectif en tête qu’Elasticsearch a été choisi comme fondation pour la recherche dans l’annuaire des adhérents. Les fonctionnalités actuellement disponibles ne sont qu’un aperçu, le niveau zéro, de ce qu’il est possible de réaliser. Maintenant qu’Elasticsearch a été intégré au système, implémenter des fonctionnalités de recherche plus poussées est relativement simple : il suffit de modifier la manière dont l’application le requête, et éventuellement adapter la requête SQL qui sert à importer les données. Un coût plutôt faible en regard de la valeur ajoutée !


david-sferruzza-profile.jpeg

David Sferruzza. Ingénieur généraliste de formation, David est responsable R&D chez Escale, une agence nantaise d'innovation web, et prépare une thèse doctorale à l'Université de Nantes dans le domaine du génie logiciel !




killian-blais-profile.png

Killian Blais. Développeur web chez Escale, Killian est spécialisé en PHP et particulièrement dans l’écosystème Laravel.