Wie Elasticsearch bei der Erstellung von ChatGPT-Prompts in natürlicher Sprache hilft

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

ChatGPT ist zurzeit in aller Munde. Dieses Large Language Model (LLM) ist unter anderem in der Lage, Code zu generieren. Wir haben diese Funktion genutzt, um Elasticsearch DSL-Suchanfragen zu erstellen. Das Ziel besteht darin, für die Suche in Elasticsearch® einen Satz wie „Zeig mir die ersten 10 Dokumente von 2017 aus dem Aktienkursindex“ verwenden zu können. Das Experiment hat gezeigt, dass dies – mit Einschränkungen – möglich ist. In diesem Post beschreiben wir dieses Experiment und die Open-Source-Bibliothek, die wir für diesen Anwendungsfall veröffentlicht haben.

Kann ChatGPT Elasticsearch DSL-Code generieren?

Wir beginnen das Experiment mit einigen Tests, mit denen wir herausfinden möchten, inwiefern ChatGPT in der Lage ist, eine Elasticsearch DSL-Suchanfrage zu generieren. Zu diesem Zweck müssen wir ChatGPT Kontextinformationen über die Struktur der Daten geben, die durchsucht werden sollen. 

Daten werden in Elasticsearch in einem Index gespeichert. So ein Index ähnelt einer „Tabelle“ in einer relationalen Datenbank. Er hat ein Mapping (eine Zuordnung), das mehrere Felder und deren Typen definiert. Das heißt, dass wir die Mapping-Informationen des Index bereitstellen müssen, der abgefragt werden soll. Auf diese Weise erhält ChatGPT den notwendigen Kontext, um die Suchanfrage in Elasticsearch DSL-Code zu übersetzen.

Elasticsearch bietet für das Abrufen des Mappings eines Index eine „Get Mapping“-API. Für unser Experiment haben wir ein Aktienkursindex-Dataset verwendet, das Sie hier finden. Dieses Dataset enthält die Aktienkurse von 500 „Fortune“-Unternehmen aus dem Zeitraum Februar 2013 bis Februar 2018.

Im Folgenden sehen Sie die erste fünf Zeilen der CSV-Datei mit den Daten aus diesem Dataset:

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

Jede Zeile enthält das Datum, den Eröffnungswert, den Höchstwert und den Tiefstwert sowie den Schlusswert dieses Tages, das Volumen der gehandelten Aktien und schließlich den Namen der Aktie, also z. B. „American Airlines Group Inc. (AAL)“.

Das Mapping für den Aktienkursindex sieht wie folgt aus:

{
  "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"}
      }
    }
  }
}

Zum Abrufen des Mappings aus Elasticsearch können wir die GET /stocks/_mapping-API verwenden.

[Weiterführender Artikel: ChatGPT und Elasticsearch: OpenAI und private Daten]

Prompt erstellen, um herauszufinden, ob es funktioniert

Um eine in natürlicher Sprache ausgedrückte Suchanfrage in Elasticsearch DSL-Code übersetzen zu können, müssen wir den richtigen Prompt für ChatGPT finden. Das ist das Schwierigste bei diesem Prozess. Es geht darum, ChatGPT so zu programmieren, dass es das richtige Fragenformat (also den richtigen „Prompt“) verwendet.

Nach einigen Iterationen haben wir den folgenden Prompt entwickelt, der ganz gut zu funktionieren scheint:

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.

{mapping} und {query} im Prompt sind zwei Platzhalter, an deren Stelle die JSON-Zeichenfolge des Mappings (z. B. wie sie von „GET /stocks/_mapping“ im Beispiel oben zurückgegeben wird) und die Suchanfrage in natürlicher Sprache (z. B.: „Zeig mir die ersten 10 Dokumente von 2017“) tritt.

ChatGPT hat natürlich seine Grenzen und in einigen Fällen ist es nicht in der Lage, eine Frage zu beantworten. Unserer Erfahrung nach passiert das meistens dann, wenn der Satz im Prompt zu allgemein gehalten oder nicht eindeutig genug ist. Zur Lösung dieser Situation müssen wir den Prompt um weitere Details erweitern. Dieser Prozess wird „Iteration“ (schrittweise Verfeinerung) genannt. Es bedarf mehrerer Schritte, um schließlich zu dem Satz zu gelangen, mit dem wir unser Ziel erreichen.

Wenn Sie ausprobieren möchten, wie ChatGPT einen Satz für die Suche in eine Elasticsearch DSL- (oder sogar SQL-)Suchanfrage übersetzen kann, können Sie dsltranslate.com verwenden.

Alles zusammengeführt

Mithilfe der ChatGPT-API von OpenAI und der Elasticsearch-API für das Mapping und die Suche führen wir alles in einer experimentellen Bibliothek für PHP zusammen.

