Comprendre Elasticsearch #2 : comment mieux l’optimiser ?
Dans le premier article de cette série, nous avons vu comment Elasticsearch fonctionne : shards, segments, index inversé, Doc Values, analyse des requêtes et rôle de Lucene.
Cette compréhension est indispensable pour passer à l’étape suivante : l’optimisation.
Car optimiser Elasticsearch ne consiste pas simplement à augmenter les ressources d’un cluster ou à modifier quelques paramètres de configuration.
Les performances se jouent bien plus tôt, dès la conception de l’index et du mapping, dans la manière dont les données sont analysées, dans le choix du nombre de shards et jusque dans la construction des requêtes.
Et surtout, il n’existe pas une optimisation universelle.
Cherche-t-on à accélérer l’indexation ?
À réduire le temps de réponse des recherches ?
À améliorer la pertinence des résultats ?
À diminuer la consommation de ressources ?
Les réponses ne seront pas nécessairement les mêmes.
Dans cet article, nous allons passer en revue plusieurs leviers permettant d’optimiser Elasticsearch, en nous appuyant sur les mécanismes présentés dans le premier volet.
1. Commencer par le mapping
Le premier levier d’optimisation est probablement l’un des plus importants : le mapping.
Elasticsearch propose un mapping dynamique qui permet de créer automatiquement les champs lorsqu’un document est indexé. C’est pratique pour démarrer rapidement.
Mais dès que l’application prend de l’ampleur, laisser Elasticsearch déterminer automatiquement la structure de ses données peut devenir une mauvaise stratégie.
Pourquoi ?Parce que le type choisi pour un champ détermine les structures d’indexation disponibles et donc les opérations que l’on pourra effectuer efficacement dessus.
Il est donc préférable de définir explicitement son mapping en fonction des usages réels de l’application.
Quel type pour quel besoin ?
Quelques exemples :
| Besoin | Type à envisager |
| Recherche plein texte | text |
| Tri ou agrégation | keyword |
| Recherche avec des wildcards | wildcard |
| Autocomplétion | completion |
Prenons une adresse e-mail.
Si l’application doit permettre des recherches sur une partie du domaine, par exemple : *@entreprise.fr , le type wildcard peut être pertinent.
À l’inverse, si une donnée doit principalement être utilisée pour des tris ou des agrégations, un champ keyword sera généralement plus adapté.
Le choix du mapping doit donc partir d’une question simple : Que vais-je réellement faire avec cette donnée ?
Ne pas tout indexer par défaut
Un autre point mérite une attention particulière.
Par défaut, Elasticsearch indexe les champs d’un document. Cela peut sembler logique : après tout, mieux vaut pouvoir rechercher partout. Mais si un champ ne sera jamais utilisé pour une recherche, un tri ou une agrégation, son indexation peut simplement augmenter la taille de l’index et contribuer à augmenter la taille et le nombre de structures manipulées par Lucene.
Dans un contexte avec plusieurs millions de documents, ces choix peuvent rapidement avoir un impact.
L’optimisation commence donc avant même la première requête : n’indexez pas une donnée uniquement parce que vous pouvez le faire. Indexez-la parce que votre application en a besoin.
2. Adapter les analyzers à votre recherche
Le mapping ne fait pas tout.
Pour les champs de type text, Elasticsearch permet également de définir des analyzers personnalisés.
Un analyzer est constitué notamment :
- d’un tokenizer ;
- de filtres ;
- éventuellement de versions personnalisées de ces composants.
Le tokenizer définit la manière dont une chaîne de caractères est découpée en tokens.
Les filtres interviennent ensuite pour transformer ces tokens.
Et un point important : les filtres sont exécutés dans l’ordre dans lequel ils sont configurés.
Cette mécanique permet d’adapter précisément la façon dont un texte est indexé et recherché.
Tester son analyzer avant de l’utiliser
Avant de mettre un analyzer en production, il est particulièrement utile de vérifier ce qu’il produit réellement.
Elasticsearch fournit pour cela l’API : GET /_analyze
Elle permet de visualiser les tokens générés à partir d’un texte et donc de vérifier si l’analyse correspond réellement au besoin fonctionnel.
C’est une étape simple, mais elle peut éviter beaucoup de tâtonnements lorsqu’une recherche ne retourne pas les résultats attendus.
3. Le shingle : améliorer la pertinence des recherches textuelles
Parmi les filtres disponibles, le shingle peut être particulièrement intéressant pour certaines recherches, notamment sur des titres.
Son principe est de regrouper plusieurs tokens consécutifs afin de créer de nouveaux tokens.
Prenons : « Qui veut la peau de Roger Rabbit »
Avec une configuration permettant de créer des shingles de 2 à 3 mots, on peut notamment obtenir :
Qui veutQui veut laveut laveut la peaula peaula peau depeau depeau de Rogerde Rogerde Roger RabbitRoger Rabbit
On ne travaille donc plus uniquement avec des mots isolés, mais également avec des groupes de mots consécutifs.
Quel intérêt pour la pertinence ?
Imaginons deux titres :
- Roger Rabbit
- Roger dit le Rabbit
Une recherche sur « Roger Rabbit » pourrait faire correspondre les deux documents.
Pourtant, le premier titre correspond beaucoup mieux à la recherche. Avec un shingle, la séquence Roger Rabbit existe dans le premier titre mais pas dans le second.
On peut donc utiliser cette information pour favoriser le résultat qui correspond le mieux à l’intention de recherche. Cela permet d’affiner la pertinence sans nécessairement exclure les autres résultats.
Attention à l’explosion du nombre de tokens
Le shingle est cependant à utiliser avec précaution. Plus la chaîne est longue et plus les tailles de shingles autorisées sont importantes, plus le nombre de tokens générés peut augmenter. Cela signifie davantage de données à indexer et donc davantage d’espace et de travail pour Lucene.
Un analyzer plus sophistiqué n’est donc pas automatiquement un analyzer plus performant.
Comme souvent avec Elasticsearch, il faut trouver le bon compromis entre qualité des résultats, coût d’indexation et consommation de ressources.
4. Le type nested : puissant, mais à utiliser avec parcimonie
Le type nested est un autre bon exemple de fonctionnalité extrêmement utile lorsqu’elle répond à un véritable besoin… mais qui peut avoir un coût.
Imaginons un document contenant : [John Doe, Roger Rabbit]
Sans le type nested, Lucene traite les valeurs du tableau comme appartenant au même champ.
On peut alors retrouver les tokens :
- John
- Doe
- Roger
- Rabbit
Le problème apparaît lorsqu’on recherche : John Rabbit
Les tokens existent bien dans le document. Mais la combinaison John Rabbit n’existe pas réellement dans les données : John est associé à Doe et Roger à Rabbit.
Pourquoi nested résout ce problème ?
Le type nested permet d’indiquer que les éléments du tableau doivent être considérés comme des documents liés au document parent.
Dans notre exemple :
John Doedevient un document imbriqué ;Roger Rabbitdevient un autre document imbriqué.
Lucene peut alors conserver la relation entre les différentes valeurs. Cette mécanique repose notamment sur un mécanisme de block join.
Le problème est que ces documents supplémentaires et les traitements nécessaires pour maintenir leurs relations ajoutent de la complexité.
Le nested est donc très utile lorsque la relation entre les valeurs est importante, mais il ne doit pas être utilisé par défaut.
Bon réflexe : utilisez nested lorsque vous avez besoin de préserver la relation entre les objets d’un tableau. Sinon, son coût supplémentaire n’est pas forcément justifié.
5. Trouver le bon nombre de shards
Le nombre de shards est un sujet classique lorsqu’on parle de performances Elasticsearch.
Une intuition assez naturelle serait : Plus il y a de shards, plus le travail peut être réalisé en parallèle, donc plus la recherche est rapide. En réalité, ce n’est pas aussi simple.
Comme nous l’avons vu dans le premier article, chaque shard correspond à une instance Lucene avec ses propres structures et sa propre consommation de ressources.
Lorsqu’une requête concerne plusieurs shards, Elasticsearch doit également récupérer et fusionner les résultats provenant de chacun d’entre eux.
Multiplier inutilement les shards augmente donc le travail à effectuer. Une shard trop petite peut ainsi consommer des ressources qui ne sont pas justifiées par le volume de données qu’elle contient.
Quelques repères
Les recommandations mentionnées dans notre contexte sont notamment :
- viser une taille de shard comprise entre 10 et 50 Go ;
- conserver le nombre de documents d’une shard sous 200 millions.
Ces valeurs constituent des repères, pas une règle universelle.
Le dimensionnement doit être adapté au volume de données, au nombre de requêtes, au matériel disponible et aux caractéristiques de l’application. Le bon nombre de shards dépend du contexte.
6. Optimiser aussi la construction des requêtes
L’optimisation ne s’arrête pas à l’indexation. La façon dont l’application interroge Elasticsearch est tout aussi importante.
Elasticsearch propose plusieurs moyens d’interroger et d’exploiter les données, chacun étant destiné à des usages spécifiques :
- Query DSL : recherche de documents ;
- ES|QL : analyse de données ;
- SQL : reporting et BI ;
- EQL : corrélation d’événements ;
- PromQL : séries temporelles et métriques.
Pour une application de recherche classique, nous nous intéresserons principalement au Query DSL.
Distinguer recherche et filtre
Lorsqu’on construit une requête, une première question doit être posée : quels éléments servent réellement à rechercher et lesquels servent simplement à filtrer ?
Cette distinction est importante. Un élément placé dans filter n’influence pas le score de pertinence. Il n’a donc pas à participer au calcul du scoring comme un élément de recherche. Cela permet notamment d’éviter de demander à Elasticsearch de calculer de la pertinence là où elle n’est pas nécessaire.
Prenons un catalogue e-commerce. On peut avoir :
- une recherche utilisateur sur
chaussure running; - un filtre sur
marque = X; - un filtre sur
taille = 42; - un filtre sur
disponible = true.
La recherche textuelle doit contribuer à la pertinence. Les autres critères sont avant tout des contraintes de filtrage. Cette distinction doit donc apparaître dans la construction de la requête.
7. Choisir le bon type de recherche
Toutes les recherches ne se valent pas non plus.
match : Une requête match analyse le texte recherché en utilisant l’analyzer configuré. Elle est donc adaptée aux recherches textuelles qui nécessitent cette analyse et cette normalisation.
term : À l’inverse, term recherche la valeur telle quelle, sans passer par l’analyseur. Cela peut être pertinent pour des données telles qu’un EAN ou une référence, lorsque l’on cherche une correspondance exacte et qu’une analyse linguistique n’apporte rien.
Le choix entre match, term et les autres types de requêtes doit donc être dicté par la nature de la donnée et le comportement recherché, pas simplement par l’habitude.
8. Utiliser les multi-fields pour travailler la pertinence
Il est parfois intéressant d’indexer une même donnée de plusieurs façons. C’est le principe des multi-fields. Une même donnée peut ainsi être disponible avec différents mappings ou analyzers.
Prenons un titre. On peut par exemple disposer :
- d’une analyse standard pour retrouver les mots ;
- d’une analyse utilisant les shingles pour favoriser les séquences de mots pertinentes.
La requête peut alors exploiter les deux approches.
L’objectif est de favoriser un document contenant les bons mots dans le bon ordre, sans nécessairement éliminer un document contenant les mêmes mots mais dans une disposition différente. Cela permet de construire une pertinence plus fine.
L’optimisation Elasticsearch est une question de compromis
Il serait tentant de chercher une liste universelle de « bonnes pratiques Elasticsearch ». Mais ce serait passer à côté du principal sujet. Il faut d’abord déterminer ce que l’on cherche à optimiser.
Les choix ne seront pas nécessairement les mêmes selon que l’objectif est :
- d’accélérer l’indexation ;
- de réduire le temps de réponse des recherches ;
- d’améliorer la pertinence ;
- de réduire la consommation de ressources ;
- de supporter une augmentation importante du volume.
Et ces objectifs peuvent parfois entrer en conflit. Un analyzer plus complexe peut améliorer la pertinence mais augmenter le coût de l’indexation. Des structures supplémentaires peuvent accélérer certains usages mais consommer davantage de ressources.
Davantage de shards peuvent augmenter le parallélisme mais aussi le coût de coordination et de fusion des résultats. Optimiser Elasticsearch consiste donc à trouver le meilleur équilibre pour une application donnée.
D’autres leviers d’optimisation à explorer
Le mapping, les analyzers, les shards et les requêtes ne sont évidemment pas les seuls sujets.
Elasticsearch et Lucene proposent de nombreux mécanismes qui méritent également d’être étudiés lorsque les problématiques deviennent plus avancées :
- le
refresh; - les Stored Fields ;
- la coordination entre les nodes ;
- le Query Cache ;
- les mécanismes de merge ;
- et bien d’autres paramètres liés à l’architecture et aux usages.
Ils mériteraient à eux seuls des articles dédiés.
Conclusion : avant d’optimiser, comprendre
L’optimisation d’Elasticsearch commence dès la conception.
Elle passe par le choix du mapping, la sélection des champs réellement indexés, la configuration des analyzers, l’utilisation raisonnée de nested, le dimensionnement des shards et, enfin, la construction des requêtes. Mais il n’existe pas de configuration parfaite valable pour toutes les applications.
Le bon choix dépend toujours du contexte, du volume de données et surtout de ce que l’on cherche à améliorer. Performance d’indexation, vitesse de recherche et pertinence des résultats sont liées. Améliorer l’un peut avoir des conséquences sur les autres.
C’est pourquoi la meilleure démarche consiste d’abord à comprendre le fonctionnement du moteur, puis à mesurer, identifier les points de friction et choisir les leviers adaptés.
Avant de chercher à optimiser Elasticsearch, commencez par comprendre ce qu’il fait réellement. C’est souvent le premier gain de performance.
Besoin d’aller plus loin ?
Vous exploitez Elasticsearch sur un catalogue volumineux, une application métier ou une plateforme nécessitant des recherches rapides et pertinentes ?
Notre équipe peut vous accompagner dans l’analyse de votre architecture Elasticsearch, l’identification des points de blocage et la mise en œuvre de solutions adaptées à vos objectifs de performance.
Vous souhaitez faire le point sur votre architecture ou vos performances Elasticsearch ? Parlons-en.