Comment utiliser ChatGPT pour convertir une phrase en requête en langage dédié d'Elasticsearch

blog-thumb-elasticsearch-gears-light-blue.png

Ces derniers temps, tout le monde parle de ChatGPT. L'une des fonctionnalités intéressantes que propose ce grand modèle de langage (LLM) est la possibilité de générer du code. Nous nous en sommes servis pour générer des requêtes en Elasticsearch DSL. L'objectif est de faire des recherches dans Elasticsearch® en utilisant des phrases du type : "Retourner les 10 premiers documents de 2017 de l'indice boursier". Cet exercice nous a montré que c'était possible, avec toutefois quelques limites. Dans cet article, nous décrirons cette expérience et la bibliothèque open source que nous avons publiée pour ce cas d'utilisation.

ChatGPT peut-il générer des requêtes en Elasticsearch DSL ?

Pour démarrer cette expérience, nous réalisons quelques tests pour vérifier la capacité de ChatGPT à générer des requêtes en Elasticsearch DSL. Dans cette optique, vous devez fournir du contexte à ChatGPT concernant la structure des données dans lesquelles vous souhaitez effectuer vos recherches. 

Dans Elasticsearch, les données sont stockées dans un index, qui s'apparente à un "tableau" dans une base de données relationnelles. Son mapping définit plusieurs champs et leur type. Nous devons donc fournir les informations relatives au mapping de l'index que nous souhaitons interroger. En procédant de la sorte, nous fournissons à ChatGPT le contexte nécessaire pour convertir une requête en Elasticsearch DSL.

Elasticsearch propose une API Get Mapping" pour extraire le mapping d'un index. Dans notre expérience, nous nous sommes servis d'un ensemble de données venant d'un indice boursier disponible ici. Cet ensemble de données contient les valeurs des actions des entreprises du Fortune 500 sur cinq ans, de février 2013 à février 2018.

Nous avons indiqué ci-dessous les cinq premières lignes du fichier CSV de l'ensemble de données :

date,open,high,low,close,volume,name
2013-02-08,15.07,15.12,14.63,14.75,8407500,AAL
2013-02-11,14.89,15.01,14.26,14.46,8882000,AAL
2013-02-12,14.45,14.51,14.1,14.27,8126000,AAL
2013-02-13,14.3,14.94,14.25,14.66,10259500,AAL

Chaque ligne contient la date de l'action, la valeur du jour à l'ouverture (open), les valeurs haute (high) et basse (low), la valeur à la fermeture (close), le volume des actions échangées, et pour finir, le nom (name) de l'action (par exemple, American Airlines Group Inc. (AAL).

Le mapping associé à l'indice boursier est le suivant :

{
  "stocks": {
    "mappings": {
      "properties": {
        "close": {"type":"float"},
        "date" : {"type":"date"},
        "high" : {"type":"float"},
        "low"  : {"type":"float"},
        "name" : {
          "type": "text",
          "fields": {
            "keyword":{"type":"keyword", "ignore_above":256}
          }
        },
        "open"  : {"type":"float"},
        "volume": {"type":"long"}
      }
    }
  }
}

Nous pouvons utiliser l'API GET /stocks/_mapping pour extraire le mapping d'Elasticsearch.

[Article associé : ChatGPT et Elasticsearch : les liens entre OpenAI et les données privées]

Création d'une invite

Pour convertir une requête exprimée en langage naturel en Elasticsearch DSL, nous devons déterminer l'invite appropriée à fournir à ChatGPT. C'est la partie la plus difficile du processus : programmer ChatGPT avec le format de question approprié (en d'autres termes, l'invite appropriée).

Après avoir effectué quelques itérations, nous avons abouti à l'invite suivante, qui semble fonctionner plutôt bien :

Given the mapping delimited by triple backticks ```{mapping}``` translate the text delimited by triple quotes in a valid Elasticsearch DSL query """{query}""". Give me only the json code part of the answer. Compress the json output removing spaces.

Les valeurs {mapping} et {query} dans l'invite sont deux espaces réservés à remplacer par la chaîne json du mapping (par exemple, celles renvoyées par GET /stocks/_mapping ci-dessus) et la requête exprimée en langage naturel (par exemple, retourner les 10 premiers documents de 2017).

