🙆

【AWS】OpenSearchでハイブリッド検索をお試し

2024/02/03に公開

OpenSearchでハイブリッドクエリのスコア正規化がサポートされたとのことなのでそのお試しです。
それ以前でハイブリッド検索を行う場合はキーワード検索とベクトル検索それぞれのクエリを投げて結果を自前で合算してスコアを算出する必要があったようですが、これでシンプルに実装できるようになったようです。

OpenSearchはOSSなのでそちらで使う方法もありますが、設定が面倒くさそうなので今回はAWSを使用します。

OpenSearchでハイブリッド検索をお試し

以下今回のハイブリッド検索概要

  • キーワード検索の形態素解析はkuromojiを使用
  • ベクトル検索はk-NN近傍
  • テキストのベクトル化(embedding)はgoogle colab上でOpenAI APIを使用して行いました。
    • テキストのベクトル化自体も取り込みパイプライン(OpenSearchのニューラル検索?)として設定することも可能ですが、embeddingモデルに関する知識がなかったためOpenAIに頼っています。
  • 動作確認は基本的にOpenSearchダッシュボードのDevToolsで行っています。
    • ベクトルのembedding部分はgoole colab上で行っています。
    • ハイブリッド検索をgoogle colab上でpythonを使って行おうとしたのですが、上手く動作しなかったため

AWS側でOpenSearchの設定を行う

IAMユーザ作成

ここで作成したIAMユーザーはgoogle colab経由でデータ登録する場合に使用します。
OpenSearchにアクセスするためのIAMユーザopensearch-userを作成し、ポリシーAmazonOpenSearchServiceFullAccessをアタッチします。

後ほど使うのでユーザーのARNを控えアクセスキーの作成をしておいてください。

OpenSearchドメイン作成

動作確認用なのでできるだけ費用がかからないように設定します。
以下設定の一覧ですが、項目数が多いのでデフォルトそのままの項目は記載を省いています。

  • ドメイン名: opensearch-domain
  • ドメインの作成方法: 標準作成
  • テンプレート: 開発/テスト
  • デプロイオプション: スタンバイが無効のドメイン
  • アベイラビリティーゾーン: 1-AZ
  • エンジンオプション
    • バージョン: 2.11
  • データノード
    • Instance family - new: General Purpose
    • インスタンスタイプ: t3.small.search
    • ノードの数: 1
  • 専用マスターノード: 非チェック
  • ネットワーク
    • ネットワーク: パブリックアクセス
    • IP address type - new: デュアルスタックモード
  • きめ細かなアクセスコントロール
    • きめ細かなアクセスコントロールを有効化: チェック
    • マスターユーザーの作成
      • マスターユーザー名: opensearch-admin
        • ※ 1~16文字となっていますが、16を超えても保存自体はできてしまうので注意。もし16文字を超えて登録してしまったら再度マスターユーザーの作成を行なってください。
      • マスターパスワード: 任意のパスワード
  • アクセスポリシー: きめ細かなアクセスコントロールのみを使用

これでドメインを作成します。
ここで作成したマスターユーザーはOpenSearchダッシュボードにログインする際に使用します。

OpenSearchダッシュボードにログインする

ドメインの「一般的な情報」に記載されている「OpenSearch Dashboards URL」をブラウザで開くとダッシュボードのログイン画面が出てくるので、Usernameにopensearch-adminと設定したパスワードを入力してください。

最初ログインするとtenantを選択するポップアップが出てきますが、PrivateでOKです。業務で使う場合にダッシュボードの内容を共有するためにグローバルを設定することもあるかもです。

ダッシュボードには「Dev Tools」というものがあって検索や登録を比較的簡単に試せます。今回も基本はDevToolsで確認しています。(ベクトル検索で使うベクトルが長すぎるのでデータ登録に関してはgoogle colabを使用してpythonで行いました。)

