Optimisation des requêtes Elasticsearch lentes : bonnes pratiques pour le réglage des performances

Diagnostiquez et améliorez les requêtes Elasticsearch lentes grâce à une meilleure forme de requête, une pagination optimisée, une mise en cache, des mappings adaptés et l'API Profile.

Optimisation des requêtes Elasticsearch lentes : bonnes pratiques pour le réglage des performances

Les requêtes Elasticsearch lentes proviennent généralement de l'une des quatre causes suivantes : la requête demande trop de données, le mapping rend la requête coûteuse, le cluster manque de ressources, ou l'application répète des recherches coûteuses qui devraient être mises en cache ou repensées. La solution dépend de la cause identifiée.

Avant de tout réécrire, capturez une requête lente réelle avec son index, ses filtres, son tri, ses agrégations, la profondeur de page, la taille de la réponse et le temps d'exécution. Une agrégation de tableau de bord, une requête d'autocomplétion et un travail d'exportation sollicitent Elasticsearch de manière différente.

Comprendre les goulots d'étranglement des performances des requêtes

Avant de plonger dans les solutions, il est utile de comprendre les raisons courantes des requêtes Elasticsearch lentes. Celles-ci incluent souvent :

  • Requêtes complexes : Requêtes avec plusieurs clauses bool, requêtes imbriquées, ou opérations coûteuses comme wildcard ou regexp sur de grands ensembles de données.
  • Récupération inefficace des données : Récupération inutile de _source, ou récupération d'un grand nombre de documents pour la pagination.
  • Contraintes de ressources : CPU, mémoire ou E/S disque insuffisants sur les nœuds de données.
  • Mappings sous-optimaux : Utilisation de types de données incorrects ou non-exploitation de doc_values pour les agrégations.
  • Déséquilibre ou surcharge des shards : Trop de shards, pas assez de shards, ou répartition inégale des shards/données.
  • Absence de cache ou mauvaise adaptation du cache : Répétition de recherches coûteuses sans utiliser la mise en cache des requêtes, le contexte de filtre, ou la mise en cache au niveau de l'application lorsque cela est approprié.

Optimisation de la structure des requêtes

La façon dont vous construisez vos requêtes a un impact profond sur leurs performances. De petits changements peuvent entraîner des améliorations significatives.

1. Récupérer uniquement les champs nécessaires (filtrage _source et stored_fields)

Par défaut, Elasticsearch renvoie l'intégralité du champ _source pour chaque document correspondant. Si vos documents sont volumineux et que l'interface utilisateur n'a besoin que d'un titre, d'un ID et d'un horodatage, la récupération du document complet gaspille la bande passante réseau et le temps d'analyse.

  • Filtrage _source : Utilisez le paramètre _source pour spécifier un tableau de champs à inclure ou exclure.

    GET /my-index/_search
    {
      "_source": ["title", "author", "publish_date"],
      "query": {
        "match": {
          "content": "Elasticsearch performance"
        }
      }
    }
    
  • stored_fields : Si vous avez explicitement stocké des champs spécifiques dans votre mapping ("store": true), vous pouvez les récupérer avec stored_fields. La plupart des déploiements ne stockent pas beaucoup de champs de cette façon, donc le filtrage _source est la solution la plus courante.

    GET /my-index/_search
    {
      "stored_fields": ["title", "author"],
      "query": {
        "match": {
          "content": "Elasticsearch performance"
        }
      }
    }
    

2. Privilégier les types de requêtes efficaces

