Elasticsearchを使用してChatGPTへのプロンプトを自然言語で用意する方法

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

最近は、誰もがChatGPTを話題にしています。この大規模言語モデル(LLM)が備える優れた機能の1つは、コードを生成できることです。私たちは、この機能を使ってElasticsearch DSLのクエリを作成してみました。この試みの目標は、Elasticsearch®で「stocksインデックスから2017年の最初の10件のドキュメントを抽出してください」のような文を用いて検索することです。 この実験から、いくつか制約はあるものの、それが可能であることがわかりました。本記事では、今回の実験と、このユースケース向けに公開したオープンソースライブラリについて解説します。

ChatGPTはElasticsearch DSLクエリを生成できるのか?

この実験は、まず、ChatGPTがElasticsearch DSLクエリを生成できるかどうかに焦点を当てたいくつかのテストからスタートします。このようなテスト対象の場合、検索するデータの構造に関するコンテキストをChatGPTに提供する必要があります。

Elasticsearchでは、データはインデックスに格納されます。インデックスはリレーショナルデータベースの"テーブル"に似たものです。複数のフィールドとそれぞれのタイプがマッピングによって定義されます。そのため、クエリを実行するインデックスのマッピング情報を提供する必要があります。それにより、ChatGPTはクエリをElasticsearch DSLに変換するために必要なコンテキストを取得できます。

Elasticsearchには、インデックスのマッピングを取得するためのGET mapping APIが用意されています。今回の実験では、こちらで入手できるstocksインデックスデータセットを使用しました。このデータセットには、2013年2月から2018年2月までの5年間のFortune 500企業の株価が含まれています。

実験では、データセットが含まれたCSVファイルの最初の5行が出力されました。

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

各行には、株の取引日(date)、その日の始値(open)高値(high)安値(low)終値(close)、取引された株の出来高(volume)、最後に株の銘柄(name)が含まれています。上は、American Airlines Group Inc.(AAL)の例です。

stocksインデックスに関連付けられたマッピングは次のようになります。

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

これに対して、GET /stocks/_mapping APIを使用することで、Elasticsearchからマッピングを取得できます。

[関連記事:ChatGPTとElasticsearch:OpenAIによるプライベートデータへの対応]

確認するためのプロンプトを作成しよう

人間の言葉で表されたクエリをElasticsearch DSLに変換するには、ChatGPTに与える適切なプロンプトを見つける必要があります。これは工程の中で最も難しい部分です。正しい質問形式(つまり、適切なプロンプト)を使用してChatGPTを実際にプログラムする必要があるためです。

いくらかの試行錯誤の末、かなりうまく機能しそうな次のようなプロンプトを作成できました。

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}{query}はプレースホルダーです。それぞれ、マッピングJSON文字列(例:前のサンプルのGET /stocks/_mappingによって返されたもの)と、人間の言葉で表されたクエリ(例:2017年の最初の10件のドキュメントを返してください)に置き換えます。

もちろん、ChatGPTにも限界があり、質問に答えられない場合があります。これは、ほとんどの場合、プロンプトで使用されている文が大まかすぎるか曖昧であることが原因だとわかりました。この状況を解決するには、より詳しい情報でプロンプトを強化する必要があります。このようなプロセスはイテレーションと呼ばれます。プロンプトで使用する適切な文を定義するには、何度か試してはやり直す作業が必要です。

ChatGPTによってElasticsearch DSLクエリの検索文をどのように変換できるかを試したい場合は、dsltranslate.comを利用できます(SQLの変換も試せます)。

すべてを集約

OpenAIが提供するChatGPT APIとマッピングや検索用のElasticsearch APIを使用して、今回の実験のすべてをPHPの実験ライブラリとしてまとめました。

このライブラリでは、以下のAPIでsearch()関数を公開しています。

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

ここで、$indexは使用されるインデックス名、$promptは人間の言葉で表されたクエリ、$boolはキャッシュを使用するためのオプションパラメーター(デフォルトで有効)です。

この関数のプロセスを次の図に示します。

ElasticsearchとOpenAIの図

入力はindexprompt(左端)です。indexはElasticsearchからマッピングを取得するために使用されます(GET mapping APIを使用).それによって得られるのは、JSON形式のマッピングです。後続のAPIコードでChatGPTに送信するクエリ文字列を作成する際に使用されます。この実験では、コード内で変換できるOpenAIのgpt-3.5-turboモデルを使用しました。

ChatGPTからの結果には、Elasticsearchへのクエリに使用するElasticsearch DSLクエリが含まれています。そうして、結果(result)がユーザーに返されます。Elasticsearchへのクエリの実行には、公式のelastic/elasticsearch-phpクライアントを使用しました。

応答時間を最適化し、ChatGPT APIの使用コストを抑えるために、ファイルに基づくシンプルなキャッシュシステムを使用しました。実験では、キャッシュを使用して次のことを行いました。

  • Elasticsearchから返されたマッピングJSONを格納する:このJSONを、インデックスに基づいて名前を付けたファイルに格納しています。これにより、Elasticsearchを再度呼び出すことなく、マッピング情報を取得できます。
  • ChatGPTによって生成されたElasticsearch DSLを格納する:生成されたElasticsearch DSLをキャッシュするために、使用するプロンプトのハッシュ(MD5)を使ってキャッシュファイルに名前を付けました。このアプローチにより、同じクエリに対して以前に生成されたElasticsearch DSLを再利用できるため、ChatGPT APIを再度呼び出す必要がなくなります。