IAMユーザーをOpenSearchの内部マスターユーザー追加する

先ほど作成したIAMユーザーでOpenSearchにアクセスできるようにIAMユーザーをOpenSearchの内部マスターユーザーに追加します。
これでこのIAMユーザーのアクセスキーを使用してAPI経由でOpenSearchにアクセスできるようになります。
※ その他に良い方法があると思いますが、今回やるのは動作確認程度なので一旦これで良しとします。

作成したドメインの「セキュリティ設定」タブに行き「編集」を押します。
そこで「きめ細かなアクセスコントロール」の「マスターユーザーとしてIAM ROLEを設定する」を選択し、その下のIAM ARNに最初に作成したIAMユーザーのARNを設定し、変更を保存します。

ハイブリッド検索を試す

ベクトル部分が長すぎるためデータ投入はgoogle colabで行っています。DevToolsのクエリも書いていますが、ベクトル部分は省略しています。
また、google colabでの各操作を実装していますが、一番重要なハイブリッド検索が上手く動作していません。検索パイプラインを指定しても上手く効いていないようです。

(ハイブリッド検索以外はgoogle colabで行うのが良いかも)

google colabファイル

このcolabを使用する場合は以下をcolabのシークレットに設定してください。

  • IAMユーザーのアクセスキー
    • AWS_ACCESS_KEY_ID
    • AWS_SECRET_ACCESS_KEY
  • OpenSearchのドメインエンドポイント(IPv4)
    • OPENSEARCH_ENDPOINT
  • OpenAIのAPIキー
    • OPENAI_API_KEY

https://colab.research.google.com/drive/1q8_FsluMLht6qLq-YqRNeVYlTtd2Kgkx?usp=sharing

index作成

indexを作成しつつsettingsとmappingsも定義します。

  • settings
    • index.knnをtrueにするとindexに対してベクトル検索ができるようになる
  • mappings
    • propertiesとして以下2つを定義
      • keyword
        • 型をtextにしキーワード検索ができるようにします。analyzerはkuromojiを指定。
      • vector
        • 型をknn_vectorにしベクトルとして定義。次元数は1536
PUT my_hybrid_index
{
  "settings": {
    "index.knn": true
  },
  "mappings": {
    "properties": {
      "keyword":  {
        "type": "text",
        "analyzer": "kuromoji"
      },
      "vector": {
        "type": "knn_vector", 
        "dimension": 1536
      }
    }
  }
}

検索パイプラインの設定

簡単にハイブリッド検索をするためには必須の検索パイプラインを設定します。
キーワード検索とベクトル検索の重みを0.5ずつにしているの2つの検索の平均を元にスコアリングされます。

PUT /_search/pipeline/hybrid-search-pipeline
{
  "description": "Pipeline for hybrid search",
  "phase_results_processors": [
    {
      "normalization-processor": {
        "normalization": {
          "technique": "min_max"
        },
        "combination": {
          "technique": "arithmetic_mean",
          "parameters": {
            "weights": [
              0.5,
              0.5
            ]
          }
        }
      }
    }
  ]
}

事前データを登録

事前データとしてChatGPTに適当に作成してもらった日本の文化などに関するテキストを登録します。ベクトルの部分にはOpenAIのembeddings APIで生成したベクトルを設定します。
ベクトルを記載すると行数が多くなりすぎるので[]のみで省略していますが、以下のような形式でデータを登録できるはずです。