Certains types de requêtes sont intrinsèquement plus gourmands en ressources que d'autres.

  • Éviter les wildcards en tête et les regexp larges : Les requêtes wildcard et regexp peuvent être coûteuses, surtout avec des wildcards en tête comme *test. Les requêtes prefix sont généralement plus gérables que les recherches avec wildcard en tête, mais elles nécessitent toujours des mappings sensés et une entrée limitée.

    # Inefficace - éviter le wildcard en tête
    {
      "query": {
        "wildcard": {
          "name.keyword": {
            "value": "*search"
          }
        }
      }
    }
    
    # Mieux - si vous connaissez le préfixe
    {
      "query": {
        "prefix": {
          "name.keyword": {
            "value": "Elastic"
          }
        }
      }
    }
    
  • Utiliser match_phrase pour l'intention de phrase : Si l'utilisateur recherche une phrase exacte, match_phrase exprime mieux cette intention que plusieurs clauses match non liées. Ce n'est pas toujours moins cher, mais cela évite de renvoyer des documents qui contiennent les mots éloignés les uns des autres.

  • Contexte de filtre pour les conditions oui/non : Lorsque vous vous souciez uniquement de savoir si un document correspond à une condition, placez cette condition dans le contexte filter ou utilisez constant_score. Cela évite un travail de scoring inutile et est plus favorable au cache.

    GET /my-index/_search
    {
      "query": {
        "constant_score": {
          "filter": {
            "term": {
              "status": "active"
            }
          }
        }
      }
    }
    

3. Optimiser les requêtes booléennes

  • Utiliser des filtres pour les contraintes structurées : Placez les ID de tenant, les valeurs de statut, les plages de dates et les balises exactes dans filter, pas dans must, sauf si elles nécessitent un scoring. Elasticsearch peut réorganiser et optimiser les clauses en interne, donc ne vous fiez pas à l'ordre JSON comme principal outil de performance.
  • Utiliser minimum_should_match intentionnellement : Cela peut améliorer la pertinence et réduire les correspondances larges, mais le définir trop haut peut masquer des résultats valides.

4. Pagination efficace (search_after et scroll)

La pagination traditionnelle from/size devient très inefficace pour les pages profondes (par exemple, from: 10000, size: 10). Elasticsearch doit récupérer et trier tous les documents jusqu'à from + size sur chaque shard, puis jeter les documents from.

  • search_after : Pour la pagination profonde en temps réel, search_after est recommandé. Il utilise l'ordre de tri du dernier document de la page précédente pour trouver l'ensemble de résultats suivant, similaire aux curseurs dans les bases de données traditionnelles. Il est sans état et s'adapte mieux.

    # Première requête
    GET /my-index/_search
    {
      "size": 10,
      "query": {"match_all": {}},
      "sort": [{"timestamp": "asc"}, {"_id": "asc"}]
    }
    
    # Requête suivante utilisant les valeurs de tri du dernier document de la première requête
    GET /my-index/_search
    {
      "size": 10,
      "query": {"match_all": {}},
      "search_after": [1678886400000, "doc_id_XYZ"],
      "sort": [{"timestamp": "asc"}, {"_id": "asc"}]
    }
    
  • API scroll : Pour la récupération en masse de grands ensembles de données, comme le réindexage ou les exportations, scroll peut encore être utile. Pour les versions plus récentes d'Elasticsearch et les analyses complètes d'index de longue durée, envisagez également point-in-time plus search_after. Scroll ne convient pas à la pagination en temps réel destinée aux utilisateurs.

5. Optimiser les agrégations

Les agrégations peuvent être gourmandes en ressources, en particulier sur les champs à haute cardinalité.

  • Pré-calcul des agrégations : Envisagez d'exécuter des agrégations complexes non temps réel lors de l'indexation ou selon un calendrier pour pré-calculer les résultats et les stocker dans un index séparé.
  • doc_values : Assurez-vous que les champs utilisés dans les agrégations ont doc_values activé (ce qui est la valeur par défaut pour la plupart des champs non textuels). Cela permet à Elasticsearch de charger les données pour les agrégations efficacement sans charger _source.
  • eager_global_ordinals : Pour les champs keyword fréquemment utilisés dans les agrégations terms, définir eager_global_ordinals: true dans le mapping peut améliorer les performances en pré-construisant les ordinaux globaux. Cela entraîne un coût au moment de l'actualisation de l'index mais accélère les agrégations au moment de la requête.

Exploiter les techniques de mise en cache

Elasticsearch offre plusieurs niveaux de mise en cache qui peuvent accélérer considérablement les requêtes répétées.

1. Cache de requêtes de nœud

  • Mécanisme : Met en cache les résultats des clauses de filtre dans les requêtes bool qui sont utilisées fréquemment. C'est un cache en mémoire au niveau du nœud.
  • Efficacité : Plus efficace pour les clauses de filtre répétées. Ne comptez pas dessus pour chaque requête ; Elasticsearch décide ce qui vaut la peine d'être mis en cache.
  • Configuration : Activé par défaut. Vous pouvez contrôler sa taille avec indices.queries.cache.size (10% du tas par défaut).

