Chapitre 10. Améliorer les performances

Ce chapitre couvre

  • Les API bulk, multiget et multisearch
  • Actualisation, vidange, fusion et stockage des données
  • Filtre de caches et réglage
  • Réglage des scripts
  • Le concept de warmers 
  • Équilibrage de la taille de segment de mémoire JVM et des caches du système d’exploitation

Elasticsearch est généralement qualifié de rapide lorsqu’il s’agit d’indexer, de rechercher et d’extraire des statistiques par le biais d’agrégations. Sauf que le terme « rapide » est un concept vague qui rend inévitable la question de «combien de temps? Comme pour tout, la rapidité dépend du cas d’utilisation, du matériel et de la configuration.

Dans ce chapitre, notre objectif est de vous présenter les meilleures pratiques de configuration d’Elasticsearch afin que vous puissiez le rendre performant pour votre cas d’utilisation. Dans chaque situation, il sera question de faire des compromis pour de la vitesse. Vous devrez donc bien choisir vos batailles:

  • Complexité des applications – Dans la première partie du chapitre, nous verrons comment regrouper plusieurs requête, telles que les requêtes d’index, de mise à jour, de suppression, d’obtention et de recherche, au cours d’un seul appel HTTP. Votre application doit être consciente de ce regroupement, mais il peut considérablement améliorer vos performances globales. Pensez à une indexation 20 ou 30 fois meilleure car vous aurez moins de trajets sur le réseau.
  • Vitesse d’indexation pour la vitesse de recherche ou l’inverse – Dans la deuxième partie du chapitre, nous examinerons plus en détail la manière dont Elasticsearch traite les segments Lucene: comment l’actualisation, la vidange, le regroupement de règles et les paramètres de stockage, fonctionnent et comment ils influencent l’indexation et les performances de recherche. Tès souvent, le réglage des performances au niveau de l’indexation a un impact négatif sur les recherches, et inversement.
  • Mémoire— Un facteur important dans la vitesse d’Elasticsearch est la mise en cache. Nous allons maintenant explorer les détails du cache de filtres et savoir comment utiliser les filtres pour en tirer le meilleur parti. Nous examinerons également le cache de requête de fragment et la manière de laisser suffisamment d’espace au système d’exploitation pour mettre en cache vos index, tout en conservant une taille de segment de mémoire suffisante pour Elasticsearch. Si la recherche sur les caches à froid devient trop lente, vous pourrez garder les caches au chaud en exécutant des requêtes en arrière-plan avec des index warmers.
  • Toutes les réponses ci-dessus— Selon le cas d’utilisation, la façon dont vous analysez le texte au moment de l’index et le type de requête que vous utilisez peut soit être plus compliquée, ralentir d’autres opérations ou alors utiliser davantage de mémoire. Dans la dernière partie du chapitre, nous explorerons les compromis typiques que vous obtiendrez lors de la modélisation de vos données et de vos requêtes: devez-vous générer plus de termes lorsque vous indexez ou parcourez plus de termes lors de votre recherche? Devez-vous tirer parti des scripts ou essayer de les éviter? Comment devez-vous gérer la pagination en profondeur?

Nous discuterons de tous ces points et répondrons à ces questions dans ce chapitre. À la fin, vous aurez appris à adapter Elasticsearch rapidement à votre cas d’utilisation et vous comprendrez mieux comment cela fonctionne. Regrouper plusieurs opérations dans une seule requête HTTP est souvent le moyen le plus simple d’améliorer les performances et procure le gain de performances le plus important. Commençons par regarder comment vous pouvez le faire via les API en bulk, multiget et multisearch.

10.1. GROUPEMENT DE REQUETES

La meilleure chose à faire pour une indexation plus rapide est d’envoyer plusieurs documents à indexer simultanément via l’API en bulk. Cela permettra d’économiser les allers-retours sur le réseau et d’accroître le débit d’indexation. Un seul groupe peut accepter n’importe quelle opération d’indexation; Par exemple, vous pouvez créer des documents ou les écraser. Vous pouvez également ajouter des opérations de mise à jour ou de suppression à l’API Bulk. Ce n’est pas seulement pour l’indexation.

Si votre application doit envoyer plusieurs opérations get ou search à la fois, il existe également des équivalents de type bloc pour celles-ci également: les API multiget et multisearch. Nous les explorerons plus tard, mais nous commencerons par l’API Bulk car, en production, c’est le «moyen» d’indexer pour la plupart des cas d’utilisation.

10.1.1. Indexation, mise à jour et suppression en masse

Jusqu’ici, dans ce livre, vous avez indexé des documents un à un. C’est bien pour jouer, mais cela implique des pénalités en termes de performance et cela en plus dans au moins deux directions:

  • Votre application doit attendre une réponse de Elasticsearch avant de pouvoir continuer.
  • Elasticsearch doit traiter toutes les données de la requête pour chaque document indexé.

Si vous avez besoin de plus de vitesse d’indexation, Elasticsearch propose une l’API Bulk, que vous pouvez utiliser pour indexer plusieurs documents à la fois, comme le montre la figure 10.1.

Comme le montre la figure, vous pouvez le faire en utilisant les requêtes HTTP, comme vous l’avez fait jusqu’à présent pour l’indexation des documents, et vous obtiendrez une réponse contenant les résultats de toutes les demandes d’indexation.

Indexation en groupe

Dans la figure 10.1, vous indexerez un ensemble de deux documents. Pour ce faire, vous devez effectuer une requête HTTP POST sur l’endpoint _bulk, avec des données dans un format spécifique. Le format a les exigences suivantes:

  • Chaque demande d’indexation est composée de deux documents JSON séparés par une nouvelle ligne: une avec l’opération (index dans votre cas) et les métadonnées (comme l’index, le type et l’ID) et l’autre avec le contenu du document.
  • Les documents JSON doivent être un par ligne. Cela implique que chaque ligne doit se terminer par une nouvelle ligne (\ n ou le caractère ASCII 10), y compris la dernière ligne de l’ensemble des demandes.

Pour chacune des deux requêtes d’indexation, vous ajoutez à la première ligne le type d’opération et des métadonnées. Le nom du champ principal est le type d’opération: il indique ce que Elasticsearch a à voir avec les données suivantes. Pour le moment, vous avez utilisé index pour l’indexation et cette opération écrasera les documents ayant le même identifiant s’ils existent déjà. Vous pouvez modifier cela en utilisant le mot clé create, pour vous assurer que les documents ne soient pas écrasés, ou alors update, delete, comme vous le verrez plus tard.

_index et _type indiquent où indexer chaque document. Vous pouvez mettre le nom de l’index ou alors les deux: le type et l’index dans l’URL. Il seront considéré comme l’index et le type par défaut pour chaque opération dans le bloc. Par exemple:

ou

Vous pouvez ensuite omettre les champs _index et type de la requête. Si vous spécifiez, les valeurs d’index et le type du corps de la requête remplacent ceux de l’URL.

Le champ _id indique l’ID du document que vous indexez. Si vous l’omettez, Elasticsearch généra un identifiant unique pour vous, ce qui est utile si vous n’avez pas encore d’identifiant pour vos documents. Les logs, par exemple, fonctionne bien avec les ID générés, étant donné qu’il n’ont pas d’ID naturel. De plus, vous n’avez pas besoin de récupérer les logs à partir de leur IDs.

Si vous n’avez pas besoin de fournir des identifiants et que vous indexez tous les documents dans le même index et le même type, la requête bulk de la figure 10.1 devient beaucoup plus simple.

Le résultat de votre insertion doit être un JSON contenant le temps nécessaire pour indexer votre enveloppe et les réponses pour chaque opération. Un indicateur d’erreur indique également si l’une des opérations a échoué. La réponse entière devrait ressembler à ceci:

Notez que, comme vous avez utilisé la génération automatique d’ID, les opérations d’index ont été modifiées à create. Si un document ne peut pas être indexé pour une raison quelconque, cela ne signifie pas que tout le bloc a échoué, car les éléments du même groupe sont indépendants les uns des autres. C’est pourquoi vous obtenez une réponse pour chaque opération, au lieu d’une pour l’ensemble. Vous pouvez utiliser le JSON de réponse dans votre application pour déterminer quelle opération a réussi et laquelle a échoué.



En termes de performances, la taille de la requête bulk compte. Si vos volumes sont trop gros, ils utiliseront trop de mémoire. Si elles sont trop petites, il y a trop de surcharge du réseau. La position idéale dépend de la taille du document (vous devez regrouper quelques gros documents ou plusieurs plus petits) et de la puissance de votre cluster. Un grand cluster doté de machines puissantes peut traiter des volumes plus importants plus rapidement tout en offrant des performances décentes aux recherches. En fin de compte, vous devez tester et trouver le bon compromis pour votre cas d’utilisation. Vous pouvez commencer avec des valeurs telles que 1 000 petits documents (tels que des logs) par bloc et augmenter jusqu’à obtenir un gain significatif. Veillez à surveiller votre cluster entre-temps, comme nous le verrons au chapitre 11.

Mise à jour ou suppression en bloc

Au sein d’un même bloc, vous pouvez avoir un nombre illimité d’opérations d’index ou de création, ainsi qu’un nombre illimité d’opérations de mise à jour ou de suppression.

Les opérations de mise à jour ressemblent aux opérations d’index / de création que nous venons de décrire, à l’exception du fait que vous devez spécifier l’ID. En outre, le contenu du document contiendrait un document ou un script en fonction de la manière dont vous souhaitez mettre à jour, tout comme vous avez spécifié un document ou un script au chapitre 3 lorsque vous effectuiez des mises à jour individuelles.

Les opérations de suppression sont un peu différentes des autres car vous n’avez aucun contenu de document. Vous avez juste la ligne de métadonnées, comme pour les mises à jour, qui doit contenir l’ID du document.

Dans la figure suivante, vous avez un bloc contenant les quatre opérations: index, create, update et delete.

Si les API bulk peuvent être utilisées pour regrouper plusieurs opérations d’index, de mise à jour et de suppression, vous pouvez faire la même chose pour les requêtes de type search et get avec les API multisearch et multiget, respectivement. Nous allons examiner ces prochaines.

10.1.2. API multi-recherche et multiget

L’utilisation de multisearch et multiget présente les mêmes avantages que l’API Bulk: lorsque vous devez effectuer plusieurs search ou get, leur regroupement permet d’économiser du temps, qui sera responsable de la latence du réseau.

Multisearch

Un cas d’utilisation pour l’envoi simultané de plusieurs requêtes de recherche se produit lorsque vous effectuez une recherche dans différents types de documents. Par exemple, supposons que vous ayez un champ de recherche dans votre site get-together. Vous ne savez pas si là recherche doit être faite dans les groupes ou les événements. Vous allez donc rechercher les deux et proposer différents onglets dans l’interface utilisateur: un pour les groupes et un pour les événements. Ces deux recherches ont des critères de scoring complètement différents, vous pouvez donc les exécuter dans différentes requêtes ou regrouper ces requêtes dans une requête multi-recherche.

L’API multisearch présente de nombreuses similitudes avec l’API Bulk:

  • Vous attaquez l’endpoint _msearch et vous pouvez ou non spécifier un index et un type dans l’URL.
  • Chaque requête comporte deux chaînes JSON d’une seule ligne: la première peut contenir des paramètres tels que l’index, le type, la valeur de routage ou le type de recherche – que vous aurez normalement placés dans l’URI s’il s’agissait d’une requête unique. La deuxième ligne contient le corps de la requête, qui correspond normalement à la charge utile s’il s’agissait d’une requête unique.

La figure ci-dessous montre un exemple de requête multisearch d’événements et de groupes concernant « Elasticsearch« .

Multiget