POST _bulk
{ "index": { "_index": "my_hybrid_index", "_id": "1" } }
{ "keyword": "寿司が好きです。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "2" } }
{ "keyword": "日本の桜は美しいです。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "3" } }
{ "keyword": "東京は活気がある都市です。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "4" } }
{ "keyword": "京都には多くの神社があります。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "5" } }
{ "keyword": "日本料理は世界的に人気があります。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "6" } }
{ "keyword": "富士山は日本の象徴的な山です。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "7" } }
{ "keyword": "日本のアニメは多くの国で愛されています。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "8" } }
{ "keyword": "相撲は日本の伝統的なスポーツです。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "9" } }
{ "keyword": "日本の歴史は非常に長いです。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "10" } }
{ "keyword": "日本語は独特な表現が多い言語です。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "11" } }
{ "keyword": "日本の夏は非常に暑いです。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "12" } }
{ "keyword": "冬には北海道でスキーが楽しめます。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "13" } }
{ "keyword": "日本の映画産業は成長しています。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "14" } }
{ "keyword": "漫画は日本文化の重要な部分です。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "15" } }
{ "keyword": "日本の教育システムは厳しいと言われています。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "16" } }
{ "keyword": "日本の技術革新は目覚ましいものがあります。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "17" } }
{ "keyword": "日本の公共交通機関は非常に便利です。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "18" } }
{ "keyword": "日本の伝統芸能は世界的にも珍しいです。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "19" } }
{ "keyword": "日本の茶文化は深い歴史を持っています。", vector: []}
{ "index": { "_index": "my_hybrid_index", "_id": "20" } }
{ "keyword": "日本の春は花見で賑わいます。", vector: []}

キーワード検索

適当にキーワード検索

GET /my_hybrid_index/_search
{
  "_source": ["keyword"],
  "query": {
    "match": {
      "keyword": {
        "query": "伝統文化が気になるね",
        "analyzer": "kuromoji"
      }
    }
  }
}

以下が検索にひっかかるはず。伝統や文化というキーワードで検索されていますね。

日本の伝統芸能は世界的にも珍しいです。
日本の茶文化は深い歴史を持っています。
漫画は日本文化の重要な部分です。
相撲は日本の伝統的なスポーツです。

ベクトル検索

適当にベクトル検索。sizeを5にしているので5件取得できるはずです。
※ベクトルは省略

GET my_hybrid_index/_search
{
  "size": 5,
  "query": {
    "knn": {
      "vector": {
        "vector": [],
        "k": 5
      }
    }
  }
}

以下が検索にひっかかるはず。

日本の春は花見で賑わいます。
日本の桜は美しいです。
日本の夏は非常に暑いです。
冬には北海道でスキーが楽しめます。
日本のアニメは多くの国で愛されています。

ハイブリッド検索

ハイブリッド検索は先ほど作成した検索パイプラインを使用します。
※ベクトルは省略

GET /my_hybrid_index/_search?search_pipeline=hybrid-search-pipeline
{
  "size": 5,
  "_source": ["keyword"],
  "query": {
    "hybrid": {
      "queries": [
        {
          "match": {
            "keyword": {
              "query": "伝統文化が気になるね",
              "analyzer": "kuromoji"
            }
          }
        },
        {
          "knn": {
            "vector": {
            	"vector": [],
            	"k": 5
            }
          }
        }
      ]
    }
  }
}

以下が検索にひっかかるはず。なんとなくキーワード検索とベクトル検索が合わさった感じになってます。

相撲は日本の伝統的なスポーツです。
日本の春は花見で賑わいます。
漫画は日本文化の重要な部分です。
日本の桜は美しいです。
日本の夏は非常に暑いです。

最後に

google colabでのハイブリッド検索が上手く動作しないのが何故かよく分からない
opensearch-pyのコード軽く読んでみたけどよく分からない
無念

メモ

  • 返却される値はDevTools上で?search_pipeline=hybrid-search-pipelineを外して実行した時と同じ
  • キーワード検索やベクトル検索に使用しているclient.searchにsearch_pipelineが無い(ライブラリがまだ未対応?)
  • 代替手段としてclient.transport.perform_requestを使用したが、こちらも上手く動作せず
    • 誤った検索パイプライン名を指定した場合はillegal_argument_exceptionが返却されるのでクエリパラメータ自体は渡っていそう

参考

Discussion