VISASQ Dev Blog

ビザスク開発ブログ

Elasticsearch と On Your Data を使った RAG の実現

「GPT-4o」が5月にリリースされて、この記事を書く準備をしていたら、数日前に「GPT-4o mini」がリリースされて、「マジ!?」 となっている今日この頃

リモート勤務と電気代の天秤に絶賛悩んでいる 検索チームの tanker です。 (一応補足すると、会社から一律 5千円のリモート勤務手当が出ています)

Azure OpenAI の On Your Data で Elasticsearch が利用可能に

Azure OpenAI で RAG (※) を実現するための機能として On Your Data があります。 (※ 信頼性の高い内部のデータを使って回答を生成する仕組み)

従来は、Azure 上のリソース上に置いたデータを対象としていましたが、外部の Elasticsearch に置いたデータについても preview 版ですが サポートするようになりました。 www.elastic.co

ということで、実際に試してみました。

実行結果

  • Elasticsearch には、過去の相談テーマをベクトルデータとして格納
  • 投げた質問(プロンプト)は以下の通りです
    • 「ビル清掃に関連する課題を教えてください」
  • 返ってきた回答(choices[0].message.content )の内容を整形したのが以下の通りです
    • [doc1] 等は choices[0].message.context.citations 配列に対応しています

結果を見るに「ビル」もしくは「清掃」に関係しそうなトピックを Elasticsearch 上から探して回答文章を作ってくれました。 1~4 の トピックについては元データ内から引用している一方で、各トピックと「ビル清掃の課題」との関係性を説明する文章(例えば、導入コストの話とか)は元データには含まれていないので生成AIによって創作されています。 (回答の創造性の程度に関しては、プロンプトなどで調整可能かと思います)

ビル清掃に関連する課題について、以下のような点が挙げられます。

1. **ロボットの活用**:
   - ビル清掃におけるロボットの活用は進んでいますが、まだ課題が残っています。例えば、ロボットの導入コストやメンテナンス、操作の習熟度などが挙げられます [doc1]。
2. **業務体制**:
   - xxxxxx  [doc2]。
3. **内装デザイン**:
   - xxxxxx  [doc3][doc5]。
4. **第三者管理方式**:
   - xxxxxx  [doc4]。

これらの課題に対して、適切な対策を講じることが求められます。

Python コード

access_token = DefaultAzureCredential().get_token(
    "https://cognitiveservices.azure.com/.default",
)
client = AzureOpenAI(
    api_version=api_version,
    api_key=access_token.token,
    azure_endpoint=endpoint,
)
chat_completion = client.chat.completions.create(
    model=gpt4_deployment_name,
    messages=[{"role": "user", "content": question}],
    extra_body={
        "data_sources": [
            {
                "type": "elasticsearch",
                "parameters": {
                    "endpoint": es_endpoint,
                    "index_name": es_index_name,
                    "authentication": {
                        "type": "encoded_api_key",
                        "encoded_api_key": es_encoded_api_key,
                    },
                    "query_type": "vector",
                    "embedding_dependency": {
                        "deployment_name": embedding_deployment_name,
                        "type": "deployment_name",
                    },
                    "fields_mapping": {
                        "vector_fields": ["embedding_title"],
                        "title_field": "title",
                        "content_fields": ["title", "questions.original"],
                        "filepath_field": "id",
                        "url_field": "id",
                    },
                }
            }
        ]
    }
)
print(chat_completion.model_dump_json())

Azure OpenAI からのレスポンス

{
  "id": "77826f5c-b353-40e4-917a-764b414738a1",
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "logprobs": null,
      "message": {
        "content": "(長すぎるので省略)",
        "role": "assistant",
        "function_call": null,
        "tool_calls": null,
        "end_turn": true,
        "context": {
          "citations": [
            {
              "content": "xxxxxxxxxx",
              "title": "xxxxxxxxxx",
              "url": "555555",
              "filepath": "555555",
              "chunk_id": "0"
            },
            {
              "content": "xxxxxxxxxx",
              "title": "xxxxxxxxxx",
              "url": "4444444",
              "filepath": "4444444",
              "chunk_id": "0"
            },
            {
              "content": "xxxxxxxxxx",
              "title": "xxxxxxxxxx",
              "url": "111111",
              "filepath": "111111",
              "chunk_id": "0"
            },
            {
              "content": "xxxxxxxxxx",
              "title": "xxxxxxxxxx",
              "url": "222222",
              "filepath": "222222",
              "chunk_id": "0"
            },
            {
              "content": "xxxxxxxxxx",
              "title": "xxxxxxxxxx",
              "url": "3333333",
              "filepath": "3333333",
              "chunk_id": "0"
            }
          ],
          "intent": "[\"ビル清掃の課題\", \"ビル清掃に関連する問題\", \"ビル清掃のチャレンジ\"]"
        }
      }
    }
  ],
  "created": 1721358626,
  "model": "gpt-4o",
  "object": "extensions.chat.completion",
  "system_fingerprint": "fp_abc28019ad",
  "usage": {
    "completion_tokens": 368,
    "prompt_tokens": 3910,
    "total_tokens": 4278
  }
}

実行するためにやったこと