Multiget est utile lorsque certains traitements externes à Elasticsearch vous obligent à récupérer un ensemble de documents sans effectuer de recherche. Par exemple, si vous stockez des métriques système et que l’ID est un horodatage, vous devrez peut-être extraire des métriques spécifiques à des moments précis sans filtre. Pour ce faire, vous devez appeler le endpoint _mget et envoyer un tableau de documents avec l’index, le type et l’ID des documents que vous souhaitez récupérer, comme dans la figure suivante.

Comme avec la plupart des autres API, l’index et le type sont facultatifs, car vous pouvez également les insérer dans l’URL de la requête. Lorsque l’index et le type sont communs à tous les ID, il est recommandé de les insérer dans l’URL et de les placer dans un tableau ids, ce qui raccourcit considérablement la requête de la figure 10.5:

Le regroupement de plusieurs opérations dans les mêmes requête avec l’API multiget peut introduire un peu de complexité à votre application, mais cela rendra ces requêtes plus rapides sans coûts importants. Il en va de même pour les API multisearch et bulk. Pour en tirer le meilleur parti, vous pouvez tester différentes tailles de requêtes et déterminer la taille la mieux adaptée à vos documents et à votre hardware.

Nous verrons ensuite comment Elasticsearch traite les documents groupés en interne, sous la forme de segments Lucene, et comment ajuster ces processus pour accélérer l’indexation et la recherche.

10.2 OPTIMISATION DE LA MANIPULATION DES SEGMENTS DE LUCENE

Une fois que Elasticsearch a reçu les documents de votre application, il les indexe en mémoire dans des index inversés appelés segments. De temps en temps, ces segments sont écrits sur le disque. Rappelez-vous du chapitre 3 que ces segments ne peuvent pas être modifiés, mais seulement supprimés, pour que le système d’exploitation puisse les mettre en cache plus facilement. De plus, des segments plus grands sont périodiquement créés à partir de segments plus petits pour consolider les index inversés et accélérer les recherches.

Ils y a beaucoups de mannière d’influencer la façon dont Elasticsearch gère ces segments à chaque étape. Leur configuration pour s’adapter à votre cas d’utilisation permet souvent d’importants gains de performances. Dans cette section, nous allons examiner ces boutons et les diviser en trois catégories:

  • Fréquence d’actualisation et de flush: l’actualisation ouvre à nouveau la vue d’Elasticsearch sur l’index, ce qui permet de rechercher les nouveaux documents indexés. Le flush commit(sauvegarde) les données indexées de la mémoire vers le disque. Les opérations de rafraîchissement et de flush coûtent cher en termes de performances. Il est donc important de les configurer correctement pour votre cas d’utilisation.
  • Stratégies de fusion: Lucene (et par héritage, Elasticsearch) stocke les données dans des groupes de fichiers immuables, appelés segments. Lorsque vous indexez plus de données, plus de segments sont créés. Étant donné qu’une recherche dans plusieurs segments est lente, de petits segments sont fusionnés en arrière-plan en segments plus grands afin que leur nombre soit gérable. La fusion nécessite beaucoup de performances, en particulier pour le sous-système E / S. Vous pouvez ajuster la politique de fusion pour influer sur la fréquence des fusions et sur la taille des segments.
  • Stockage et limitation du stockage: Elasticsearch limite l’impact des fusions sur les E / S de votre système à un certain nombre d’octets par seconde. En fonction de votre matériel et de votre cas d’utilisation, vous pouvez modifier cette limite. Il existe également d’autres options pour la manière dont Elasticsearch utilise le stockage. Par exemple, vous pouvez choisir de stocker vos index uniquement en mémoire.

Nous allons commencer par la catégorie qui vous donne généralement le plus grand gain de performance des trois: la fréquence d’actualisation et de flush.

10.2.1. Actualisation et déclenchement des méchanismes de flush

Rappelez-vous au chapitre 2, on a dit qu’ Elasticsearch est dit temps quasi réel, NRT(near-real-time); c’est parce que les recherches ne sont souvent pas effectuées sur les toutes dernières données indexées (ce qui serait en temps réel), mais presque.

Cette étiquette en temps quasi réel convient parfaitement, car normalement, Elasticsearch conserve une vue ponctuelle de l’index, ce qui permet à plusieurs recherches d’accéder aux mêmes fichiers et de réutiliser les mêmes caches. Pendant ce temps, les documents nouvellement indexés ne seront plus visibles pour ces recherches tant que vous n’aurez pas actualisé.

L’actualisation, comme son nom l’indique, actualise cette vue ponctuelle de l’index afin que vos recherches puissent accéder aux données nouvellement indexées. C’est le bon côté. L’inconvénient est que chaque actualisation entraîne une pénalité en termes de performances: certains caches seront invalidés, ce qui ralentira les recherches et le processus de réouverture lui-même aura besoin de temps de traitement, ce qui ralentira l’indexation.

Quand rafraîchir

Le comportement par défaut consiste à actualiser automatiquement chaque index chaque seconde. Vous pouvez modifier l’intervalle de chaque index en modifiant ses paramètres, ce qui peut être fait durant l’exécution. Par exemple, la commande suivante définira l’intervalle d’actualisation automatique sur 5 secondes:



Pour confirmer que vos modifications ont été appliquées, vous pouvez obtenir tous les paramètres d’index en exécutant curl localhost:9200/get-together/ _settings?pretty.



Au fur et à mesure que vous augmentez la valeur de refresh_interval, le débit d’indexation augmente, car l’actualisation nécessite moins de ressources système.

Vous pouvez également définir refresh_interval sur -1 pour désactiver efficacement les actualisations automatiques et vous fier à l’actualisation manuelle. Cela fonctionne bien pour les cas d’utilisation où les indices ne changent que périodiquement par lots, comme pour une chaîne de vente au détail où les produits et les stocks sont mis à jour toutes les nuits. Le débit d’indexation est important car vous souhaitez utiliser ces mises à jour rapidement, mais l’actualisation des données ne l’est pas, car vous n’obtenez pas les mises à jour en temps réel, de toute façon. Vous pouvez donc effectuer des index/mises à jour groupés et ceci avec l’actualisation automatique désactivée et raffraichir manuellement lorsque vous avez terminé.

Pour actualiser manuellement un index, on a son endpoint _refresh:

Quand faire le flush

Si vous êtes habitué aux anciennes versions de Lucene ou de Solr, vous aurez peut-être tendance à penser que lorsqu’une actualisation est effectuée, toutes les données indexées (en mémoire) depuis la dernière actualisation sont également validées sur le disque.

Avec Elasticsearch (et Solr 4.0 ou version ultérieure), le processus d’actualisation et le processus d’enregistrement de segments en mémoire sur le disque sont indépendants. En effet, les données sont d’abord indexées en mémoire, mais après une actualisation, Elasticsearch effectuera également une recherche des segments en mémoire. Le processus de validation(commit) des segments en mémoire dans l’index Lucene réel que vous avez sur le disque s’appelle un flush et il se produit que les segments soient interrogeables ou non.

Afin de s’assurer que les données en mémoire ne soient pas perdues lorsqu’un nœud tombe en panne ou qu’un fragment est déplacé, Elasticsearch conserve la trace des opérations d’indexation qui n’avaient pas encore été flushées dans un journal des transactions. En plus de valider, commiter des segments en mémoire sur le disque, le flush efface également le journal des transactions, comme illustré à la figure 10.2.

Le flush est déclenché dans l’une des conditions suivantes, comme illustré à la figure 10.3:

  • La mémoire tampon est pleine.
  • Un certain temps a passé depuis le dernir flush.
  • Le journal des transactions a atteint un certain seuil.

Pour contrôler la fréquence d’un flush, vous devez régler les paramètres qui contrôlent ces trois conditions.

La taille de la mémoire tampon est définie dans le fichier de configuration elasticsearch.yml via le paramètre index.memory.index_buffer_size. Cela contrôle la mémoire tampon globale pour l’ensemble du nœud et la valeur peut correspondre à un pourcentage du segment de mémoire global de la machine virtuelle Java tel que 10% ou à une valeur fixe telle que 100 Mo.

Les paramètres du journal des transactions sont spécifiques à l’index et contrôlent à la fois la taille à laquelle le flush est déclenché (via index.translog.flush_threshold_size) et le temps écoulé depuis le dernier flush(via index.translog.flush_threshold_period). Comme avec la plupart des paramètres d’index, vous pouvez les modifier au moment de l’exécution:

Lorsqu’un flush est effectué, un ou plusieurs segments sont créés sur le disque. Lorsque vous exécutez une requête, Elasticsearch (via Lucene) examine tous les segments et fusionne les résultats dans un résultat de fragment global. Ensuite, comme vous l’avez vu au chapitre 2, les résultats par partition sont regroupés dans les résultats globaux renvoyés à votre application.

La chose essentielle à retenir ici concernant les segments est que plus vous devez rechercher de segments, plus la recherche est lente. Pour limiter le nombre de segments, Elasticsearch (à nouveau via Lucene) fusionne plusieurs ensembles de segments plus petits en segments plus grands en arrière-plan.

10.2.2. Fusions et politiques de fusion

Nous avons d’abord présenté les segments du chapitre 3 sous forme d’ensembles de fichiers immuables utilisés par Elasticsearch pour stocker les données indexées. Comme ils ne changent pas, les segments sont facilement mis en cache, ce qui facilite les recherches. De plus, les modifications apportées à l’ensemble de données, telles que l’ajout d’un document, ne nécessitent pas de reconstruction de l’index pour les données stockées dans des segments existants. Cela accélère également l’indexation des nouveaux documents, mais ce n’est pas totalement une bonne nouvelle. En effet la mise à jour d’un document ne peut pas changer le document réel; il ne peut en indexer qu’un nouveau. Cela nécessite également la suppression de l’ancien document. La suppression, à son tour, ne peut pas supprimer un document de son segment (cela nécessiterait de reconstruire l’index inversé), il est donc uniquement marqué comme supprimé dans un fichier .del séparé. Les documents ne seront réellement supprimés que lors de la fusion de segments.

Cela nous amène aux deux objectifs de la fusion de segments: contrôler le nombre total de segments (et, par conséquent, les performances de la requête) et supprimer les documents supprimés.

La fusion de segments se produit en arrière-plan, conformément à la politique de fusion définie. La stratégie de fusion par défaut est hiérarchisée, ce qui, comme illustré à la figure 10.4, divise les segments en couches et si vous avez un nombre de segments supérieur au nombre maximal défini dans une couche, une fusion est déclenchée dans cette couche.

Il existe d’autres stratégies de fusion, mais dans ce chapitre, nous nous concentrerons uniquement sur la stratégie de fusion à plusieurs niveaux, qui est la stratégie par défaut, car elle convient mieux à la plupart des cas d’utilisation.



Mike McCandless (co-auteur de Lucene in Action, deuxième édition [Manning Publications, 2010]) propose de jolies vidéos et des explications sur les différentes politiques de fusion: http://blog.mikemccandless.com/2011/02/ visualizing-lucenes-segment-merges.html.



Réglage des options de politique de fusion

La fusion se produit lorsque vous indexez, mettez à jour ou supprimez des documents. Ainsi, plus vous fusionnez, plus ces opérations coûtent cher. Inversement, si vous souhaitez une indexation plus rapide, vous devez en fusionner moins et sacrifier certaines performances de recherche.