2. Cache de requêtes de shard

  • Mécanisme : Met en cache les résultats de recherche au niveau du shard, le plus souvent pour les requêtes lourdes en agrégations avec size=0. C'est un bon choix pour les requêtes de tableau de bord répétées sur des données qui ne changent pas à chaque seconde.

  • Efficacité : Excellent pour les requêtes de tableau de bord ou les applications analytiques où la même requête (y compris les agrégations) est exécutée de manière répétée avec des paramètres identiques.

  • Comment l'utiliser : Activez-le explicitement dans votre requête en utilisant "request_cache": true.

    GET /my-index/_search?request_cache=true
    {
      "size": 0,
      "query": {
        "bool": {
          "filter": [
            {"term": {"status.keyword": "active"}},
            {"range": {"timestamp": {"gte": "now-1h"}}}
          ]
        }
      },
      "aggs": {
        "messages_per_minute": {
          "date_histogram": {
            "field": "timestamp",
            "fixed_interval": "1m"
          }
        }
      }
    }
    
  • Mises en garde : Le cache est invalidé chaque fois qu'un shard est actualisé (nouveaux documents indexés ou existants mis à jour). Utile uniquement pour les requêtes qui renvoient des résultats identiques fréquemment.

3. Cache du système de fichiers (niveau OS)

  • Mécanisme : Le cache du système de fichiers du système d'exploitation joue un rôle critique. Elasticsearch s'appuie fortement sur lui pour mettre en cache les segments d'index fréquemment consultés.
  • Efficacité : Crucial pour les performances des requêtes. Si les segments d'index sont dans la RAM, les E/S disque sont complètement contournées, ce qui conduit à une exécution beaucoup plus rapide des requêtes.
  • Bonne pratique : Laissez une quantité substantielle de RAM pour le cache du système de fichiers. Un point de départ courant est de maintenir le tas JVM autour de la moitié de la mémoire système, en gardant à l'esprit les limites habituelles du tas Elasticsearch, puis validez avec votre charge de travail.

4. Mise en cache au niveau de l'application

  • Mécanisme : Implémentez un cache au niveau de votre couche applicative (par exemple, en utilisant Redis, Memcached ou un cache en mémoire) pour les résultats de recherche fréquemment demandés.
  • Efficacité : Peut fournir les temps de réponse les plus rapides en contournant complètement Elasticsearch pour les requêtes répétées. Idéal pour les résultats de recherche statiques ou à évolution lente.
  • Considérations : La stratégie d'invalidation du cache est essentielle. Nécessite une conception minutieuse pour garantir la cohérence des données.

Utilisation de l'API Profile pour l'identification des goulots d'étranglement

L'API Profile est un outil inestimable pour comprendre exactement comment Elasticsearch exécute une requête et où le temps est passé. Elle décompose le temps d'exécution pour chaque composant de votre requête et de votre agrégation.

Comment utiliser l'API Profile

Ajoutez simplement "profile": true au corps de votre requête de recherche.

GET /my-index/_search
{
  "profile": true,
  "query": {
    "bool": {
      "must": [
        {"match": {"title": "Elasticsearch"}},
        {"term": {"status.keyword": "published"}}
      ],
      "filter": [
        {"range": {"publish_date": {"gte": "2023-01-01"}}}
      ]
    }
  },
  "aggs": {
    "top_authors": {
      "terms": {
        "field": "author.keyword",
        "size": 10
      }
    }
  }
}

Interprétation des résultats de l'API Profile

La réponse inclura une section profile détaillant l'exécution des requêtes et des agrégations sur chaque shard. Les métriques clés à rechercher incluent :

  • description : Le composant de requête ou d'agrégation spécifique.
  • time_in_nanos : Le temps passé à exécuter ce composant.
  • breakdown : Sous-métriques détaillées comme build_scorer_time, collect_time, set_weight_time pour les requêtes, et reduce_time pour les agrégations.
  • children : Composants imbriqués, montrant comment le temps est distribué dans les requêtes complexes.

Exemple d'interprétation :