Bien entendu, ChatGPT est limité, et dans certains cas, il ne sera pas capable de répondre à la question posée. Nous avons constaté que, la plupart du temps, lorsque le cas se présente, c'est que la phrase utilisée dans l'invite est trop générale ou ambiguë. Pour pallier ce problème, nous devons améliorer l'invite en indiquant plus de détails. C'est ce qu'on appelle une itération. Elle nécessite plusieurs étapes pour définir la phrase appropriée à utiliser.

Si vous voulez voir comment ChatGPT peut convertir une phrase de recherche en une requête en Elasticsearch DSL (ou en SQL), vous pouvez utiliser dsltranslate.com.

Mise en commun

À l'aide de l'API ChatGPT proposée par OpenAI et de l'API Elasticsearch pour le mapping et la recherche, nous réunissons tous les éléments dans une bibliothèque expérimentale en PHP.

Cette bibliothèque présente une fonction search() avec l'API suivante :

search(string $index, string $prompt, bool $cache = true)

$index est le nom de l'index à utiliser, $prompt est la requête exprimée en langage naturel et $bool est un paramètre facultatif pour utiliser un cache (activé par défaut).

Le processus de cette fonction est illustré dans le diagramme ci-dessous :

elasticsearch openai diagram

Les données d'entrée sont index et prompt (à gauche). L'index est utilisé pour récupérer le mapping à partir d'Elasticsearch (avec l'API Get Mapping). Le résultat obtenu est un mapping en JSON utilisé pour construire la chaîne de requête à envoyer à ChatGPT à l'aide du code d'API suivant. Nous avons utilisé le modèle gpt-3.5-turbo d'OpenAI, qui est capable d'effectuer des conversions en code.

Le résultat fourni par ChatGPT contient une requête en Elasticsearch DSL, dont nous nous sommes servis pour interroger Elasticsearch. Ce résultat est ensuite renvoyé à l'utilisateur. Pour interroger Elasticsearch, nous avons utilisé le client elastic/elasticsearch-php officiel.

Pour optimiser le temps de réponse et réduire le coût associé à l'utilisation de l'API ChatGPT, nous avons utilisé un système de mise en cache simple basé sur les fichiers. Nous nous en sommes servis pour :

  • Stocker le JSON de mapping renvoyé par Elasticsearch : nous stockons ce JSON dans un fichier dont le nom est fonction de l'index. Nous pouvons ainsi récupérer les informations de mapping sans avoir à appeler de nouveau Elasticsearch.
  • Stocker l'Elasticsearch DSL généré par ChatGPT : pour mettre en cache l'Elasticsearch DSL généré, nous nommons le fichier de cache à l'aide du hachage (MD5) de l'invite utilisée. Cette approche nous permet de réutiliser l'Elasticsearch DSL précédemment généré pour la même requête, ce qui évite d'avoir à appeler de nouveau l'API ChatGPT.

Nous avons aussi ajouté la possibilité de récupérer l'Elasticsearch DSL de manière programmée grâce à la fonction getLastQuery().

Réalisation de l'expérience avec des données financières

Nous avons utilisé Elastic Cloud pour stocker les valeurs des actions consignées ici. Concrètement, nous nous sommes servis d'un simple script en vrac pour lire le fichier des actions au format CSV et l'envoyer à Elasticsearch via l'API Bulk.

Pour en savoir plus sur la configuration d'Elastic Cloud et la récupération d'une clé d'API, lisez cette documentation.

Une fois l'index des actions stocké, nous avons utilisé un simple script PHP pour tester certaines requêtes exprimées en anglais. Le script que nous avons utilisé est examples/test.php.

Pour exécuter ce script examples/test.php, nous devons définir trois variables d'environnement :

  • OPENAI_API_KEY : la clé d'API d'OpenAI
  • ELASTIC_CLOUD_ENDPOINT : l'URL de l'instance Elasticsearch
  • ELASTIC_CLOUD_API_KEY : la clé d'API d'Elastic Cloud

À l'aide du mapping des actions, nous avons testé les requêtes suivantes et enregistré toutes les réponses en Elasticsearch DSL :

Query: Return the first 10 documents of 2017