Afin d’avoir plus ou moins de fusion, vous avez quelques options de configuration. Voici les plus importants:

  • index.merge.policy.segments_per_tier: plus la valeur est élevée, plus vous pouvez avoir de segments dans un niveau. Cela se traduira par moins de fusion et une meilleure performance d’indexation. Si vous avez peu d’indexation et que vous souhaitez améliorer les performances de recherche, réduisez cette valeur.
  • index.merge.policy.max_merge_at_once: ce paramètre limite le nombre de segments pouvant être fusionnés simultanément. Vous devez généralement le rendre égal à la valeur de segments_per_tier. Vous pouvez réduire la valeur max_merge_at_once pour forcer la diminution de la fréquence des fusion, mais il est préférable de le faire en augmentant segments_per_tier. Assurez-vous que max_merge_at_once n’est pas supérieur à segments_per_tier car cela entraînerait une trop grande fusion.
  • index.merge.policy.max_merged_segment: ce paramètre définit la taille maximale du segment. Les segments plus importants ne seront pas fusionnés avec d’autres segments. Vous pouvez baisser cette valeur si vous souhaitez moins de fusion et une indexation plus rapide car les segments plus volumineux sont plus difficiles à fusionner.
  • index.merge.scheduler.max_thread_count: la fusion a lieu en arrière-plan sur des threads distincts. Ce paramètre définit le nombre maximal de threads pouvant être utilisés pour la fusion. C’est la limite stricte du nombre de fusions possibles à la fois. Vous augmentez ce paramètre pour une stratégie de fusion agressive sur une machine dotée de plusieurs processeurs et d’E / S rapides, et vous le diminuez si vous avez un processeur ou des E / S lents.

Toutes ces options sont spécifiques à l’index et, comme pour le journal des transactions et les paramètres d’actualisation, vous pouvez les modifier au moment de l’exécution. Par exemple, le fragment de code suivant force davantage la fusion en réduisant le nombre de segments_per_tier à 5 (et, par conséquent, max_merge_at_once), réduit la taille de segment maximale à 1 Go et réduit le nombre de threads à 1 pour fonctionner mieux avec des disques en rotation:

Optimiser les indices

Comme pour le rafraîchissement et le flush, vous pouvez déclencher une fusion manuellement. Un appel à la fusion forcée est également appelé optimisation, car vous l’exécutez généralement sur un index qui ne sera pas modifié ultérieurement pour l’optimiser à un nombre spécifié (faible) de segments pour une recherche plus rapide.

Comme dans toute fusion agressive, l’optimisation nécessite beaucoup d’E / S et invalide de nombreux caches. Si vous continuez à indexer, mettre à jour ou supprimer des documents à partir de cet index, de nouveaux segments seront créés et les avantages de l’optimisation seront perdus. Ainsi, si vous souhaitez moins de segments sur un index en constante évolution, vous devez ajuster la stratégie de fusion.

L’optimisation a du sens sur un index statique. Par exemple, si vous indexez des données de médias sociaux et que vous en avez un par jour, vous savez que vous ne modifierez jamais l’index d’hier avant de le supprimer définitivement. Comme l’illustre la figure 10.5, il pourrait être utile de l’optimiser pour un nombre réduit de segments, ce qui réduira sa taille totale et accélérera les requêtes une fois que les caches auront été réchauffés(warmed).

Pour optimiser, vous devez attquer le endpoint _optimize du ou des index à optimiser. L’option max_num_segments indique le nombre de segments par partition que vous devriez obtenir:

Un appel à l’optimisation peut prendre beaucoup de temps sur un index volumineux. Vous pouvez le faire en arrière-plan en définissant wait_for_merge sur false.

L’une des raisons pouvant expliquer le ralentissement d’une optimisation (ou de toute fusion) est qu’Elasticsearch limite par défaut la quantité d’opérations de fusion que le débit peut utiliser. Cette limitation s’appelle la store throttling et nous en discuterons ensuite, ainsi que d’autres options pour stocker vos données.

10.2.3. Le stockage et le store throttling

Dans les premières versions d’Elasticsearch, une fusion intensive risquait de ralentir le cluster au point que les requêtes d’indexation et de recherche prendraient une longueur inacceptable, ou que les noeuds risquaient de ne plus répondre. Tout cela était dû à la pression de la fusion sur le débit d’E / S, ce qui ralentirait l’écriture de nouveaux segments. En outre, la charge du processeur était plus élevée en raison d’une attente d’E / S.

En conséquence, Elasticsearch limite désormais la quantité de débit d’E / S que les fusions peuvent utiliser via le store throttling. Par défaut, il existe un paramètre au niveau du nœud appelé index.store.throttle.max_bytes_per_sec, dont la valeur par défaut est de 20 Mo à partir de la version 1.5.

Cette limite est bonne pour la stabilité dans la plupart des cas d’utilisation mais ne fonctionnera pas bien pour tout le monde. Si vous avez des machines rapides et beaucoup d’indexation, les fusions ne suivront pas, même s’il ya assez de CPU et d’E / S pour les exécuter. Dans de telles situations, Elasticsearch n’effectue l’indexation interne que sur un seul thread, ce qui le ralentit pour permettre aux fusions de suivre. En fin de compte, si vos machines sont rapides, l’indexation peut être limitée par le store throttling. Pour les nœuds dotés de disques SSD, vous pouvez augmenter normalement la limite de throttling à 100–200 Mo.

Modification des limites de store throttling

Si vous avez des disques rapides et que vous avez besoin de plus de débit d’E / S pour la fusion, vous pouvez augmenter la limite de store throttling. Vous pouvez également supprimer complètement la limite en définissant les indices .store.throttle.type sur none. De l’autre côté du spectre, vous pouvez appliquer la limite de store throttling à toutes les opérations de disque d’Elasticsearch, et pas seulement à la fusion, en définissant index.store.throttle.type sur all.

Ces paramètres peuvent être modifiés à partir du fichier elasticsearch.yml sur chaque nœud, mais ils peuvent également être modifiés au moment de l’exécution via l’API de mise à jour des paramètres du cluster. Normalement, vous les paramétrez tout en surveillant le nombre réel d’activités de fusion et d’autres activités sur disque. Nous allons vous montrer comment faire cela au chapitre 11.



Elasticsearch 2.0, qui reposera sur Lucene 5.0, utilisera la fonctionnalité auto-io-throttle de Lucene [1], qui limitera automatiquement les fusions en fonction du niveau d’indexation. Si l’indexation est faible, les fusions seront davantage limitées afin qu’elles n’affectent pas les recherches. S’il y a beaucoup d’indexation, il y aura moins de limitation au niveau de la fusion, de sorte que les fusions ne prennent pas de retard.



La commande suivante lève la limite de limitation à 500 Mo / s mais l’applique à toutes les opérations. La modification sera également persistante pour survivre au redémarrage complet du cluster (contrairement aux paramètres transitoires perdus lors du redémarrage du cluster):



Comme pour les paramètres d’index, vous pouvez également obtenir les paramètres de cluster pour voir s’ils sont appliqués. Vous le feriez en exécutant la commande curl localhost:9200/_cluster/settings?



Configuration du store

Lorsque nous avons parlé des flush, des fusions et du store throttling, nous avons dit «disque». Elasticsearch stockera les index dans le répertoire de données qui est par défaut /var/lib/elasticsearch/data si vous l’avez installé à partir d’un package RPM / DEB ou le répertoire data/ si c’était manuellement, à partir de l’archive tar.gz ou ZIP décompressé. Vous pouvez modifier le répertoire de données à partir de la propriété path.data de elasticsearch.yml.



Vous pouvez spécifier plusieurs répertoires dans path.data qui, dans la version 1.5 au moins, placera des fichiers différents dans des répertoires différents pour réaliser une répartition (en supposant que ces répertoires se trouvent sur des disques différents). Si c’est ce que vous recherchez, il vaut souvent mieux utiliser RAID0, en termes de performances et de fiabilité. Pour cette raison, il est prévu de placer chaque fragment dans le même répertoire au lieu de le séparer.



L’implémentation du store par défaut stocke les fichiers d’index dans le système de fichiers et fonctionne bien pour la plupart des cas d’utilisation. Pour accéder aux fichiers de segment Lucene, l’implémentation du store par défaut utilise le MMapDirectory de Lucene pour les fichiers généralement volumineux ou nécessitant un accès aléatoire, tels que les dictionnaires de termes. Pour les autres types de fichiers, tels que les champs stockés, Elasticsearch utilise le répertoire NIOFS de Lucene.

MMapDirectory

MMapDirectory tire parti des caches de système de fichiers en demandant au système d’exploitation de mapper les fichiers nécessaires dans la mémoire virtuelle afin d’accéder directement à cette mémoire. Pour Elasticsearch, il semble que tous les fichiers soient disponibles en mémoire, mais ce n’est pas nécessairement le cas. Si la taille de votre index est supérieure à votre mémoire physique disponible, le système d’exploitation supprimera volontiers les fichiers inutilisés des caches pour laisser de la place aux nouveaux fichiers à lire. Si Elasticsearch a de nouveau besoin de ces fichiers non mis en cache, ceux-ci seront chargés en mémoire, tandis que d’autres fichiers inutilisés sont supprimés, etc. La mémoire virtuelle utilisée par MMapDirectory fonctionne de manière similaire à la mémoire virtuelle du système (swap), où le système d’exploitation utilise le disque pour extraire de la mémoire inutilisée afin de pouvoir servir plusieurs applications.

NIOFSDirectory

Les fichiers mappés en mémoire impliquent également un temps système car l’application doit indiquer au système d’exploitation de mapper un fichier avant de pouvoir y accéder. Pour réduire cette surcharge, Elasticsearch utilise NIOFSDirectory pour certains types de fichiers. NIOFSDirectory accède aux fichiers directement, mais il doit copier les données nécessaires à la lecture dans une mémoire tampon de la pile JVM. Cela le rend bon pour les petits fichiers à accès séquentiels, alors que MMapDirectory fonctionne bien pour les gros fichiers à accès aléatoire.

L’implémentation du store par défaut est la meilleure pour la plupart des cas d’utilisation. Cependant, vous pouvez choisir d’autres implémentations en remplaçant index.store.type dans les paramètres d’index par des valeurs autres que celles par défaut:

  • mmapfs— Ceci utilisera le MMapDirectory seul et fonctionnera bien, par exemple, si vous avez un index relativement statique qui tient dans votre mémoire physique.
  • niofs – Ceci utilisera NIOFSDirectory seul et fonctionnera bien sur les systèmes 32 bits, où l’espace d’adressage de la mémoire virtuelle est limité à 4 Go, ce qui vous évitera d’utiliser mmapfs ou par défaut pour des index plus volumineux.

Ces paramètres doivent être configurés lors de la création de l’index. Par exemple, la commande suivante crée un index mmap-ed appelé unit-test:

Si vous souhaitez appliquer le même type de votre store à tous les index nouvellement créés, vous pouvez définir index.store.type sur mmapfs dans elasticsearch.yml. Au chapitre 11, nous présenterons des modèles d’index permettant de définir des paramètres d’index s’appliquant à de nouveaux index correspondant à des modèles spécifiques. Les modèles peuvent également être modifiés au moment de l’exécution. Nous vous recommandons de les utiliser à la place de l’équivalent plus statique de elasticsearch.yml si vous créez souvent de nouveaux index.



Fichiers ouverts et limites de la mémoire virtuelle

Les segments Lucene stockés sur le disque peuvent s’étendre sur de nombreux fichiers et, lorsqu’une recherche est exécutée, le système d’exploitation doit pouvoir en ouvrir plusieurs. De plus, lorsque vous utilisez le type de magasin par défaut ou mmapfs, le système d’exploitation doit mapper certains de ces fichiers stockés en mémoire – même si ces fichiers ne sont pas en mémoire, ils sont comme dans l’application, et le noyau prend soin de les charger et les décharger dans le cache. Linux a des limites configurables qui empêchent les applications d’ouvrir trop de fichiers en même temps et de mapper trop de mémoire. Ces limites sont généralement plus conservatrices que nécessaire pour les déploiements Elasticsearch. Il est donc recommandé de les augmenter. Si vous installez Elasticsearch à partir d’un package DEB ou RPM, vous n’avez pas à vous en préoccuper, car ils ont été augmentés par défaut. Vous pouvez trouver ces variables dans /etc/default/elasticsearch ou /etc/sysconfig/elasticsearch:

Pour augmenter ces limites manuellement, vous devez exécuter ulimit -n 65535 en tant qu’utilisateur qui lance Elasticsearch pour les fichiers ouverts, puis sysctl -w vm.max_map_count = 262144 en tant que root pour la mémoire virtuelle.



Le type par défaut du store est généralement le plus rapide en raison de la manière dont le système d’exploitation met en cache les fichiers. Pour que la mise en cache fonctionne correctement, vous devez disposer de suffisamment de mémoire libre.



À partir de Elasticsearch 2.0, vous pourrez compresser davantage les champs stockés (et _source) en définissant index.codec sur best_compression.  La valeur par défaut (nommée default, comme avec les types du store) compresse toujours les champs stockés à l’aide de LZ4, mais best_compression utilise deflate. [4] Une compression plus élevée ralentira les opérations nécessitant _source, telles que l’extraction des résultats ou la mise en surbrillance. Les autres opérations, telles que les agrégations, doivent être au moins aussi rapides car l’index global sera plus petit et plus facile à mettre en cache.



Nous avons mentionné comment les opérations de fusion et d’optimisation invalident les caches. La gestion des caches pour qu’Elasticsearch fonctionne correctement mérite plus d’explications. Nous en discuterons ensuite.

10.3. FAIRE LA MEILLEUR UTILISATION DES CACHES

Un des points forts d’Elasticsearch – sinon le plus fort – est le fait que vous pouvez interroger des milliards de documents en quelques millisecondes avec du matériel standard. Et l’une des raisons pour lesquelles cela est possible est sa mise en cache intelligente. Vous avez peut-être remarqué qu’après l’indexation de nombreuses données, la deuxième requête peut être beaucoup plus rapide que la première. C’est à cause de la mise en cache, par exemple lorsque vous combinez des filtres et des requêtes. Le cache de filtres joue un rôle important dans la rapidité de vos recherches.

Dans cette section, nous aborderons le cache de filtre et deux autres types de caches: le cache de requête de fragment, utile lorsque vous exécutez des agrégations sur des index statiques car il met en cache le résultat global, et les caches du système d’exploitation, qui conservent votre débit d’E / S. élevé en mettant en cache les index en mémoire.

Enfin, nous allons vous montrer comment garder toutes ces caches au chaud en lançant des requêtes à chaque actualisation avec les warmers d’index. Commençons par examiner le type de cache principal spécifique à Elasticsearch – le cache de filtre – et comment vous pouvez exécuter vos recherches pour en tirer le meilleur parti.

10.3.1. Filtres et Filtres de caches

Au chapitre 4, vous avez vu que beaucoup de requêtes ont un filtre équivalent. Supposons que vous souhaitiez rechercher des événements sur le site de rencontre qui se sont passés le mois dernier. Pour ce faire, vous pouvez utiliser la query de plage ou le filtre de plage équivalent.

Au chapitre 4, nous avons dit qu’entre les deux, nous vous recommandons d’utiliser le filtre, car il peut être mis en cache. Le filtre de plage(range) est mis en cache par défaut, mais vous pouvez contrôler si un filtre est mis en cache ou non via le flag _cache.



Elasticsearch 2.0 mettra en cache, par défaut, uniquement les filtres fréquemment utilisés et uniquement sur des segments plus volumineux (fusionnés au moins une fois). Cela devrait empêcher la mise en cache de manière trop agressive, mais devrait également récupérer les filtres fréquents et les optimiser. Cet indicateur s’applique à tous les filtres. Par exemple, l’extrait suivant filtrera les événements avec « elasticsearch » dans la balise verbatim mais ne mettra pas les résultats en cache:



Bien que tous les filtres aient le flag _cache, il ne s’applique pas dans 100% des cas. Pour le filtre de plage(range), si vous utilisez « now » comme l’une des limites, le flag est ignoré. Pour les filtres has_child ou has_parent, l’indicateur _cache ne s’applique pas du tout.



Cache de filtre
Les résultats d’un filtre mis en cache sont stockés dans le cache de filtres. Ce cache est alloué au niveau du nœud, à l’instar de la taille de la mémoire tampon d’index que vous avez vue précédemment. La valeur par défaut est 10%, mais vous pouvez la modifier elasticsearch.yml en fonction de vos besoins. Si vous utilisez beaucoup de filtres et que vous les mettez en cache, il peut être judicieux d’augmenter la taille. Par exemple:

Comment savoir si vous avez besoin de plus (ou moins) de cache de filtre? En monitorant votre utilisation réelle. Comme nous le verrons au chapitre 11 sur l’administration, Elasticsearch expose de nombreuses métriques, notamment la quantité de cache de filtre réellement utilisée et le nombre d’expulsions(éviction) de cache. Une expulsion se produit lorsque le cache est plein et qu’Elasticsearch supprime l’entrée la moins récemment utilisée (LRU) afin de laisser de la place à la nouvelle.

Dans certains cas d’utilisation, les entrées du cache de filtres ont une durée de vie courte. Par exemple, les utilisateurs filtrent généralement les événements de rencontre d’un sujet particulier, affinent leurs requêtes jusqu’à ce qu’ils trouvent ce qu’ils veulent, puis s’en vont. Si personne d’autre ne recherche des événements sur le même sujet, cette entrée en mémoire cache restera inchangée jusqu’à ce qu’elle soit finalement expulsée. Un cache complet comportant de nombreuses expulsions nuirait aux performances car chaque recherche consomme des cycles de processeur pour comprimer de nouvelles entrées de cache en évacuant les anciennes.

Dans de tels cas d’utilisation, pour empêcher les expulsions de se produire exactement au moment où les requêtes sont exécutées, il est logique de définir une durée de vie (TTL) sur les entrées de cache. Vous pouvez le faire index par index en ajustant index.cache.filter.expire. Par exemple, l’extrait suivant expirera les caches de filtre après 30 minutes:

En plus de vous assurer que vous avez assez de place dans vos caches de filtres, vous devez exécuter vos filtres de manière à tirer parti de ces caches.

Combinaison de filtres

Vous devez souvent combiner des filtres, par exemple lorsque vous recherchez des événements dans une certaine période, mais également avec un certain nombre de participants. Pour de meilleures performances, vous devez vous assurer que les caches soient bien utilisés lorsque les filtres sont combinés et que les filtres sont exécutés dans le bon ordre.

Pour comprendre comment combiner au mieux les filtres, nous devons revenir sur un concept présenté au chapitre 4: les bitsets. Un bitet est un tableau compact de bits, utilisé par Elasticsearch pour mettre en cache si un document correspond à un filtre ou non. La plupart des filtres (tels que les filtres de plage et de termes) utilisent des bits pour la mise en cache. D’autres filtres, tels que le filtre de script, n’utilisent pas de bits car Elasticsearch doit de toute façon parcourir tous les documents. Le tableau 10.1 montre quels filtres importants utilisent des bits et lesquels ne le font pas.

Pour les filtres qui n’utilisent pas de bits, vous pouvez toujours définir _cache sur true afin de mettre en cache les résultats de ce filtre exact. Les bits sont différents de la simple mise en cache des résultats car ils présentent les caractéristiques suivantes:

  • Ils sont compacts et faciles à créer. Par conséquent, la surcharge de la création du cache lors de la première exécution du filtre est insignifiante.
  • Ils sont stockés par filtre individuel; Par exemple, si vous utilisez un filtre de terme dans deux requêtes différentes ou dans deux filtres de booléen différents, le bitset du filtre terme peut être réutilisé.
  • Ils sont faciles à combiner avec d’autres bitsets. Si vous avez deux requêtes utilisant des bits, il est facile pour Elasticsearch d’effectuer un ET / OU au niveau du bit afin de déterminer quels documents correspondent à la combinaison.

Pour tirer parti des bitset, vous devez combiner des filtres qui les utilisent dans un filtre bool qui effectuera l’opération AND ou OU au niveau des bitset, ce qui est simple pour votre processeur. Par exemple, si vous souhaitez afficher uniquement les groupes dont Lee est membre ou qui contiennent le tag elasticsearch, cela pourrait ressembler à ceci:

L’alternative à la combinaison de filtres consiste à utiliser les filtres AND, OR, et NOT. Ces filtres fonctionnent différemment car, contrairement au filtre bool, ils n’utilisent aucun de ces opérateurs au niveau du bit. Ils lancent le premier filtre, transmettent les documents correspondants au suivant, etc. Par conséquent, les filtre,AND, OR, et NOT sont préférables lorsqu’il s’agit de combiner des filtres qui n’utilisent pas de bits. Par exemple, si vous souhaitez afficher les groupes composés d’au moins trois membres et ayant organisé des événements en juillet 2013, le filtre peut ressembler à ceci:

Si vous utilisez des filtres utilisant les bitset et d’autres non, vous pouvez combiner ceux bitset dans un filtre bool et insérer ce filtre bool dans un filtre AND, OR, et NOT, ainsi que les filtres non-bits. Par exemple, dans la figure suivante, vous rechercherez des groupes comprenant au moins deux membres, où Lee est l’un d’eux, ou alors le groupe parle d’Elasticsearch.

Que vous combiniez les filtres BOOL, AND, OR, et NOT , l’ordre dans lequel ces filtres sont exécutés est important. Les filtres utilisants moins de ressources, tels que le filtre de terme, doivent être placés avant les autres, tels que le filtre de script. Cela ferait fonctionner le filtre coûteux sur un ensemble de documents plus petit, les documents ayant déjà étés validés par les filtres précédents.

Exécution de filtres sur les données de champ

Jusqu’à présent, nous avons expliqué comment les bits et les résultats en cache accélèrent l’utilisation de vos filtres. Certains filtres utilisent des bitsets; certains peuvent mettre en cache les résultats globaux. Certains filtres peuvent également fonctionner sur des données de champ. Nous avons d’abord abordé les données de terrain(fielddata) au chapitre 6 en tant que structure en mémoire permettant de mapper les documents aux termes. Ce mapping est l’opposé de l’index inversé, qui mappe les termes aux documents. Les données de champ sont généralement utilisées lors du tri et des agrégations, mais certains filtres peuvent également les utiliser: les filtres de termes et de plages.



Une alternative aux données de champ en mémoire consiste à utiliser des valeurs de document(doc_value), qui sont calculées au moment de l’index et stockées sur le disque avec le reste de votre index. Comme nous l’avons souligné au chapitre 6, les doc_values fonctionnent pour les champs numériques et non analysés. Dans Elasticsearch 2.0, les doc_values seront utilisées par défaut pour ces champs car la conservation des données de champ dans le segment de mémoire de la machine virtuelle Java ne garantie généralement pas l’augmentation des performances.



Un filtre de termes peut avoir beaucoup de termes, et un filtre de plage avec une large plage correspondra (sous le capot) à beaucoup de nombres (et les nombres sont aussi des termes). L’exécution normale de ces filtres tentera de faire correspondre chaque terme séparément et renverra l’ensemble de documents uniques, comme illustré à la figure 10.6.

Comme vous pouvez l’imaginer, filtrer selon de nombreux termes pourrait coûter cher car il y aurait beaucoup de listes à croiser. Lorsque le nombre de termes est grand, il peut être plus rapide de prendre les valeurs de champ réelles un par un et de voir si les termes correspondent ou non, plutôt que de chercher dans l’index, comme l’illustre la figure 10.7.

Ces valeurs de champ seraient chargées dans le cache de données de champ en définissant l’exécution sur données de champ dans les filtres de termes ou de plage. Par exemple, le filtre de plage suivant obtiendra les événements survenus en 2013 et sera exécuté sur les données de champ:

