Maîtrise du Query DSL Elasticsearch : Commandes Essentielles pour la Récupération de Données

Libérez la puissance de la récupération Elasticsearch en maîtrisant le Query DSL. Ce guide décompose les structures de requêtes JSON essentielles, en se concentrant sur l'utilisation pratique des requêtes `match`, `term` et range. Apprenez la différence cruciale entre les clauses `must` (scoring) et `filter` (mise en cache) dans la requête `bool` fondamentale, vous permettant de construire efficacement des recherches de données complexes et performantes.

Maîtrise du Query DSL Elasticsearch : Commandes Essentielles pour la Récupération de Données

Le Query DSL Elasticsearch est le langage JSON que vous utilisez lorsqu'une simple boîte de recherche ne suffit pas. Il vous permet de mélanger la recherche en texte intégral, les filtres exacts, les plages de dates, le tri, la pagination et les agrégations en une seule requête. Cette flexibilité est utile, mais elle facilite également l'écriture d'une requête qui renvoie les mauvais documents ou qui fonctionne bien en test et ralentit en production.

La meilleure façon d'apprendre le Query DSL est de garder deux questions à l'esprit : « Est-ce que je recherche du texte pour la pertinence ? » et « Est-ce que je filtre des valeurs exactes ? » La plupart des choix de requêtes découlent de cette distinction.

L'Anatomie d'une Requête de Recherche Elasticsearch

Toutes les recherches Elasticsearch sont effectuées sur le point de terminaison _search d'un index spécifique (ou d'index). Une requête de recherche de base est une requête POST contenant un corps JSON qui définit les paramètres de la requête. La partie la plus critique de ce corps est l'objet query.

Structure de base :

POST /your_index_name/_search
{
  "query": { ... Définissez votre structure de requête ici ... },
  "size": 10, 
  "from": 0
}

Types de Requêtes Fondamentaux : Précision et Pertinence

Le Query DSL offre un large éventail de requêtes adaptées à différents types de données et besoins de correspondance. Le choix de la requête a un impact significatif à la fois sur le score de pertinence et les performances.

1. Recherche en Texte Intégral : La Requête match

La requête match est la norme pour la recherche en texte intégral sur les champs analysés. Elle tokenise le terme de recherche et vérifie la présence de tokens correspondants dans le(s) champ(s) spécifié(s).

Cas d'utilisation : Recherche de texte en langage naturel où le score de pertinence est important.

Exemple : Trouver des documents où le champ 'description' contient le mot 'cloud' ou 'computing'.

GET /products/_search
{
  "query": {
    "match": {
      "description": "cloud computing"
    }
  }
}

2. Correspondance de Valeur Exacte : La Requête term

La requête term recherche les documents contenant le terme exact spécifié. Contrairement à match, elle n'effectue pas d'analyse sur la chaîne de recherche, ce qui la rend idéale pour les correspondances exactes sur des mots-clés, des identifiants ou des champs indexés numériquement.

Cas d'utilisation : Filtrage par valeurs exactes dans des champs non analysés (comme les champs keyword ou les nombres).

Exemple : Récupérer un produit avec l'ID exact SKU10021.

GET /products/_search
{
  "query": {
    "term": {
      "product_id": "SKU10021"
    }
  }
}

3. Requêtes de Plage (Range)

Les requêtes de plage vous permettent de filtrer les documents dont la valeur d'un champ se situe dans une plage spécifiée (numérique, date ou chaîne).

Syntaxe : Utilise gt (supérieur à), gte (supérieur ou égal à), lt (inférieur à) et lte (inférieur ou égal à).

Exemple : Trouver les commandes passées après le 1er janvier 2024.

GET /orders/_search
{
  "query": {
    "range": {
      "order_date": {
        "gte": "2024-01-01",
        "lt": "2025-01-01"
      }
    }
  }
}

4. Filtrage par Présence : La Requête exists

La requête exists identifie les documents où un champ spécifique est présent (c'est-à-dire non nul ou manquant).

Exemple : Trouver tous les utilisateurs qui ont fourni une adresse e-mail.

GET /users/_search
{
  "query": {
    "exists": {
      "field": "email_address"
    }
  }
}

Construire une Logique Complexe avec la Requête bool

Pour pratiquement toutes les applications de recherche réelles, vous devez combiner plusieurs critères. La requête bool est l'outil essentiel pour cela, vous permettant de combiner d'autres clauses de requête en utilisant la logique booléenne.

Clauses dans bool

La requête bool accepte quatre clauses principales :

  1. must : Toutes les clauses de ce tableau doivent correspondre. Les clauses dans must contribuent au score de pertinence.
  2. filter : Toutes les clauses de ce tableau doivent correspondre, mais elles sont exécutées dans un contexte sans scoring. Cela les rend beaucoup plus rapides pour les critères d'inclusion/exclusion stricts.
  3. should : Au moins une clause de ce tableau devrait correspondre. Ces clauses influencent le score de pertinence mais sont facultatives pour la correspondance.
  4. must_not : Aucune des clauses de ce tableau ne doit correspondre (l'équivalent d'un NON logique).

Exemple Pratique de Requête bool

Combinons plusieurs concepts pour trouver des documents haute priorité qui mentionnent 'sécurité' mais excluent les brouillons et sont disponibles dans la région 'US'.

GET /logs/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "content": "security breach"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "region.keyword": "US"
          }
        }
      ],
      "should": [
        {
          "term": {
            "priority": 5
          }
        }
      ],
      "must_not": [
        {
          "term": {
            "status.keyword": "DRAFT"
          }
        }
      ]
    }
  }
}