Result:
{
  "query":{
    "range":{
      "date":{
        "gte":"2017-01-01",
        "lte":"2017-12-31"
      }
    }
  },
  "size":10
}
Query: Return the first 30 names of all the different stock names

Result:
{
  "size":0,
  "aggs":{
    "unique_names":{
      "terms":{
        "field":"name.keyword",
        "size":30
      }
    }
  }
}
Query: Return the max value of the field "high" for each stock in 2015

Result: 
{
  "aggs":{
    "stocks":{
      "terms":{"field":"name.keyword","size":100},
      "aggs":{
        "max_high":{"max":{"field":"high"}}
      }
    }
  },
  "query":{
    "bool":{
      "must":[{
        "range":{
          "date":{
            "gte":"2015-01-01",
            "lt":"2016-01-01"
          }
        }
      }]
    }
  }
}
Query: Return the average value of the field "high" for each stock in 2015

Result:
{
  "size":0,
  "aggs":{
    "stocks":{
      "terms":{
        "field":"name.keyword",
        "size":10000
      },
      "aggs":{
        "2015":{
          "filter":{
            "range":{
              "date":{
                "gte":"2015-01-01",
                "lt":"2016-01-01"
              }
            }
          },
          "aggs":{
            "avg_high":{
              "avg":{
                "field":"high"
              }
            }
          }
        }
      }
    }
  }
}
Query: Return the max value of the field "high" for all the documents with name MON in 2014

Result:

{
  "query":{
    "bool":{
      "must":[
        {
          "match":{
            "name.keyword":"MON"
          }
        },
        {
          "range":{
            "date":{
              "gte":"2014-01-01",
              "lt":"2015-01-01"
            }
          }
        }
      ]
    }
  },
  "size":0,
  "aggs":{
    "max_high":{
      "max":{
        "field":"high"
      }
    }
  }
}
Query: Return the documents that have the difference between close and open fields > 20

Response:
{
  "query":{
    "bool":{
      "must":[
        {
          "script":{
            "script":{
              "lang":"painless",
              "source":"doc['close'].value - doc['open'].value > 20"
            }
          }
        }
      ]
    }
  }
}

Comme vous pouvez le constater, les résultats sont plutôt bons. La dernière réponse concernant la différence entre des champs fermés et des champs ouverts est impressionnante ! 

Toutes les requêtes ont été traduites en Elasticsearch DSL de manière appropriée, en respectant la question exprimée en langage naturel.

Exprimez-vous dans votre langue !

L'une des fonctionnalités très pratiques de ChatGPT est la possibilité de poser des questions en différentes langues.

Cela signifie que vous pouvez utiliser cette bibliothèque et poser une question dans la langue de votre choix, par exemple, en italien, en espagnol, en français, en allemand, etc.

En voici un exemple :

# English
$result = $chatGPT->search('stocks', 'Return the first 10 documents of 2017');
# Italian
$result = $chatGPT->search('stocks', 'Restituisci i primi 10 documenti del 2017');
# Spanish
$result = $chatGPT->search('stocks', 'Devuelve los 10 primeros documentos de 2017');
# French
$result = $chatGPT->search('stocks', 'Retourner les 10 premiers documents de 2017');
# German
$result = $chatGPT->search('stocks', 'Senden Sie die ersten 10 Dokumente des Jahres 2017 zurück');

Toutes les recherches précédentes aboutissent aux mêmes résultats, produisant la même requête Elasticsearch (plus ou moins) :

{"size":10,"query":{"range":{"date":{"gte":"2017-01-01","lt":"2018-01-01"}}}}

Important : ChatGPT est un LLM qui a été optimisé pour l'anglais, ce qui signifie que, pour avoir des résultats optimaux, il est préférable de saisir des requêtes en anglais.

Limites des LLM

Malheureusement, ChatGPT, comme tous les LLM de façon générale, n'est pas capable de vérifier l'exactitude de sa réponse d'un point de vue sémantique. Il fournit une réponse qui semble correcte d'un point de vue statistique. Ce qui signifie que nous ne pouvons pas vérifier si la requête générée par ChatGPT en Elasticsearch DSL correspond bien à la requête en langage naturel.