L’utilisation de l’exécution de données de terrain est particulièrement utile lorsque les données de terrain sont déjà utilisées par une opération de tri ou une agrégation. Par exemple, l’exécution d’une agrégation de termes sur le champ tags accélérera le filtre des termes suivants sur ce champs, car les données du champ sont déjà chargées.



Autres modes d’exécution pour le filtre de termes: bool et AND/OR

Le filtre de termes a également d’autres modes d’exécution. Si le mode d’exécution par défaut (appelé plain) crée un jeu de bits pour mettre en cache le résultat global, vous pouvez le définir sur bool afin de disposer d’un jeu de bits pour chaque terme. Ceci est utile lorsque vous avez différents filtres de termes, qui ont beaucoup de termes en commun.

En outre, il existe des modes and/or d’exécution qui exécutent un processus similaire, à l’exception des filtres de termes individuels qui sont encapsulés dans un filtre and/or au lieu d’un filtre bool.

Généralement, l’approche and/or est plus lente que bool car elle ne tire pas parti des bitset. and/or peut être plus rapide si les premiers filtres de termes ne correspondent qu’à quelques documents, ce qui rend les filtres ultérieurs extrêmement rapides.



Pour résumer, vous avez trois options pour exécuter vos filtres:

  • Les mettre en cache dans le cache de filtres, ce qui est génial lorsque les filtres sont réutilisés
  • Ne pas les mettre en cache s’ils ne sont pas réutilisés
  • Exécution de filtres de termes et de plages sur les données de champ, ce qui est utile lorsque vous avez plusieurs termes, en particulier si les données de champ de ce champ sont déjà chargées

Ensuite, nous examinerons le cache de requête de fragment, ce qui est utile lorsque vous réutilisez des requêtes de recherche complètes sur des données statiques.

10.3.2. Cache de requête aux niveau des fragments

Le cache de filtres est spécialement conçu pour accélérer l’exécution des éléments d’une recherche, à savoir les filtres configurés pour être mis en cache. Il est également spécifique à un segment: si certains segments sont supprimés par le processus de fusion, les caches d’autres segments restent intacts. En revanche, le cache de requête de fragment maintient un mapping entre la requête entière et ses résultats au niveau du fragment, comme illustré à la figure 10.8. Si un fragment a déjà répondu à une requête identique, il peut y répondre de nouveau mais cette fois ci à partir du cache.

À partir de la version 1.4, les résultats mis en cache au niveau de la partition sont limités au nombre total de hits (pas les hits eux-mêmes), d’agrégations et de suggestions. C’est pourquoi (dans la version 1.5, au moins), le cache de requêtes au niveau des fragments ne fonctionne que lorsque votre requête a search_type défini sur count.



En définissant search_type à count dans les paramètres URI, vous indiquez à Elasticsearch que les résultats de la requête ne vous intéressent pas, mais uniquement leur nombre. Nous examinerons le nombre et d’autres types de recherche plus loin dans cette section. Dans Elasticsearch 2.0, définir size sur 0 fonctionnera également et search_type = count sera obsolète.



Les entrées du cache de requête au niveau des fragments diffèrent d’une requête à l’autre, elles ne s’appliquent donc qu’à un ensemble restreint de requêtes. Si vous recherchez un terme différent ou utilisez une agrégation légèrement différente, ce sera un échec au niveau du cache. De même, lorsqu’une actualisation se produit et que le contenu de la partition est modifié, toutes les entrées du cache de la requête  sont invalidées. Sinon, de nouveaux documents correspondants pourraient avoir été ajoutés à l’index et vous obtiendriez des résultats obsolètes à partir du cache.

Cette étroitesse des entrées de cache rend le cache de requête précieux uniquement lorsque les fragments changent rarement et que vous avez de nombreuses requêtes identiques. Par exemple, si vous indexez les logs et que vous avez des index temporels, vous pouvez souvent exécuter des agrégations sur des index plus anciens qui restent généralement inchangés jusqu’à ce qu’ils soient supprimés. Ces index plus anciens sont des candidats idéaux pour un cache de requête au niveau des fragments.

Pour activer le cache de requête par défaut au niveau de l’index, vous pouvez utiliser l’API s de mise à jour des paramètres d’index:



Comme pour tous les paramètres d’index, vous pouvez activer le cache de requête au niveau des fragments lors de la création d’index, mais il est logique de le faire uniquement si votre nouvel index est fréquemment interrogé et mis à jour rarement.



Pour chaque requête, vous pouvez également activer ou désactiver ce cache, en écrasant la configuration faite au niveau de l’index. Ceci en ajoutant le paramètre query_cache. Par exemple, pour mettre en cache l’agrégation fréquente top_tags sur notre index de rendez-vous, même si la valeur par défaut est désactivée, vous pouvez l’exécuter comme suit:

Comme le cache de filtre, le cache de requête a un paramètre de configuration size. La limite peut être modifiée au niveau du nœud en ajustant index.cache.query.size à partir de elasticsearch.yml, à partir de la valeur par défaut de 1% du segment de mémoire JVM.

Lors du dimensionnement du segment de mémoire JVM lui-même, vous devez vous assurer que vous disposez de suffisamment d’espace pour le cache et le cache de requête. Si la mémoire (en particulier le segment de la machine virtuelle Java) est limitée, vous devez réduire la taille du cache pour laisser plus de place à la mémoire utilisée de toute façon par les requêtes d’index et de recherche afin d’éviter les exceptions de mémoire insuffisante, les fameuses out-of-memory

En outre, vous devez disposer de suffisamment de RAM disponible en plus du segment de mémoire JVM pour permettre au système d’exploitation de mettre en cache les index stockés sur le disque. sinon, vous aurez beaucoup de recherches sur le disque.

Nous verrons ensuite comment équilibrer le segment de mémoire de la machine virtuelle Java avec les caches de système d’exploitation et comprendre pourquoi cela est important.

10.3.3. Tas de la jvm et caches de système d’exploitation

Si Elasticsearch ne dispose pas d’assez de mémoire pour terminer une opération, il génère une exception de mémoire insuffisante(out-of-memory exception) qui fait planter le noeud et le fait sortir du cluster. Cela impose une charge supplémentaire aux autres nœuds lors de la réplication et du déplacement des fragments afin de revenir à l’état configuré. Comme les nœuds sont généralement égaux, cette charge supplémentaire risque de faire manquer de mémoire à un autre nœud. Un tel effet domino peut détruire votre cluster tout entier.

Lorsque le segment de mémoire de la machine virtuelle Java est petit, même si vous ne voyez pas d’erreur de mémoire insuffisante dans les log, le nœud peut devenir tout aussi insensible. Cela peut arriver parce que le manque de mémoire oblige le garbage collector (GC) à fonctionner plus longtemps et plus souvent afin de libérer de la mémoire. Etant donné qu’il prend plus de temps processeur, le nœud dispose de moins de puissance de calcul pour répondre aux requête ou même pour répondre aux pings du master, ce qui entraîne la chute du nœud au niveau du cluster.



Trop de GC? Faisons une recherche sur le Web pour obtenir des conseils sur le réglage du GC!

Lorsque le GC prend beaucoup de temps de calcul, l’ingénieur en nous est tenté de trouver ce paramètre magique de JVM qui guérira tout. Le plus souvent, c’est le mauvais endroit pour chercher une solution, car un GC lourd est simplement un symptôme du fait qu’Elasticsearch a besoin de plus de ressources qu’elle n’en a.

Bien que l’augmentation de la taille du tas soit une solution évidente, ce n’est pas toujours possible. La même chose s’applique à l’ajout de plusieurs nœuds de données. Au lieu de cela, vous pouvez utiliser plusieurs astuces pour réduire l’utilisation de votre tas:

  • Réduisez la taille de la mémoire tampon d’index décrite à la section 10.2.
  • Réduisez le cache de filtre et / ou le cache de requête de fragment.
  • Réduisez la valeur size des recherches et des agrégations (pour les agrégations, vous devez également vous occuper de shard_size).
  • Si vous devez vous débrouiller avec de grandes tailles, vous pouvez ajouter des nœuds qui ne sont ni maîtres  et ni de type data pour agir en tant que clients. Ils prendront la peine d’agréger les résultats de recherches et d’agrégations par partition.$

Enfin, Elasticsearch utilise un autre type de cache pour résoudre le problème de la récupération de place par Java. Il existe un espace jeune génération où de nouveaux objets sont alloués. Ils sont «promus» à l’ancienne génération si le système en a besoin assez longtemps ou si de nombreux nouveaux objets sont alloués et que l’espace réservé aux jeunes se remplit. Ce dernier problème apparaît surtout avec les agrégations, qui doivent parcourir de grands ensembles de documents et créer de nombreux objets pouvant être réutilisés lors de la prochaine agrégation.

Normalement, vous voulez que ces objets potentiellement réutilisables utilisés par les agrégations soient promus vers l’ancienne génération au lieu de certains objets temporaires aléatoires qui se trouvent juste là lorsque la jeune génération se remplit. Pour ce faire, Elasticsearch implémente un PageCacheRecycler dans lequel les grands tableaux utilisés par les agrégations ne peuvent pas être récupérés par le garbage collector. Ce cache de page par défaut représente 10% du segment de mémoire total et, dans certains cas, il peut en être trop (par exemple, vous disposez de 30 Go de segment de mémoire, ce qui fait carrément du cache 3 Go). Vous pouvez contrôler la taille de ce cache à partir de elasticsearch.yml via cache.recycler.page.limit.heap.

Néanmoins, il est parfois nécessaire d’ajuster les paramètres de votre JVM (bien que les valeurs par défaut soient très bonnes), par exemple lorsque vous avez presque assez de mémoire mais que le cluster a des problèmes lorsque des pauses GC rares mais longues s’installent. Il existe des options  permettant au GC d’intervenir plus souvent mais d’immobiliser moins longtemps votre système, en échangeant efficacement le débit global pour une meilleure latence:

  • Augmentez l’espace de survie (inférieur -XX: SurvivorRatio) ou l’ensemble de la jeune génération (inférieur -XX: NewRatio) par rapport au tas total. Vous pouvez vérifier si cela est nécessaire en surveillant différentes générations. Plus d’espace devrait laisser plus de temps au jeune GC pour nettoyer les objets éphémères avant qu’ils ne soient promus à l’ancienne génération, où un autre GC immobiliserait votre système plus longtemps. Mais si ces espaces sont trop grands, le GC travaillera trop dur et deviendra inefficace, car les objets ayant une durée de vie plus longue doivent être copiés entre les deux espaces survivants.
  • Utilisez le CPG G1 (-XX: + UseG1GC), qui allouera dynamiquement de l’espace pour différentes générations et est optimisé pour les cas d’utilisation à grande mémoire et à faible latence. Il n’est plus utilisé par défaut à partir de la version 1.5 car il reste encore quelques bogues sur les machines 32 bits. Assurez-vous donc de le tester avant d’utiliser G1 en production.


Pouvez-vous avoir un tas trop gros?

Il était peut-être évident qu’un tas trop petit est mauvais, mais avoir un tas trop gros n’est pas bon non plus. Une taille de tas supérieure à 32 Go créera automatiquement des pointeurs non compressés et une perte de mémoire. Combien de mémoire gaspillée? Cela dépend du cas d’utilisation: cela peut varier d’aussi peu que 1 Go pour 32 Go si vous faites principalement des agrégations (qui utilisent de grands tableaux contenant peu de pointeurs) à environ 10 Go si vous utilisez beaucoup de caches de filtres (qui ont beaucoup de petites entrées avec beaucoup de pointeurs). Si vous avez réellement besoin de plus de 32 Go de mémoire, il est parfois préférable d’exécuter deux nœuds ou plus sur le même ordinateur, chacun avec moins de 32 Go de mémoire, et de diviser les données entre eux par le biais du sharding.