Diese Bibliothek zeigt eine search()-Funktion mit der folgenden API:

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

Hier ist $index der zu verwendende Indexname, $prompt die in natürlicher Sprache ausgedrückte Suchanfrage und $bool ein optionaler Parameter zur Nutzung eines Cache (standardmäßig aktiviert).

Die Funktionsweise kann dem folgenden Diagramm entnommen werden:

Diagramm zu Elasticsearch und OpenAI

Die Eingaben sind index und prompt (links). Der Index wird verwendet, um das Mapping aus Elasticsearch abzurufen (mithilfe der „Get Mapping“-API). Das Ergebnis ist ein Mapping in JSON, mit dessen Hilfe die Suchanfrage erstellt wird, die dann an ChatGPT gesendet wird. Dabei kommt der folgende API-Code zum Einsatz. Wir haben das OpenAI-Modell gpt-3.5-turbo verwendet, das in der Lage ist, im Code zu übersetzen.

Das Ergebnis, das ChatGPT zurückgibt, enthält eine Elasticsearch DSL-Suchanfrage, die wir Elasticsearch stellen können. Deren Ergebnis wird dann an die Nutzerin bzw. den Nutzer zurückgegeben. Für die Suchanfrage an Elasticsearch haben wir den offiziellen elastic/elasticsearch-php-Client verwendet.

Zur Optimierung der Antwortzeit und zur Reduzierung der Kosten der Nutzung der ChatGPT-API kam ein einfaches Caching-System auf Dateibasis zum Einsatz. Der Cache wurde für die folgenden Zwecke genutzt:

  • Speichern des von Elasticsearch zurückgegebenen Mapping-JSON-Codes: Wir speichern diesen JSON-Code in einer nach dem Index benannten Datei. Auf diese Weise können wir die Mapping-Informationen abrufen, ohne Elasticsearch unnötig oft aufrufen zu müssen.
  • Speichern des von ChatGPT generierten Elasticsearch DSL-Codes: Für das Caching des generierten Elasticsearch DSL-Codes haben wir die Cache-Datei mithilfe des Hash (MD5) des verwendeten Prompts benannt. Dadurch können wir bereits generierten Elasticsearch DSL-Code für dieselbe Suchanfrage wiederverwenden und müssen nicht noch einmal die ChatGPT-API bemühen.

Außerdem haben wir die Möglichkeit hinzugefügt, den Elasticsearch DSL-Code über die getLastQuery()-Funktion per Programmcode abzurufen.

Ausführung des Experiments mit Finanzdaten

Die Aktienkursdaten von hier haben wir in Elastic Cloud gespeichert. Ein einfaches Bulk-Skript hat uns geholfen, die Aktienkursdatei im CSV-Format zu lesen, und mit der Bulk-API haben wir sie an Elasticsearch gesendet.

Weitere Informationen dazu, wie Sie eine Elastic Cloud einrichten und den API-Schlüssel abrufen können, finden Sie in der Dokumentation.

Nachdem wir den Aktienkursindex gespeichert haben, haben wir mit einem einfachen PHP-Skript eine in Englisch formulierte Suchanfrage getestet. Dabei handelte es sich um das Skript examples/test.php.

Zur Ausführung dieses Skripts mussten wir drei Umgebungsvariablen festlegen:

  • OPENAI_API_KEY: Dies ist der API-Schlüssel von OpenAI.
  • ELASTIC_CLOUD_ENDPOINT: Dies ist die URL der Elasticsearch-Instanz.
  • ELASTIC_CLOUD_API_KEY: Dies ist der API-Schlüssel von Elastic Cloud.

Mithilfe des Aktienkurs-Mappings haben wir die folgenden Suchanfragen getestet und alle Elasticsearch DSL-Antworten aufgezeichnet:

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"
            }
          }
        }
      ]
    }
  }
}

Wie Sie sehen, sind die Ergebnisse gar nicht so schlecht. Besonders das letzte, in dem es um den Unterschied zwischen geschlossenen und offenen Feldern geht, hat uns wirklich beeindruckt! 

Alle Anfragen wurden in eine gültige Elasticsearch DSL-Anfrage übersetzt, die gemäß der in natürlicher Sprache ausgedrückten Frage korrekt ist.

Unterstützung für verschiedene Sprachen

Eine sehr schöne Funktion von ChatGPT ist die Fähigkeit, Fragen in unterschiedlichen Sprachen schreiben zu können.

Das heißt, dass Sie diese Bibliothek nutzen und die Suchanfragen in verschiedenen natürlichen Sprachen – Deutsch, Französisch, Italienisch, Spanisch usw. – angeben können.

Hier ein Beispiel:

# 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');

Alle vorherigen Suchen haben dieselben Ergebnisse, die (mehr oder weniger) zur folgenden Elasticsearch-Suchanfrage führen:

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