また、getLastQuery()関数を使用してプログラムに従ってElasticsearch DSLを取得できる機能も追加しました。

財務データで実験を行う

上記の実験ではこちらで出力されたstocksの値を格納するのにElastic Cloudを使用しました。具体的には、シンプルな一括スクリプトを使用してCSVのstocksファイルを読み取り、Bulk APIを使用してそれをElasticsearchに送信しました。

Elastic CloudのセットアップとAPIキーの取得の方法については、こちらのドキュメントをご覧ください。

stocksインデックスを格納できたところで、簡単なPHPスクリプトを使用して、英語で書いたクエリをテストしました。使用したスクリプトはexamples/test.phpです。

このexample/test.phpスクリプトを実行するには、3つの環境変数を設定する必要があります。

  • OPENAI_API_KEY:OpenAIのAPIキー
  • ELASTIC_CLOUD_ENDPOINT:ElasticsearchインスタンスのURL
  • ELASTIC_CLOUD_API_KEY:Elastic CloudのAPIキー

stocksマッピングを使用して、次のようなクエリをテストし、すべての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"
            }
          }
        }
      ]
    }
  }
}

ご覧のように、結果はかなり良好です。最後のcloseフィールドとopenフィールドの違いに関するテストの結果は、非常に印象的でした。

すべてのリクエストは、自然言語で表現された質問を的確に反映した、有効なElasticsearch DSLクエリに変換されています。

あなたが普段話す言語を使ってみよう!

さまざまな言語で質問を指定できることが、ChatGPTの優れた点です。

つまり、このライブラリを使用して、イタリア語、スペイン語、フランス語、ドイツ語などのさまざまな自然言語でクエリを指定できます。

以下に例を挙げます。

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

上記のすべての検索で同じ結果が得られ、次のElasticsearchクエリが(程度の違いはあれ)生成されます。

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

重要:ChatGPTは英語向けに最適化されているLLMです。つまり、英語で入力されたクエリを使用した場合に最良の結果が得られます。

LLMの限界

残念ながら、ChatGPTとLLMは、一般に、意味論的な観点から回答の正確さを検証することはできません。提示される回答は、統計的な観点から見て正しいと思われるものです。つまり、ChatGPTによって生成されたElasticsearch DSLクエリが自然言語によるクエリの正しい変換であるかどうかをテストすることはできないのです。

言うまでもなく、これは現時点では大きな制約です。数学演算など他のいくつかのユースケースでは、ChatGPTのWolfram Pluginのような外部プラグインを使用して正確性の問題を解消することができます。このケースでは、ChatGPTの結果は、数学記号モデルを使用して応答の正確性をチェックするWolframエンジンを使用します。

正確面の制約は、ChatGPTの回答には常にチェックが必要であることを意味していますが、それとは別に、Elasticsearch DSLクエリ内の人間の文章を変換する機能にも限界があります。

たとえば、前出のstocksデータセットを使用して、次のように質問するとします。

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

ChatGPTによって生成されたDSLクエリは無効となり、次のElasticsearchエラーが発生します。

日付フィールド[2015-01-01]を形式[yyyy]で解析できませんでした。

日付の形式に関する明らかな曖昧さを取り除き、より具体的な情報を使用して文を言い換えれば、次のように正しい回答を得ることができます。

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

原則として、人間の文章そのままではなく、Elasticsearch DSLとして何を期待するかを説明する記述を使用して文を表現する必要があります。

まとめ

この記事では、自然言語による検索文をElasticsearch DSLクエリに変換するためのChatGPTの実験的なユースケースを紹介しました。OpenAI APIを使用してクエリを内部で変換するためのシンプルなライブラリをPHPで作成し、キャッシュシステムも用意しました。

回答の正確さには限界があるものの、実験の結果は期待を感じさせるものです。とはいえ、ChatGPTやますます人気が高まっている他のLLMモデルを使用して、自然言語でElasticsearchにクエリを実行できる可能性については、今後も調査を続けていくつもりです。

ElasticsearchとAIの可能性についてさらに詳しくはこちらをご覧ください



このブログ記事では、それぞれのオーナーが所有・運用するサードパーティの生成AIツールを使用している可能性があります。Elasticはこれらのサードパーティのツールについていかなる権限も持たず、これらのコンテンツ、運用、使用、またはこれらのツールの使用により生じた損失や損害について、一切の責任も義務も負いません。個人情報または秘密/機密情報についてAIツールを使用する場合は、十分に注意してください。提供したあらゆるデータはAIの訓練やその他の目的に使用される可能性があります。提供した情報の安全や機密性が確保される保証はありません。生成AIツールを使用する前に、プライバシー取り扱い方針や利用条件を十分に理解しておく必要があります。  

Elastic、Elasticsearch、および関連するマークは、米国およびその他の国におけるElasticsearch N.V.の商標、ロゴ、または登録商標です。他のすべての会社名および製品名は、各所有者の商標、ロゴ、登録商標である場合があります。