Explication de l'Exemple :

  • Must : Le document doit contenir l'expression "security breach" dans le champ de contenu analysé.
  • Filter : Le document doit être étiqueté pour la région 'US' (une correspondance exacte rapide).
  • Should : Les documents correspondant à priority: 5 recevront un boost dans leur score de pertinence, mais les documents avec des priorités inférieures qui satisfont aux clauses must et filter seront toujours renvoyés.
  • Must Not : Les documents marqués comme 'DRAFT' sont strictement exclus.

Bonnes Pratiques pour la Construction de Requêtes

Pour garantir que vos recherches sont à la fois précises et performantes, respectez ces directives :

  • Préférez filter à must pour les critères sans scoring. Si vous vérifiez uniquement l'inclusion/exclusion (par exemple, filtrage par ID, date exacte ou statut), utilisez toujours la clause filter dans une requête bool. Cela exploite la mise en cache et évite des calculs de scoring coûteux.
  • Utilisez les Requêtes Exactes à Bon Escient : Pour les champs mappés comme text (analysés), utilisez match. Pour les champs mappés comme keyword (non analysés), utilisez term ou des requêtes de plage.
  • Évitez l'Emboîtement Profond : Bien que possible, les requêtes bool profondément imbriquées peuvent devenir difficiles à lire et à déboguer, et peuvent parfois entraîner une dégradation des performances.
  • Tirez Parti de minimum_should_match : Pour les clauses should, définir minimum_should_match (par exemple, à 1 ou 2) force un certain nombre de ces critères facultatifs à être satisfaits, les transformant effectivement en critères obligatoires tout en leur permettant de contribuer au scoring.

Le Mapping Décide Quelle Requête a du Sens

La plupart des erreurs de Query DSL commencent par le mapping. Une requête peut sembler correcte et pourtant renvoyer des résultats déroutants si le champ est mappé différemment de ce que vous pensez.

Un modèle courant est un champ texte avec un sous-champ keyword :

{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "status": { "type": "keyword" },
      "created_at": { "type": "date" },
      "price": { "type": "double" }
    }
  }
}

Utilisez match sur title lorsque vous souhaitez un comportement de texte intégral analysé. Utilisez term sur title.keyword lorsque vous avez besoin de la valeur exacte du titre. Utilisez term sur status car c'est déjà un keyword. Utilisez range sur created_at ou price car ces champs sont des valeurs de date et numériques.

Si une requête term sur un champ texte ne fonctionne pas comme prévu, le problème est souvent l'analyse. Les tokens stockés peuvent être en minuscules, divisés, racinisés ou autrement modifiés. Vérifiez le mapping avant de modifier la requête.

GET /products/_mapping

Pour les problèmes d'analyse de texte, _analyze est utile :

GET /products/_analyze
{
  "field": "description",
  "text": "Cloud Computing"
}

Cela montre quels tokens Elasticsearch va rechercher.

match, match_phrase, et multi_match

match est la requête de texte intégral quotidienne, mais ce n'est pas la seule que vous utiliserez.

Utilisez match_phrase lorsque l'ordre des mots est important :

GET /products/_search
{
  "query": {
    "match_phrase": {
      "description": "wireless charging stand"
    }
  }
}

Ceci est utile pour les noms de produits, les messages de journal, les titres de documents et les phrases où la séquence exacte a du sens. C'est plus strict que match, donc cela peut renvoyer moins de documents.

Utilisez multi_match lorsque la même entrée utilisateur doit rechercher plusieurs champs :

GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "noise cancelling headphones",
      "fields": ["title^3", "description", "brand^2"]
    }
  }
}

Les boosts ^3 et ^2 indiquent à Elasticsearch que les correspondances dans title et brand doivent compter plus que les correspondances dans description. Le boosting n'est pas une garantie qu'un document sera classé premier ; c'est un indice de scoring. Testez avec des requêtes réelles avant d'ajuster les boosts de manière trop agressive.

Pagination Sans Nuire au Cluster

Les paramètres de base from et size sont parfaits pour une pagination superficielle :

GET /products/_search
{
  "from": 20,
  "size": 10,
  "query": {
    "match": {
      "description": "laptop sleeve"
    }
  }
}

La pagination profonde est différente. Demander la page 1 000 oblige Elasticsearch à trier et à ignorer de nombreux résultats. Pour la recherche orientée utilisateur, évitez la pagination profonde illimitée. Pour les exportations ou les analyses en arrière-plan, utilisez search_after avec un tri stable :