Wichtig: ChatGPT ist ein LLM, das für Englisch optimiert wurde. Es produziert also die besten Ergebnisse, wenn die Anfragen in Englisch eingegeben werden.

Grenzen von LLMs

ChatGPT im Speziellen und LLMs im Allgemeinen sind leider nicht in der Lage, die semantische Richtigkeit ihrer Antworten zu prüfen. Für sie ist eine Antwort korrekt, wenn aus statistischer Perspektive alles stimmig ist. Das bedeutet für uns, dass wir nicht testen können, ob die von ChatGPT generierte Elasticsearch DSL-Suchanfrage die richtige Übersetzung der in natürlicher Sprache formulierten Anfrage ist.

Das ist natürlich momentan noch eine sehr große Einschränkung. Es gibt Anwendungsfälle, wie mathematische Operationen, in denen das Korrektheitsproblem mithilfe eines externen Plugins, z. B. des Wolfram-Plugins von ChatGPT, gelöst werden kann. In diesem Fall nutzt das Ergebnis von ChatGPT die Wolfram-Engine, die die Korrektheit der Antwort anhand eines mathematischen symbolischen Modells prüft.

Neben der Einschränkung durch das Fehlen einer Korrektheitsprüfung, das für uns bedeutet, dass wir die Antworten von ChatGPT nie ungeprüft übernehmen sollten, gibt es auch Grenzen, was die Fähigkeit betrifft, einen Satz in natürlicher Sprache in eine Elasticsearch DSL-Suchanfrage zu übersetzen.

Nehmen wir zum Beispiel an, dass wir für das oben erwähnte Aktienkurs-Dataset Folgendes anfordern:

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"
            }
          }
        }
      ]
    }
  }
}

In diesem Fall wäre die von ChatGPT generierte DSL-Suchanfrage nicht gültig und es würde die folgende Fehlermeldung angezeigt werden:

Failed to parse date field [2015-01-01] with format [yyyy].

Wenn wir den Satz aber mit konkreteren Angaben umformulieren und so die Mehrdeutigkeit bezüglich des Datenformats entfernen würden, erhalten wir eine korrekte Antwort:

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"
          }
        }
      ]
    }
  }
}

Im Grunde bedeutet das, dass der Satz kein echter „natürlicher“ Satz sein darf, sondern dass er mit einer Beschreibung formuliert werden muss, wie die Elasticsearch DSL-Suchanfrage aussehen soll.

Fazit

In diesem Blogpost haben wir einen experimentellen Anwendungsfall von ChatGPT vorgestellt, in dem es darum geht, in natürlicher Sprache formulierte Suchanfragen in Elasticsearch DSL-Suchanfragen zu übersetzen. Wir haben eine einfache Bibliothek in PHP entwickelt, damit wir zum Übersetzen der Suchanfrage im Hintergrund die OpenAI-API nutzen können. Dies ging mit der Bereitstellung eines Cache-Systems einher.

Trotz der Grenzen hinsichtlich der Korrektheitsprüfung der Antwort sind die Ergebnisse des Experiments vielversprechend. Dennoch werden wir uns auf jeden Fall weiter mit der Möglichkeit beschäftigen, Suchanfragen für Elasticsearch in natürlicher Sprache zu formulieren – ob in ChatGPT oder in den anderen LLMs, die sich immer größerer Beliebtheit erfreuen.

Informationen zu den weiteren Möglichkeiten, die Elasticsearch und KI bieten



In diesem Blogpost haben wir möglicherweise auf generativer KI basierende Tools von Drittanbietern verwendet, die von ihren jeweiligen Eigentümern betrieben werden. Elastic hat keine Kontrolle über die Drittanbieter-Tools und übernimmt keine Verantwortung oder Haftung für ihre Inhalte, ihren Betrieb oder ihre Verwendung sowie für etwaige Verluste oder Schäden, die sich aus der Verwendung solcher Tools durch Sie ergeben. Gehen Sie vorsichtig vor, wenn Sie KI-Tools mit persönlichen, sensiblen oder vertraulichen Daten verwenden. Alle Daten, die Sie eingeben, können für das Training von KI oder andere Zwecke verwendet werden. Es gibt keine Garantie dafür, dass Informationen, die Sie bereitstellen, sicher oder vertraulich behandelt werden. Setzen Sie sich vor Gebrauch mit den Datenschutzpraktiken und den Nutzungsbedingungen generativer KI-Tools auseinander.

Elastic, Elasticsearch und zugehörige Marken, Waren- und Dienstleistungszeichen sind Marken oder eingetragene Marken von Elasticsearch N.V. in den USA und anderen Ländern. Alle weiteren Marken- oder Warenzeichen sind eingetragene Marken oder eingetragene Warenzeichen der jeweiligen Eigentümer.