Remarque
Si vous vous retrouvez avec plusieurs nœuds Elasticsearch sur la même machine physique, vous devez vous assurer que deux répliques du même fragment ne sont pas allouées sur la même machine physique sous différents nœuds Elasticsearch. Sinon, si une machine physique tombe en panne, vous perdrez deux copies de cette partition. Pour éviter cela, vous pouvez utiliser l’allocation de fragments, comme décrit au chapitre 11.



Au-dessous de 32 Go, trop de tas n’est toujours pas idéal (en fait, vous perdez déjà des pointeurs compressés à exactement 32 Go, il est donc préférable de s’en tenir à un maximum de 31 Go). La mémoire RAM de vos serveurs qui n’est pas occupée par la JVM est généralement utilisée par le système d’exploitation pour mettre en cache les index stockés sur le disque. Ceci est particulièrement important si vous disposez d’un stockage magnétique ou réseau, car l’extraction de données à partir du disque lors de l’exécution d’une requête retardera sa réponse. Même avec les disques SSD rapides, vous obtiendrez les meilleures performances si la quantité de données que vous avez besoin de stocker sur un nœud peut tenir dans ses caches de système d’exploitation.

Jusqu’à présent, nous avons constaté qu’un tas trop petit était mauvais à cause de problèmes de GC et de mémoire insuffisante, et un trop gros l’était égalment, parce qu’il réduisait le nombre de caches d’OS. Quelle est la bonne taille de tas, alors?

Taille de tas idéale: suivez la règle de la moitié

Sans rien connaître de l’utilisation réelle du segment de mémoire dans votre cas d’utilisation, la règle empirique consiste à allouer la moitié de la RAM du nœud à Elasticsearch, sans dépasser 32 Go. Cette règle de la «moitié» donne souvent un bon équilibre entre la taille du tas et les caches de système d’exploitation.

Si vous pouvez surveiller l’utilisation réelle du segment de mémoire (et nous allons vous montrer comment procéder au chapitre 11), une bonne taille de segment de mémoire est juste assez grande pour prendre en charge l’usage normal plus les pics auxquels vous pourriez vous attendre. Des pics d’utilisation de la mémoire peuvent survenir, par exemple, si quelqu’un décide d’exécuter une agrégation de termes de taille 0 sur un champ analysé comportant de nombreux termes uniques. Cela forcera Elasticsearch à charger tous les termes en mémoire afin de les compter. Si vous ne savez pas à quoi vous attendre, la règle de base est de nouveau la moitié: définissez une taille de segment mémoire supérieure de 50% à votre utilisation habituelle.

Pour les caches de système d’exploitation, vous dépendez principalement de la RAM de vos serveurs. Cela dit, vous pouvez concevoir vos index de la manière la mieux adaptée à la mise en cache de votre système d’exploitation. Par exemple, si vous indexez les logs d’application, vous pouvez vous attendre à ce que la plupart des opérations d’indexation et de recherche impliquent des données récentes. Avec les index basés sur le temps, le dernier index est plus susceptible de tenir dans le cache du système d’exploitation que l’ensemble de données, ce qui accélère la plupart des opérations. Les recherches sur des données plus anciennes doivent souvent se trouver sur le disque, mais les utilisateurs s’attendent généralement davantage à tolérer des temps de réponse lents sur ces recherches rares s’étendant sur de plus longues périodes. En général, si vous pouvez placer des données «hot» dans le même ensemble d’index ou de fragments en utilisant des index temporels, des index utilisateurs ou des routages, vous ferez un meilleur usage des caches de système d’exploitation.

Tous les caches dont nous avons discuté jusqu’à présent – caches de filtre, caches de requête et caches de système d’exploitation – sont généralement générés lors de la première exécution d’une requête. Le chargement des caches ralentit la première requête et le ralentissement augmente avec la quantité de données et la complexité de la requête. Si le ralentissement devient problématique, vous pouvez préchauffer les caches à l’avance en utilisant des warmers d’index, comme vous le verrez ensuite.

10.3.4. Garder les caches prêts avec des warmers.

Un warmer vous permet de définir tout type de requête de recherche: il peut contenir des requêtes, des filtres, des critères de tri et des agrégations. Une fois défini, le warmer obligera Elasticsearch à exécuter la requête à chaque opération de rafraîchissement. Cela ralentira l’actualisation, mais les requêtes de l’utilisateur seront toujours exécutées sur des caches «chauds».

Les warmers sont utiles lorsque les premières requêtes sont trop lentes et il est préférable que l’opération d’actualisation gère ce problème plutôt que l’utilisateur. Si notre exemple de site get-together comportait des millions d’événements et que les performances de recherche étaient importantes, des warmers seraient utiles. Les actualisations plus lentes ne devraient pas trop vous préoccuper, car vous vous attendez à ce que les groupes et les événements soient recherchés plus souvent que modifiés.

Pour définir un warmer sur un index existant, vous devez envoyer une requête PUT à l’URI de l’index, avec _warmer comme type et le nom du warmer choisi comme identifiant(ID), comme indiqué dans la figure 10.7. Vous pouvez avoir autant de warmer que vous le souhaitez, mais gardez à l’esprit que plus vous les utilisez, plus vos rafraîchissements seront lents. En règle générale, vous utiliserez les warmers pour quelques requêtes populaires. Par exemple, dans la figure suivante, vous placerez deux warmers: un pour les événements à venir et un pour les tags les plus populaires au niveau des groupes. Il s’agit d’une agregation terms

Plus tard, vous pouvez obtenir la liste des warmers pour un index en effectuant une requête GET sur le type _warmer:

Vous pouvez également supprimer des warmers en envoyant une requête DELETE à l’URI de ce dernier:

Si vous utilisez plusieurs index, il est judicieux d’enregistrer des warmers lors de la création d’index. Pour ce faire, définissez-les sous l’attribut warmers de la même manière que pour les mappings et les paramètres, comme indiqué dans la figure suivante.



Si de nouveaux index sont créés automatiquement, ce qui peut arriver si vous utilisez des index temporels, vous pouvez définir des warmers dans un modèle d’index qui sera automatiquement appliqué aux index nouvellement créés. Nous parlerons davantage de modèles d’index dans le chapitre 11, qui explique comment administrer votre cluster Elasticsearch.



Jusqu’à présent, nous avons parlé de solutions générales: comment garder les caches au chaud et efficaces pour effectuer vos recherches rapidement, comment grouper les demandes pour réduire la latence du réseau et comment configurer l’actualisation, le flush et le stockage des segments afin de rendre votre indexation et recherche rapide. Tout cela devrait également réduire la charge de votre cluster.

Nous aborderons ensuite les meilleures pratiques plus restrictives applicables à des cas d’utilisation spécifiques, telles que la rapidité de vos scripts ou la pagination en profondeur de manière efficace.

10.4. AUTRES COMPROMIS DE PERFORMANCES

Dans les sections précédentes, vous avez peut-être remarqué que pour effectuer une opération rapidement, vous devez le payer par quelque chose. Par exemple, si vous accélérez l’indexation en actualisant moins souvent, vous payez avec des recherches qui ne peuvent pas «voir» les données récemment indexées. Dans cette section, nous allons continuer à examiner de tels compromis, en particulier ceux qui se produisent dans des cas d’utilisation plus spécifiques, en répondant à des questions sur les sujets suivants:

  • Correspondances inexactes – Devriez-vous effectuer des recherches plus rapidement en utilisant des ngrammes et des shingles au moment de l’indexation? Ou est-il préférable d’utiliser des requêtes floues(fuzzy query) et génériques(wildcard)?
  • Scripts – Devriez-vous échanger un peu de flexibilité en calculant autant que possible au moment de l’indexation? Sinon, comment pouvez-vous en tirer plus de performances?
  • Recherche distribuée: devez-vous augmenter les allers-retours sur le réseau pour obtenir des résultats plus précis?
  • Deep paging: vaut-il la peine de faire un compromis au niveau de la mémoire pour obtenir la page 100 plus rapidement?

À la fin de ce chapitre, nous aurons répondu à toutes ces questions et à d’autres qui suivront. Commençons par des correspondances inexactes.

10.4.1. Gros index ou recherches coûteuses

Rappelez-vous au chapitre 4 que pour obtenir des correspondances inexactes, par exemple pour tolérer les fautes de frappe, vous pouvez utiliser un certain nombre de requêtes:

  • La query fluzzy– Cette requête met en correspondance les termes à une certaine distance d’édition de l’original. Par exemple, omettre ou ajouter un caractère supplémentaire créerait une distance de 1.
  • La query prefixe ou filtre – Les resultats sont les termes commençant par la séquence que vous avez fournie.
  • Wildcards— Celles-ci vous permettent d’utiliser ? et * pour substituer un ou plusieurs caractères. Par exemple, « e * search » correspond à « elasticsearch ».

Ces requêtes offrent beaucoup de flexibilité, mais elles sont également plus coûteuses que les requêtes simples, telles que les requêtes de termes. Pour une correspondance exacte, Elasticsearch doit rechercher un seul terme dans le dictionnaire de termes, alors que les requêtes floues, préfixes et caractères génériques doivent rechercher tous les termes correspondant au modèle donné.

Il existe également une autre solution pour tolérer les fautes de frappe et autres correspondances inexactes: les ngrams. Rappelez-vous du chapitre 5 que les ngrams génèrent des jetons à partir de chaque partie du mot. Si vous les utilisez à la fois dans l’index et au moment de la requête, vous obtiendrez des fonctionnalités similaires à celles d’une requête floue(query fluzzy), comme le montre la figure 10.9.

Quelle approche est la meilleure pour la performance? Comme pour tout ce qui précède dans ce chapitre, il existe un compromis et vous devez choisir le lieu où vous souhaitez payer le prix:

  • Les requêtes floues (fluzzy) ralentissent vos recherches, mais votre index est identique à celui des correspondances exactes.
  • Les Ngrams, en revanche, augmentent la taille de votre index. Selon la taille des ngrammes et des termes, la taille de l’index avec les ngrammes peut augmenter plusieurs fois. De plus, si vous souhaitez modifier les paramètres ngram, vous devez réindexer toutes les données afin de réduire la flexibilité, mais les recherches sont généralement plus rapides avec ngrams.

La méthode ngram est généralement préférable lorsque la latence des requêtes est importante ou lorsque vous devez prendre en charge de nombreuses requêtes simultanées. Par conséquent, chacune d’elles nécessite moins de ressources processeur. Les ngrams font en sorte que les index soient plus grands, mais ils doivent rester dans les caches du système d’exploitation ou vous avez besoin de disques rapides, sinon les performances se dégraderont parce que votre index est trop gros.

L’approche floue, en revanche, est préférable lorsque vous avez besoin d’un débit d’indexation, lorsque la taille de l’index pose un problème ou que vous avez des disques lents. Les requêtes floues sont également utiles si vous devez les modifier souvent, par exemple en ajustant la distance d’édition, car vous pouvez effectuer ces modifications sans réindexer toutes les données.

Query prefixe et des ngrammes de bord

Pour des correspondances inexactes, vous supposez souvent que le début est juste. Par exemple, une recherche sur «élastique» peut rechercher «elasticsearch». Comme les requêtes floues, les requêtes préfixes sont plus coûteuses que les requêtes de termes ordinaires car il y a plus de termes à parcourir.

L’alternative pourrait consister à utiliser des ngrammes de bord, présentés au chapitre 5. La figure 10.10 montre côte à côte les query préfixe et les ngrammes de bord.

Comme pour les requêtes floues et les ngrams, le compromis est entre la flexibilité et la taille de l’index, qui sont meilleures dans l’approche préfixe, tandis que la latence des requêtes et l’utilisation du processeur, sont meilleures pour les ngrams de bords.

Wildcards