Si vous voyez un time_in_nanos élevé pour un WildcardQuery, cela confirme qu'il s'agit d'une partie coûteuse de votre requête. Si collect_time est élevé, cela suggère que la récupération et le traitement des documents après une correspondance sont un goulot d'étranglement, probablement en raison de l'analyse _source ou d'une pagination profonde. Un reduce_time élevé dans les agrégations peut indiquer une charge lourde pendant la phase de fusion finale.

En examinant ces métriques, vous pouvez identifier les clauses de requête ou les champs d'agrégation spécifiques qui consomment le plus de ressources, puis appliquer les techniques d'optimisation discutées précédemment.

Bonnes pratiques générales pour les performances

Au-delà des optimisations spécifiques aux requêtes, plusieurs bonnes pratiques à l'échelle du cluster et de l'index contribuent aux performances globales de la recherche.

1. Mappings d'index optimaux

  • text vs. keyword : Utilisez text pour la recherche en texte intégral et keyword pour la correspondance exacte, le tri et les agrégations. Des types mal adaptés peuvent conduire à des requêtes inefficaces.
  • doc_values : Assurez-vous que doc_values sont activés pour les champs que vous avez l'intention de trier ou d'agréger. Ils sont activés par défaut pour la plupart des types de champs qui prennent en charge le tri et les agrégations, tels que les champs keyword, numériques, de date, booléens et IP. Les champs text simples sont destinés à la recherche en texte intégral ; utilisez un sous-champ keyword lorsque vous avez besoin d'une correspondance exacte ou d'une agrégation.
  • norms : Désactivez norms ("norms": false) pour les champs où vous n'avez pas besoin de normalisation de la longueur du document (par exemple, les champs ID). Cela économise de l'espace disque et améliore la vitesse d'indexation, avec un impact minimal sur les performances des requêtes pour les requêtes sans scoring.
  • index_options : Pour les champs text, utilisez index_options: docs si vous avez seulement besoin de savoir si un terme existe dans un document, et index_options: positions (la valeur par défaut) si vous avez besoin de requêtes de phrase et de recherches de proximité.

2. Surveiller la santé du cluster et les ressources

  • Statut du cluster : Le vert est l'objectif. Le jaune signifie qu'un ou plusieurs shards de réplica ne sont pas assignés ; les recherches peuvent encore fonctionner, mais la résilience est réduite et les performances peuvent en souffrir. Le rouge signifie que des shards primaires sont manquants et que certaines données sont indisponibles.
  • Surveillance des ressources : Surveillez régulièrement l'utilisation du CPU, de la RAM, des E/S disque et du réseau sur vos nœuds de données. Les pics dans ces métriques sont souvent corrélés à des requêtes lentes.
  • Tas JVM : Gardez un œil sur l'utilisation du tas JVM. Une utilisation élevée peut entraîner des pauses fréquentes du garbage collector, rendant les requêtes lentes. Optimisez les requêtes pour réduire la pression sur le tas.

3. Allocation appropriée des shards

  • Trop de shards : Chaque shard consomme des ressources. De nombreux petits shards créent des frais généraux. Les shards de quelques dizaines de gigaoctets sont courants, mais la bonne taille dépend du tas, du modèle de requête, des objectifs de récupération et du matériel.
  • Pas assez de shards : Limite le parallélisme. Les requêtes sur un index avec trop peu de shards ne pourront pas exploiter efficacement tous les nœuds de données disponibles.

4. Stratégie d'indexation

  • Intervalle d'actualisation : Un refresh_interval plus bas (1 seconde par défaut) rend les données visibles plus rapidement mais augmente la surcharge d'indexation. Pour les charges de travail lourdes en recherche, envisagez de l'augmenter légèrement (par exemple, 5 à 10 secondes) pour réduire la pression d'actualisation.

Le flux de travail pratique est simple : trouvez la vraie requête lente, profilez-la, réduisez la quantité de données qu'elle touche, et faites correspondre le mapping à la façon dont les utilisateurs recherchent. Si la requête est déjà propre, examinez la disposition des shards, la pression du tas, le cache du système de fichiers et les E/S disque. Elasticsearch est rapide lorsque la conception de l'index, la forme de la requête et les ressources du cluster sont en accord.