C'est, évidemment, un obstacle conséquent pour le moment. Dans certains autres cas d'utilisation, comme les opérations mathématiques, nous pouvons pallier le problème d'exactitude en utilisant un plug-in externe, tel que le plug-in Wolfram de ChatGPT. Dans ce cas, le résultat de ChatGPT se sert du moteur Wolfram, qui vérifie l'exactitude de la réponse à l'aide d'un modèle symbolique mathématique.

Outre la limite concernant l'exactitude, qui nous oblige à vérifier les réponses de ChatGPT, il existe également des limitations quant à la capacité à convertir une phrase humaine en requête en Elasticsearch DSL.

Par exemple, si nous reprenons l'ensemble de données précédent concernant les actions et que nous demandons quelque chose du style :

Query: Return the first 10 documents of 2017 and 2015

Result:
{
  "from":0,
  "size":10,
  "query":{
    "bool":{
      "must":[
        {
          "range":{
            "date":{
              "gte":"2015-01-01",
              "lte":"2015-12-31",
              "format":"yyyy"
            }
          }
        },
        {
          "range":{
            "date":{
              "gte":"2017-01-01",
              "lte":"2017-12-31",
              "format":"yyyy"
            }
          }
        }
      ]
    }
  }
}

La requête en langage dédié d'Elasticsearch qui a été générée par ChatGPT n'est pas valide, ce qui entraîne l'erreur suivante dans Elasticsearch :

Failed to parse date field [2015-01-01] with format [yyyy] (Impossible d'analyser le champ de données [2015-01-01] au format [yyyy]).

Si nous reformulons la phrase à l'aide d'informations plus précises et que nous supprimons l’ambiguïté apparente concernant le format de date, nous pouvons obtenir la réponse appropriée, comme suit :

Query: Return the first 10 documents with year 2017 and 2015 in "date" field

Result:
{
  "size":10,
  "query":{
    "bool":{
      "should":[
        {
          "term":{
            "date":"2017"
          }
        },
        {
          "term":{
            "date":"2015"
          }
        }
      ]
    }
  }
}

Pour le dire simplement, la phrase doit être exprimée à l'aide d'une description indiquant comment devrait être formulé l'Elasticsearch DSL par rapport à une phrase humaine réelle.

Conclusion

Dans cet article, nous avons abordé un cas d'utilisation expérimental, consistant à utiliser ChatGPT pour convertir les phrases de recherche en langage naturel en requêtes en Elasticsearch DSL. Nous avons mis au point une bibliothèque simple en PHP pour utiliser l'API OpenAI afin de convertir la requête en coulisses, tout en nous servant d'un système de mise en cache.

Les résultats de cette expérience sont prometteurs, malgré la limite concernant l'exactitude de la réponse. Ceci étant dit, nous allons pousser davantage nos recherches sur la possibilité d'interroger Elasticsearch en langage naturel en passant par ChatGPT, ainsi que par d'autres modèles de LLM de plus en plus populaires.

Découvrez en plus sur les possibilités qu'offrent Elasticsearch et l'IA.



Dans cet article, nous sommes susceptibles d'avoir utilisé des outils d'IA générative tiers appartenant à leurs propriétaires respectifs qui en assurent aussi le fonctionnement. Elastic n'a aucun contrôle sur les outils tiers et n'est en aucun cas responsable de leur contenu, de leur fonctionnement, de leur utilisation, ni de toute perte ou de tout dommage susceptible de survenir à cause de l'utilisation de tels outils. Lorsque vous utilisez des outils d'IA avec des informations personnelles, sensibles ou confidentielles, veuillez faire preuve de prudence. Toute donnée que vous saisissez dans ces solutions peut être utilisée pour l'entraînement de l'IA ou à d'autres fins. Vous n'avez aucune garantie que la sécurisation ou la confidentialité des informations renseignées sera assurée. Vous devriez vous familiariser avec les pratiques en matière de protection des données personnelles et les conditions d'utilisation de tout outil d'IA générative avant de l'utiliser.

Elastic, Elasticsearch et les marques associées sont des marques commerciales, des logos ou des marques déposées d'Elasticsearch N.V. aux États-Unis et dans d'autres pays. Tous les autres noms de produits et d'entreprises sont des marques commerciales, des logos ou des marques déposées appartenant à leurs propriétaires respectifs.