システム全体像

事前準備として、 Elastic Cloud (Elasticsearch) に検索ベクトルデータを格納します。

response = client.embeddings.create(
    model=embedding_deployment_name,
    input=text,
)
return response.data[0].embedding

上記のコードを使い、Azure OpenAI 側に1つずつ案件テーマを渡し生成された数字配列のベクトルデータをElasticsearch に格納します。ちなみに、ベクトルデータを入れるフィールドは、以下のように専用の型で定義しておく必要があります。

"embedding_title": {"type": "dense_vector", "dims": 1536, "similarity": "cosine"}

dims に関しては、今回使った text-embedding-3-small の次元数に合わせています

learn.microsoft.com

Azure接続情報

環境変数」として以下の情報をアプリケーション側に渡す必要があります (引数として渡すことはおそらく無理)。

また、Azure 外でアプリケーションを動かす場合は、サービスプリンシパルの登録も必要です。 learn.microsoft.com

ES接続情報

ESの接続情報を渡す方法はいくつかありますが、今回は API Keys を使う方式を採用しました。

  • ※ Elastic Cloud の deployment管理の方にも API Keys はありますが、ここでは Kibana で設定できるもの
  • ※ ベクトルデータのインデキシングも 本API Keys で行う場合は privileges が不足しているので適宜加えてください
{
  "superuser": {
    "cluster": [],
    "indices": [
      {
        "privileges": [
          "read",
          "view_index_metadata"
        ],
        "names": [
          "test_with_embedding"
        ],
        "allow_restricted_indices": true
      }
    ],
    "metadata": {},
    "transient_metadata": {
      "enabled": true
    },
    "run_as": [
      "*"
    ],
    "applications": []
  }
}

以下の様に encoded_api_key の値を得ることができます。

余談その1: ベクトル検索時のエラー

Azure OpenAI から以下のエラーが返ってくることがありました。 この場合は、 fields_mapping.vector_fields (ベクトルデータが入っているフィールド) が定義されていないのが問題でした。 (最初に示したクエリーは修正済み)

openai.BadRequestError: Error code: 400
An error occurred when calling the Elasticsearch API:
 Invalid Elasticsearch configuration detected:
   Please assign a proper column/field for vector search. It should be of type dense_vector

余談その2: キーワード検索とベクトル検索

Elasticsearch を使った On Your Data では 今回使ったベクトル検索とは別にキーワード検索も利用できます (query_typesimple にすると利用できる)。ただし、精度はお世辞にもよいとは言えませんでした。

Elasticsearch の slowlog を使って実際に投げられたクエリーを見てみると、choices[0].message.content.intent にある 「ビル清掃の課題」「ビル清掃に関連する問題」「ビル清掃のチャレンジ」という検索キーワードで Elasticsearch に複数のクエリーを投げているだけなので期待するものがヒットしませんでした。

{
  "size": 10,
  "query": {
    "multi_match": {
      "query": "ビル清掃の課題",
      "fields": [
        "title^1.0"
      ]
    }
  },
  "highlight": {
    "pre_tags": [
      "<HIGH>"
    ],
    "post_tags": [
      "</HIGH>"
    ],
    "fragment_size": 2048,
    "number_of_fragments": 1000,
    "fields": {
      "title": {}
    }
  }
}

余談その3: Tools 機能との使い分け

Elaticsearch と Azure OpenAI を組み合わせた機能であれば従来からある tools 機能を使うこともできます。

learn.microsoft.com

レスポンスの finish_reason を考慮した分岐など、アプリケーション側で世話する範囲は増える一方で、細かな Elasticsearch のクエリー制御もできます。またElasticsearch の認証情報を Azure OpenAI に渡さなくてよいので、パッと見たときの安全性も高いと思います。

www.elastic.co

他にも、上記公式ブログで説明があるように Elasticsearch では nested field 以下に dense_vector フィールドを設定できるので、長い文章を適切なチャンクで分割して Elasticsearch に格納することで、ヒットした doc の中で特にベクトル検索的にスコアが高いセンテンスを特定することができます (なぜそのドキュメントがヒットしたのかのヒントになる)。

tools を使った場合の流れを文章で書くと以下の様になります (※ Expert検索は、エキスパート登録したユーザ情報を検索できるシステムのこと)。

  1. 「表記ゆれを考慮して、コンプラに詳しい人を探して」 (tools パラメータで Expert検索の存在を伝える)
  2. Expert検索システムを使ってください。検索キーワードは「コンプライアンス」「法令遵守」「内部統制」「コンプラ」「compliance」です
  3. {"query_string": "コンプライアンス OR 法令遵守 OR 内部統制 OR コンプラ OR compliance"}
  4. (ESの検索結果)
  5. ( 1 と 2 のメッセージの流れ配列の末尾に整形したESの検索結果をつけて、Azure OpenAIに返す)
  6. コンプライアンス(compliance)関連の専門知識を持つ方として以下の2名が見つかりました……」

On Your Data のキーワード検索の時とは異なり、妥当な検索キーワードを提案してくれます (プロンプトの問題なのかどうかは不明)。なので、キーワード検索をしたい場合は On Your Data よりは tools を使う方がよさそうです。