Une query wildcard dans laquelle vous placez toujours un caractère générique à la fin, tel qu’elastic*, est équivalente en termes de fonctionnalité à une query prefixe. Dans ce cas, vous avez la même alternative en utilisant des ngrammes de bord.

Si le caractère générique est au milieu, comme avec e * search, il n’existe pas de véritable équivalent au moment de l’indexation. Vous pouvez toujours utiliser ngrams pour faire correspondre les lettres fournies e et search, mais si vous n’avez aucun contrôle sur la manière dont les caractères génériques sont utilisés, la requête de caractère générique est votre seul choix.

Si le caractère générique est toujours au début, la query wildcard est généralement plus coûteuse que les caractères génériques de fin, car il n’ya pas de préfixe indiquant dans quelle partie du dictionnaire de termes rechercher les termes correspondants. Dans ce cas, l’alternative peut consister à utiliser le filtre de jeton inversé(inverse) en combinaison avec des ngrammes de bord, comme vous l’avez vu au chapitre 5. Cette alternative est illustrée à la figure 10.11.

Lorsque vous devez comptabiliser des mots côte à côte, vous pouvez utiliser la query match avec le type défini à phrase, comme vous l’avez vu au chapitre 4. Les requêtes de type phrases sont plus lentes car elles doivent prendre en compte non seulement les termes, mais également les mots clés. leurs positions dans les documents.



Remarque

Les positions sont activées par défaut pour tous les champs analysés car index_options est défini sur positions. Si vous n’utilisez pas de query de type phrase, mais uniquement de requêtes de termes, vous pouvez désactiver l’indexation des positions en définissant index_options sur freqs. Si vous ne vous souciez pas du tout du scoring (par exemple, lorsque vous indexez les logs d’application et que vous triez toujours les résultats par horodatage), vous pouvez également ignorer les fréquences d’indexation en définissant index_options sur docs.



L’alternative aux query de type phrase au moment de l’indexation est d’utiliser des shingles. Comme vous l’avez vu au chapitre 5, le zona est comme les ngrams mais pour les termes au lieu des caractères. Un texte qui serait découpé en:  Introduction, to et Elasticsearch avec une taille de 2 produirait les termes «Introduction to» et «to Elasticsearch».

La fonctionnalité résultante est similaire aux query de type phrase, et les implications en termes de performances sont similaires à celles des situations de ngram décrites précédemment: le zona(shingle) augmente la taille de l’index et ralentit l’indexation en échange de requêtes plus rapides.

Les deux approches ne sont pas exactement équivalentes, de la même manière que les caractères génériques et les ngrams ne sont pas équivalents. Avec les query phrase, par exemple, vous pouvez spécifier un slop, ce qui permet à d’autres mots d’apparaître dans votre expression. Par exemple, un slop de 2 permettrait à une séquence du type « buy the best phone » de correspondre à une requête pour « buy phone ». Cela fonctionne car au moment de la recherche, Elasticsearch est au courant de la position de chaque terme, alors que les shingles sont effectivement des termesuniques.

Le fait que les shingles(zona) soient des termes simples vous permet de les utiliser pour une meilleure correspondance des mots composés. Par exemple, de nombreuses personnes disent encore Elasticsearch la «elastic search», ce qui peut s’avérer délicat. Avec les shingles, vous pouvez résoudre ce problème en utilisant une chaîne vide comme séparateur au lieu de l’espace blanc par défaut, comme illustré à la figure 10.12.

Comme vous avez pu le constater dans notre discussion sur le zona, les ngrammes et les query fluzzy et wildcard, il existe souvent plus d’une façon de rechercher vos documents, mais cela ne signifie pas que ces méthodes sont équivalentes. Le choix du meilleur en termes de performances et de flexibilité dépend beaucoup de votre cas d’utilisation. Ensuite, nous examinerons de plus près les scripts, pour lesquels vous trouverez plus de choses identiques: plusieurs façons d’obtenir le même résultat, mais chaque méthode a ses avantages et ses inconvénients.

10.4.2. Customiser vos scripts ou ne pas les utiliser du tout

Nous avons d’abord introduit les scripts au chapitre 3 car ils peuvent être utilisés pour les mises à jour. Vous les avez revus au chapitre 6, où vous les avez utilisés pour le tri. Au chapitre 7, vous avez de nouveau utilisé des scripts, cette fois pour créer des champs virtuels au moment de la recherche à l’aide de champs de script.

Les scripts offrent beaucoup de flexibilité, mais cette flexibilité a un impact important sur les performances. Les résultats d’un script ne sont jamais mis en cache car Elasticsearch ne sait pas ce qu’il contient. Il peut y avoir quelque chose d’extérieur, comme un nombre aléatoire, qui fera correspondre un document maintenant mais pas pour la prochaine exécution. Elasticsearch n’a pas d’autre choix que d’exécuter le même script pour tous les documents concernés.

Lorsqu’ils sont utilisés, les scripts constituent souvent la partie de vos recherches qui consomme le plus de temps et de ressources CPU. Si vous souhaitez accélérer vos requêtes, un bon point de départ est d’essayer de vous passer des scripts. Si cela n’est pas possible, la règle générale est de vous rapprocher le plus possible du code natif afin d’améliorer leurs performances.

Comment pouvez-vous vous débarrasser des scripts ou les optimiser? La réponse dépend fortement du cas d’utilisation exact, mais nous allons essayer de couvrir les meilleures pratiques ici.

Éviter l’utilisation de scripts

Si vous utilisez des scripts pour générer des champs de script, comme vous l’avez fait au chapitre 7, vous pouvez le faire au moment de l’indexation. Au lieu d’indexer directement les documents et de compter le nombre de membres du groupe dans un script en examinant la longueur du tableau, vous pouvez compter le nombre de membres de votre pipeline d’indexation et l’ajouter à un nouveau champ. Dans la figure 10.13, nous comparons les deux approches.

Comme pour les ngrams, cette approche de calcul au moment de l’index fonctionne bien si la latence des requêtes a une priorité supérieure au débit d’indexation.

Outre le précalcul, la règle générale pour l’optimisation des performances pour les scripts consiste à réutiliser le plus possible les fonctionnalités existantes d’Elasticsearch. Avant d’utiliser des scripts, pouvez-vous remplir les conditions requises avec la query function_score décrite au chapitre 6? Elle offre de nombreuses façons de manipuler le score. Supposons que vous souhaitiez lancer une requête pour les événements «elasticsearch», mais que vous augmenterez le score des manières suivantes, en fonction de ces hypothèses:

  • Les événements qui se déroulent bientôt sont plus pertinents. Vous ferez chuter les scores d’événements de manière exponentielle au fur et à mesure de leur acienneté, jusqu’à 60 jours.
  • Les événements avec plus de participants sont plus populaires et plus pertinents. Vous augmenterez le score de manière linéaire avec le nombre de participants à l’évènement.

Si vous calculez le nombre de participants à l’événement au moment de l’indexation (nommez le champ participees_count), vous pouvez atteindre les deux critères sans utiliser de script:

Scripts natifs

Si vous voulez obtenir les meilleures performances d’un script, écrire des scripts natifs en Java est la meilleure solution. Un tel script natif serait un plugin Elasticsearch, et vous pouvez vous référer à l’annexe B pour un guide complet sur la façon de l’écrire.

L’inconvénient majeur des scripts natifs est qu’ils doivent être stockés sur chaque noeud dans le classpath d’Elasticsearch. Changer un script impliquera donc de le mettre à jour sur tous les nœuds de votre cluster et de le redémarrer. Ce ne sera pas un problème si vous n’avez pas à changer vos requêtes souvent.

Pour exécuter un script natif dans votre requête, définissez lang sur native et le nom du script en tant que contenu du script. Par exemple, si vous avez un plugin avec un script appelé number-OfAttendees qui calcule le nombre de participants à l’événement à la volée, vous pouvez l’utiliser dans une agrégation de statistiques comme celle-ci:

Expressions Lucene

Si vous devez changer souvent de script ou si vous voulez être prêt à le faire sans redémarrer tous vos clusters et si vos scripts fonctionnent avec des champs numériques, les expressions de Lucene seront probablement le meilleur choix.

Avec les expressions Lucene, vous fournissez une expression JavaScript dans le script au moment de la requête et Elasticsearch le compile en code natif, le rendant aussi rapide qu’un script natif. La grande limite est que vous avez accès uniquement aux champs numériques indexés. De plus, si un champ du document est manquant, la valeur 0 est prise en compte, ce qui peut fausser les résultats dans certains cas d’utilisation.

Pour utiliser les expressions Lucene, vous devez définir langue sur expression dans votre script. Par exemple, vous pouvez déjà avoir le nombre de participants, mais vous savez que seulement la moitié d’entre eux sont généralement présents. Vous souhaitez donc calculer des statistiques en fonction de ce nombre:

Si vous devez travailler avec des champs non numériques ou non indexés et que vous souhaitez pouvoir modifier facilement les scripts, vous pouvez utiliser Groovy, la langue par défaut pour les scripts depuis Elasticsearch 1.4. Voyons comment optimiser les scripts Groovy.

Statistiques de termes

Si vous devez ajuster le score, vous pouvez accéder aux statistiques sur les termes au niveau Lucene sans avoir à calculer le score dans le script lui-même, par exemple, si vous souhaitez uniquement calculer le score en fonction du nombre d’apparitions de ce terme dans le document. . Contrairement aux valeurs par défaut d’Elasticsearch, vous ne vous souciez pas de la longueur du champ dans ce document ni du nombre de fois où ce terme apparaît dans d’autres documents. Pour ce faire, vous pouvez avoir un score de script qui spécifie uniquement la fréquence du terme (nombre de fois où le terme apparaît dans le document), comme indiqué dans la figure suivante.

Accéder aux données de terrain

Si vous devez utiliser le contenu réel des champs d’un document dans un script, vous pouvez utiliser le champ _source. Par exemple, vous obtiendrez le champ organisateur en utilisant _source [‘organizer‘].

Au chapitre 3, vous avez vu comment vous pouvez stocker des champs individuels au lieu de côte à côté du champs  _source. Si un champ individuel est stocké, vous pouvez également accéder au contenu stocké. Par exemple, le même champ organizer peut être récupéré avec _fields [‘organizer’].

Le problème avec _source et _fields est qu’il est coûteux d’aller chercher le contenu de ce champ particulier. Heureusement, cette lenteur est précisément ce qui rend les données de champ nécessaires lorsque le tri et les agrégations intégrées d’Elasticsearch sont nécessaires pour accéder au contenu du champ. Comme nous l’avons vu au chapitre 6, les données de terrain sont conçues pour un accès aléatoire. Il est donc préférable de les utiliser également dans vos scripts. Il est souvent beaucoup plus rapide que l’équivalent _source ou _fields, même si les données de champ ne sont pas déjà chargées pour ce champ lors de la première exécution du script (ou si vous utilisez des doc_value, comme expliqué au chapitre 6).

Pour accéder au champ organizer via les données de champ, vous devez vous référer à doc [‘organisateur‘]. Par exemple, vous pouvez renvoyer des groupes dont l’organisateur n’est pas membre. Vous pouvez donc leur demander pourquoi ils ne participent pas à leurs propres groupes:

Il existe un inconvénient à l’utilisation de doc [‘organizer’] au lieu de _source [‘organizer’] ou de l’équivalent de _fields: vous accéderez aux termes, pas au champ d’origine du document. Si un organisateur est « Lee » et que le champ est analysé avec l’analyseur par défaut, vous obtiendrez « Lee » de _source et « lee » de doc. Il y a des compromis partout, mais nous supposons que vous vous êtes habitués à cela à ce stade du chapitre.

Nous examinerons ensuite de manière plus approfondie le fonctionnement des recherches distribuées et comment vous pouvez utiliser des types de recherche pour trouver le bon équilibre entre des résultats précis et des recherches à faible temps de latence.