GET /products/_search
{
  "size": 100,
  "sort": [
    { "created_at": "asc" },
    { "_id": "asc" }
  ],
  "search_after": ["2025-01-10T12:00:00Z", "abc123"],
  "query": {
    "term": {
      "status": "active"
    }
  }
}

Les valeurs dans search_after proviennent du tableau sort du dernier résultat de la réponse précédente. Cette approche est plus stable pour parcourir de grands ensembles de résultats.

Le Filtrage de la Source (_source) Garde les Réponses Utiles

Les performances de recherche ne se limitent pas à l'exécution de la requête. Le renvoi de documents volumineux peut ralentir le client, le réseau et le nœud coordinateur. Si l'interface utilisateur n'a besoin que de quelques champs, demandez ces champs :

GET /orders/_search
{
  "_source": ["order_id", "customer_id", "total", "created_at", "status"],
  "query": {
    "bool": {
      "filter": [
        { "term": { "status": "paid" } },
        { "range": { "created_at": { "gte": "now-7d/d" } } }
      ]
    }
  }
}

Cela rend la réponse plus facile à lire et peut réduire la taille de la charge utile. Cela ne remplace pas une bonne conception d'index, mais cela aide lorsque les documents contiennent de grandes descriptions, des blobs de métadonnées ou des tableaux imbriqués dont la page actuelle n'a pas besoin.

Le Tri et les Agrégations Nécessitent les Bons Champs

Trier sur du texte analysé est généralement une erreur. Triez sur des champs keyword, numériques ou de date :

GET /products/_search
{
  "sort": [
    { "price": "asc" },
    { "title.keyword": "asc" }
  ],
  "query": {
    "term": {
      "status": "active"
    }
  }
}

La même chose s'applique à de nombreuses agrégations. Si vous voulez des comptes par statut, agrégéz sur un champ keyword :

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "orders_by_status": {
      "terms": {
        "field": "status"
      }
    }
  },
  "query": {
    "range": {
      "created_at": {
        "gte": "now-30d/d"
      }
    }
  }
}

size: 0 indique à Elasticsearch que vous voulez uniquement les résultats d'agrégation, pas les documents correspondants. C'est une petite habitude qui garde les réponses plus propres.

Déboguer les Requêtes Avec explain et profile

Lorsqu'un résultat se classe étrangement, utilisez explain sur un seul document :

GET /products/_explain/SKU10021
{
  "query": {
    "match": {
      "description": "cloud computing"
    }
  }
}

Lorsqu'une requête est lente, utilisez profile dans un environnement non productif ou un test de production soigneusement contrôlé :

GET /products/_search
{
  "profile": true,
  "query": {
    "bool": {
      "must": [
        { "match": { "description": "cloud computing" } }
      ],
      "filter": [
        { "term": { "status": "active" } }
      ]
    }
  }
}

La sortie du profile est verbeuse, mais elle peut montrer si le temps est passé dans une requête texte, un filtre, un script ou une autre partie de la requête. Ne laissez pas le profilage activé dans le code de l'application ; utilisez-le comme outil de débogage.

Une Habitude Sensée de Construction de Requêtes

Pour la plupart des recherches d'applications, construisez la requête dans cet ordre :

  1. Mettez les contraintes exactes dans filter : ID du locataire, statut, région, fenêtre de date, permissions.
  2. Mettez le texte saisi par l'utilisateur dans must avec match, match_phrase ou multi_match.
  3. Utilisez should pour les préférences de classement, pas les exigences strictes, sauf si vous définissez minimum_should_match.
  4. Limitez _source aux champs dont l'appelant a besoin.
  5. Ajoutez un tri stable si la pagination ou les exportations sont importantes.
  6. Vérifiez le mapping avant de blâmer Elasticsearch.

Le Query DSL est puissant car il sépare le filtrage, le scoring, le tri et la mise en forme de la réponse. Une fois que vous gardez ces tâches séparées, les requêtes deviennent plus faciles à lire, plus faciles à régler et moins surprenantes en production.

Un Petit Exemple de Dépannage

Supposons qu'un utilisateur recherche ACME-1000 et n'obtient aucun résultat, même si le produit existe. N'ajoutez pas immédiatement des wildcards. Vérifiez d'abord le mapping. Si sku est un keyword, cela devrait fonctionner :

GET /products/_search
{
  "query": {
    "term": {
      "sku": "ACME-1000"
    }
  }
}

Si sku a été accidentellement mappé comme text, l'analyse a peut-être divisé ou modifié la valeur. Vous pouvez toujours l'interroger dans certains cas, mais la meilleure solution est généralement un changement de mapping pour les index futurs. Les identifiants exacts, les statuts, les régions et les ID de locataires doivent être des champs de type keyword. Les descriptions et titres rédigés par des humains doivent être des champs de type texte. Le Query DSL devient beaucoup plus facile lorsque le mapping correspond à la façon dont les gens récupèrent réellement les données.