10.4.3. Échanger des allée retour sur le réseau pour moins de données et un scoring mieux distribué

Au chapitre 2, vous avez vu comment, lorsque vous attaquez un nœud Elasticsearch avec une requête de recherche, ce nœud distribue la requête à tous les fragments impliqués et agrège les réponses de fragments individuels en une réponse finale à renvoyer à l’application.

Examinons de plus près comment cela fonctionne. L’approche naïve consisterait à extraire N documents de tous les fragments impliqués (N étant la valeur de size), les trier sur le nœud ayant reçu la demande HTTP (appelons-le le nœud de coordination), sélectionner les N premiers documents et les renvoyer à l’application. Supposons que vous envoyiez une demande avec la taille par défaut de 10 à un index avec le nombre par défaut de 5 fragments. Cela signifie que le nœud de coordination va récupérer 10 documents entiers dans chaque fragment, les trier et ne renvoyer que les 10 premiers de ces 50 documents. Mais que se passe-t-il s’il y a 10 fragments et 100 résultats? La surcharge réseau liée au transfert des documents et celle liée à la gestion de la mémoire sur le nœud de coordination exploseraient, de la même façon que la spécification de grandes valeurs de shard_size pour les agrégations nuit aux performances.

Pourquoi ne pas renvoyer uniquement les identifiants de ces 50 documents et les métadonnées nécessaires au tri vers le nœud de coordination? Après le tri, le nœud de coordination ne peut extraire que les 10 principaux documents requis des fragments. Cela réduirait la charger globale sur le réseau dans la plupart des cas, mais impliquerait deux allers-retours.

Avec Elasticsearch, les deux options sont disponibles en définissant le paramètre search_type sur la recherche. La mise en œuvre naïve de la récupération de tous les documents impliqués est query_and_fetch, alors que la méthode à deux trajets est appelée query_then_fetch, qui est également la méthode par défaut. Une comparaison des deux est présentée à la figure 10.14.

La valeur par défaut query_then_fetch (affichée à droite de la figure) s’améliore à mesure que vous attaquez plus de fragments, que vous demandez plus de documents via le paramètre size et que les documents deviennent plus volumineux, car ils transfèrent beaucoup moins de données sur le réseau. query_and_fetch n’est plus rapide que lorsque vous attaquez un fragment – c’est pourquoi il est utilisé implicitement lorsque vous recherchez un seul fragment, lorsque vous utilisez le routage ou que vous ne recuperez que des décomptes (nous en discuterons plus tard). À l’heure actuelle, vous pouvez spécifier explicitement query_and_fetch, mais dans la version 2.0, il ne sera utilisé en interne que pour ces cas d’utilisation spécifiques.

Scoring distribuée

Par défaut, les scores sont calculés par fragment, ce qui peut entraîner des inexactitudes. Par exemple, si vous recherchez un terme, l’un des facteurs est la fréquence de document (DF), qui indique combien de fois le terme recherché apparaît dans tous les documents. Le terme «tous les documents» est en fait«tous les documents de ce fragment». Si le FD d’un terme est très différent d’un fragment à l’autre, le scoring peut ne pas refléter la réalité. Vous le voyez à la figure 10.15, où le document 2 obtient un score plus élevé que le document 1, même si le document 1 contient plus d’occurrences de «elasticsearch», car il y a moins de documents avec ce terme dans son fragment.

Vous pouvez imaginer qu’avec un nombre suffisant de documents, les valeurs DF s’équilibreraient naturellement entre les fragments, et le comportement par défaut fonctionnerait parfaitement. Mais si la précision du score est une priorité ou si DF n’est pas équilibré pour votre cas d’utilisation (par exemple, si vous utilisez un routage personnalisé), vous aurez besoin d’une approche différente.

Cette approche pourrait consister à changer le type de recherche de query_then_fetch à dfs_query_then_fetch. La partie dfs indiquera au nœud de coordination de faire un appel supplémentaire aux fragments afin de rassembler les fréquences de document des termes recherchés. Les fréquences agrégées seront utilisées pour calculer le score, comme vous pouvez le voir à la figure 10.16, en classant correctement les doc 1 et 2.

Vous avez probablement déjà compris que les requêtes DFS sont plus lentes à cause de l’appel réseau supplémentaire. Assurez-vous donc d’obtenir de meilleurs scores avant de changer. Si vous avez un réseau à faible temps de latence, cette surcharge peut être négligeable. Si, en revanche, votre réseau n’est pas assez rapide ou si vous avez une grande simultanéité de requêtes, vous risquez de constater une surcharge importante.

Renvoyer seulement le decompte(count)

Mais que se passe-t-il si vous ne vous souciez pas du tout du scoring et que vous n’avez pas non plus besoin du contenu du document? Par exemple, vous n’avez besoin que du nombre de documents ou des agrégations. Dans ce cas, le type de recherche recommandé est count. Elle demande aux fragments impliqués uniquement le nombre de documents correspondants et additionne ces nombres.



Dans la version 2.0, l’ajout de size = 0 à une requête suivra automatiquement la même logique que search_type = count, et search_type = count sera devenu obsolète. Plus de détails peuvent être trouvés ici: https://github.com/elastic/elasticsearch/pull/9296.



10.4.4. Échange de mémoire pour une meilleure pagination en profondeur

Au chapitre 4, vous avez appris comment utiliserez les attributs size et form pour paginer vos résultats. Par exemple, pour rechercher «elasticsearch» dans les événements de rencontre et obtenir la cinquième page de 100 résultats, vous devez exécuter une requête comme celle-ci:

Cela va effectivement chercher les 500 meilleurs résultats, les trier et ne renvoyer que les 100 derniers. Vous pouvez imaginer à quel point cela devient inefficace à mesure que vous allez plus loin dans les pages. Par exemple, si vous modifiez le mapping et souhaitez réindexer toutes les données existantes dans un nouvel index, il se peut que vous ne disposiez pas de suffisamment de mémoire pour trier tous les résultats afin de renvoyer les dernières pages.

Pour ce type de scénario, vous pouvez utiliser le type de recherche scan, comme vous le ferez dans la figure 10.10, pour parcourir tous les groupes de rencontre. La réponse initiale ne renvoie que l’ID de défilement, qui identifie de manière unique cette requête et mémorise les pages déjà renvoyées. Pour commencer à récupérer les résultats, envoyez une demande avec cet ID de défilement. Répétez la même demande pour extraire la page suivante jusqu’à ce que vous ayez suffisamment de données ou qu’il n’y ait plus d’occurrences à renvoyer, auquel cas le tableau des occurrences est vide.

Comme pour les autres recherches, les recherches de type scan acceptent un paramètre size pour contrôler la taille de la page. Mais cette fois, la taille de la page est calculée par fragment. La taille renvoyée correspond donc à la taille multipliée par le nombre de fragments. Le délai indiqué dans le paramètre scroll de chaque demande est renouvelé chaque fois que vous obtenez une nouvelle page. C’est pourquoi vous pouvez appliquer un délai différent à chaque nouvelle requête.



Il peut être tentant d’avoir de longs délais afin de vous assurer que le scroll(défilement) n’expire pas tant que vous le traitez. Le problème est que si un scroll est actif et non utilisé, il gaspille des ressources et prend un peu de mémoire JVM pour se souvenir de la page actuelle et de l’espace disque occupé par les segments Lucene qui ne peuvent pas être supprimés par la fusion tant que le défilement n’est pas terminé ou expiré.



Le type de recherche scan renvoie toujours les résultats dans l’ordre dans lequel il les rencontre dans l’index, quel que soit le critère de tri. Si vous avez besoin à la fois de pagination approfondie et de tri, vous pouvez ajouter un paramètre scroll à une requête de recherche classique. L’envoi d’une requête GET à l’ID de défilement obtiendra la page de résultats suivante. Cette fois, size fonctionne avec précision, quel que soit le nombre de fragments. Vous obtenez également la première page de résultats avec la première requête, comme vous obtenez avec les recherches régulières:

Du point de vue des performances, ajouter du défilement à une recherche standard coûte plus cher que d’utiliser le type de recherche scan car il reste plus d’informations à conserver en mémoire lorsque les résultats sont triés. Cela étant dit, la pagination en profondeur est beaucoup plus efficace que celle par défaut, car Elasticsearch n’a pas à trier toutes les pages précédentes pour renvoyer la page en cours.

Le défilement n’est utile que lorsque vous savez à l’avance que vous souhaitez effectuer une pagination en profondeur; Ce n’est pas recommandé lorsque vous n’avez besoin que de quelques pages de résultats. Comme observé dans tout ce chapitre, vous payez un prix pour chaque amélioration de performances. Dans le cas du défilement, ce prix consiste à conserver en mémoire les informations relatives à la recherche en cours jusqu’à l’expiration du défilement ou jusqu’à ce que vous n’ayez plus de résultats.

10.5 RÉSUMÉ

Dans ce chapitre, nous avons examiné plusieurs optimisations que vous pouvez effectuer pour augmenter la capacité et la réactivité de votre cluster:

  • Utilisez l’API Bulk pour combiner plusieurs opérations d’index, de création, de mise à jour ou de suppression dans la même requête.
  • Pour combiner plusieurs requêtes de type get ou de recherche, vous pouvez utiliser les API multiget ou multisearch, respectivement.
  • Une opération de flush valide des segments Lucene en mémoire sur le disque lorsque la taille de la mémoire tampon d’index est saturée, que les logs des transactions sont devenus trop volumineux ou que le temps écoulé depuis le dernier flush est trop long.
  • Une actualisation rend les nouveaux segments, vidés(flushed) ou non, disponibles pour la recherche. Lors d’une indexation intensive, il est préférable de réduire le taux de rafraîchissement ou de le désactiver complètement.
  • La politique de fusion peut être ajustée pour plus ou moins de segments. Moins de segments rendent les recherches plus rapides, mais les fusions prennent plus de temps processeur. Plus de segments accélèrent l’indexation en réduisant le temps de fusion, mais les recherches seront plus lentes.
  • Une opération d’optimisation force la fusion, ce qui fonctionne bien pour les index statiques qui font l’objet de nombreuses recherches.
  • Le Store throttling peut limiter les performances d’indexation en retardant les fusions. Augmentez ou supprimez les limites si vous avez des E/S rapides.
  • Combinez des filtres qui utilisent des bitsets dans un filtre bool et ceux qui ne l’utilisent pas dans des filtres and/or/not.
  • Mettez le count et les agrégations dans le cache de requête de fragment si vous avez des index statiques.
  • Surveillez le segment de mémoire de la machine virtuelle Java et laissez suffisamment d’espace libre pour éviter les erreurs de récupération de memoire(garbage collection) ou les erreurs de mémoire(out-of-memory error), mais laissez également de la RAM pour les caches de système d’exploitation.
  • Utilisez des warmers d’index si la première requête est trop lente et qu’une indexation plus lente ne vous dérange pas.
  • Si vous avez de la place pour de plus grands index, l’utilisation de ngrams et de zona(shringles) au lieu de query fluzzy, wildcard ou phrase devrait accélérer vos recherches.
  • Vous pouvez souvent éviter d’utiliser des scripts en créant de nouveaux champs avec les données nécessaires dans vos documents avant de les indexer.
  • Essayez d’utiliser des expressions Lucene, des statistiques sur les termes et des données de champ dans vos scripts chaque fois que c’est possible.
  • Si vos scripts n’ont pas besoin de changer souvent, consultez l’annexe B pour apprendre à écrire un script natif dans un plugin Elasticsearch.
  • Utilisez dfs_query_then_fetch si vous n’avez pas de fréquences de documents équilibrées entre les fragments.
  • Utilisez le type de recherche count si vous n’avez pas besoin de résultats et le type de recherche scan si vous en avez besoin de plusieurs.