ベクトルフィールドを使ったテキスト類似性検索

レシピ検索エンジンとして誕生した瞬間から、Elasticsearchは高速でパワフルな全文検索エンジンとして設計されていました。そんなルーツもあり、テキスト検索の改善は、今もElasticの日々の業務のベクトルにおいて重要なモチベーションです。Elasticsearch 7.0では、高次元ベクトル向けのフィールドタイプを試験導入し、今回7.3のリリースでは、高次元ベクトルのドキュメントスコアリング向けの使用のサポートを開始しました。

本ブログ記事は、“テキスト類似性検索”と呼ばれる特定のテクニックを取り上げます。このタイプの検索では、ユーザーが短い自由文クエリを入力し、そのクエリへの類似度に基づいて、ドキュメントが順位付けされます。テキスト類似性は、幅広いユースケースに役立てることができます。

  • Q&A:よくある質問と回答のコレクションから、ユーザーが入力した内容と類似の質問を見つける。
  • 論文検索:研究論文のデータベースで、ユーザーのクエリとの関連性が高いタイトルの論文を返す。
  • 画像検索:キャプション付きの画像データセットで、ユーザーの説明に類似したキャプションを持つ画像を見つける。

ストレートなアプローチで類似性検索を行う場合、クエリと共有する語の数によってドキュメントを順位付けするという手法を考えることができます。しかし、共通の単語が非常に少なくても、ドキュメントがクエリに類似している可能性もあります。つまり、構文と意味論的なコンテクストを考慮する、類似性についてのより強固な観念が必要です。

自然言語処理(NLP)のコミュニティでは、テキスト埋め込みと呼ばれるテクニックの開発が行われてきました。これは、単語と文章を数的ベクトルでエンコード化する技術です。こうしたベクトル表現はテキスト中の言語学的コンテンツをとらえる目的で設計されており、クエリとドキュメントの間の類似性を評価するために使用することができます。

本ブログ記事では、類似性検索をサポートする目的で、テキスト埋め込みとElasticsearchのdense_vectorタイプを使う方法を探索します。はじめに埋め込み技術の概要について説明し、後半では、Elasticsearchを使った類似性検索のシンプルなプロトタイプをご紹介します。

補足: 検索にテキスト埋め込みを使う手法は複雑で、発展途上の分野です。本ブログ記事が、探索のきっかけを提供できれば幸いです。しかしこの記事は、特定の検索アーキテクチャーや実装を推奨するものではありません。

テキスト埋め込みとは?

さまざまなタイプのテキスト埋め込みについて詳しく検討し、従来型の検索アプローチと比較してみましょう。

テキスト埋め込み

テキスト埋め込みモデルは、単語を密な数ベクトルとして表現します。これらのベクトルは、単語のセマンティックな(意味論上の)プロパティを捉えるためのものです。単語のベクトルが相互に近ければ、セマンティックな意味でも類似している、ということになります。すぐれた埋め込み技術において、ベクトル空間の各方向性は、その単語の異なる側面に結び付いています。たとえば、“カナダ”のベクトルは、ある方向性では“フランス”に近い可能性があり、別の方向性では“トロント”に近いかもしれません。

NLPと検索のコミュニティは、このような語のベクトル表現に長年関心を寄せてきました。この数年で、多くの従来型のタスクがニューラルネットワークを使って再考されたことに伴い、テキスト埋め込みへの関心も再び盛り上がっています。word2vecGloVeをはじめ、単語埋め込みアルゴリズム開発の成功例も現れています。これらアプローチでは大規模なテキストの集合を活用し、各単語が出現するコンテクストを調査して、そのベクトル表現を判断しています。

  • word2vec Skip-gramのモデルはニューラルネットワークを教育し、ある文章中の単語の周囲にあるコンテクストを予測します。ネットワーク内の重みが、単語の埋め込みを提供します。
  • GloVeでは、単語の類似性を、別のコンテクストの単語と共に出現する頻度によって決定します。このアルゴリズムは、単語が共起した回数について、シンプルで直線的なモデルを教育します。

多くの研究グループがWikipediaやコモン・クロールなどの大規模なテキストコーパスで事前教育を行ったモデルを開発し、ダウンストリームのタスク向けにダウンロード、およびプラグインできる便利な形で配布しています。事前教育済みのバージョンはたいていそのまま使われますが、特定のデータセットやタスクにフィットさせるための、モデル調整が役立つこともあります。多くの場合、事前教育済みモデルを簡単に微調整するだけで、モデルを修正できます。

単語埋め込みは非常に堅牢かつ効果的であることが証明されています。現在、機械翻訳や感情分類などのNLPタスクでは、個別のトークンに代わり、単語埋め込みを使うことが一般的となっています。

文章埋め込み

研究者たちの間では最近、単語だけでなく、より長いテキストの断片を埋め込む技術に注目が集まっています。最新のアプローチは、複雑なニューラルネットワークアーキテクチャーに基づいており、また場合によって、セマンティックな情報の捕捉を支援するための教育に分類済みデータを組み込むこともあります。

教育を終えたモデルは、文章を取り込んでから、そのコンテクスト中の各単語に対してだけでなく、その文章全体に対しても、ベクトルを生成することができます。単語埋め込み同様に、現在多くの事前教育済みモデルのバージョンが流通しており、ユーザーは費用のかかる教育プロセスをスキップすることが可能です。教育は、非常にリソースインテンシブなプロセスとなることがありますが、生成されるモデルははるかに軽量です。典型的な文章埋め込みモデルは、リアルタイムアプリケーションの一部に使用できるほど高速です。

一般的な文章埋め込み技術の一例として、InferSentや、Universal Sentence Encoder, ELMoBERTがあります。単語と文章の埋め込みは、研究が活発に行われている領域です。将来さらに強力なモデルが開発されることも十分に期待できます。

従来型の検索アプローチとの比較

従来、情報を取得する一般的な方法は、テキストを数的ベクトルで表し、語彙中の各単語に1つの次元を割り当てるというものでした。部分的なテキストベクトルは、その語彙中に各単語が出現する回数に基づいて決定されます。このテキスト表現の手法はしばしば「Bag of Words」と呼ばれます。文章構造に関わらず、単純に語の発生回数を数えるためです。

テキスト埋め込みは、いくつかの重要な点において、このような従来型のベクトル表現とは異なっています。

  • エンコードされたベクトルは密で、比較的低次元であり、たいていは100から1,000次元の範囲で分布します。対照的に、Bag of Wordsのベクトルは疎で、50,000超もの次元からなる可能性もあります。アルゴリズムの埋め込みは、セマンティックな意味のモデル化の一環で、テキストをより低次の空間にエンコードします。理想的には、この新しいベクトル空間で、同義語的な単語やフレーズが、最終的に類似の表現となります。
  • 文章埋め込みは、ベクトル表現を決定する際、語の順番を考慮することができます。たとえば、"tune in"というフレーズは、"in tune"とは異なるベクトルとしてマップされる可能性があります。
  • 実際には、文章埋め込みがテキストのより大きな節をうまく一般化しないことは少なくありません。一般的に、短いパラグラフよりも長いテキストの表現には使われていません。

類似性検索に埋め込みを使う

たとえばここに、多数の質問と回答の集合があります。ユーザーは質問することができ、私たちはこの集合から類似の質問を取得することで、ユーザーが回答を見つけるお手伝いをしたい、という状況だとします。

類似の質問を取得するために、テキスト埋め込みを使うことができます。

  • インデックス中、各質問に文章埋め込みモデルを通過させて、数的ベクトルを生成する。
  • ユーザーがクエリを入力すると、クエリは同じ文章埋め込みモデルを通過し、ベクトルが生成される。応答を順位付けするため、各質問とクエリのベクトルの類似性を計算する。埋め込みベクトルを比較する際は、コサイン類似度を使うことが一般的です。

これをElasticsearchで実行したシンプルな例が、こちらのレポジトリにあります。メインのスクリプトが、StackOverflow datasetの20,000個未満の質問をインデックスします。その後、ユーザーはこのデータセットに対して自由文のクエリを入力することが可能になります。

このスクリプトの各部の詳細についてこの後実際にお見せしますが、はじめにサンプルの結果を見てみましょう。このメソッドは多くのケースで、クエリとインデックスされた質問の間に強力に重なり合う単語がない場合でも、類似性を捉えることができます。たとえば次のような結果になります。

  • "zipping up files"(ファイルのzip化)で、"Compressing / Decompressing Folders & Files"(圧縮/フォルダーとファイルの圧縮)が返される
  • "determine if something is an IP"(IPかどうか判断する)で、"How do you tell whether a string is an IP or a hostname"(あるストリングがIPまたはホスト名であるか見分ける)が返される
  • "translate bytes to doubles"(バイトをdoubleに置き換える)で、"Convert Bytes to Floating Point Numbers in Python"(Pythonでバイトを浮動小数点数に変換する)が返される

実装

このスクリプトを開始するには、まずダウンロードして、TensorFlowに埋め込みモデルを作成します。この例ではGoogleのUniversal Sentence Encoderを採用していますが、他の埋め込みメソッドを使用することも可能です。今回のスクリプトでは、追加の教育や微調整は行わず、埋め込みモデルをそのまま使います。

次にElasticsearchインデックスを作成します。このインデックスには、質問のタイトル、タグ、ベクターとしてエンコードされた質問のタイトルのマッピングが含まれます。

"mappings": {
  "properties": {
    "title": {
      "type": "text"
    },
    "title_vector": {
      "type": "dense_vector",
      "dims":512
    }
    "tags": {
      "type": "keyword"
    },
    ...
  }
}

dense_vectorのマッピングで、このベクトルに含まれる次元の数を指定するよう求められます。title_vectorフィールドをインデックスする際、Elasticsearchはこのフィールドがマッピングで指定された数と同じ数の次元を持っているか確認します。

ドキュメントをインデックスするには、質問のタイトルに埋め込みモデルを通過させ、数値配列を取得します。この配列は、ドキュメントのtitle_vectorフィールドに追加されます。

ユーザーがクエリを入力すると、そのテキストははじめに同じ埋め込みモデルを通過し、query_vectorというパラメーターに格納されます。Elasticsearch 7.3より、cosineSimilarity関数がElasticsearchネイティブスクリプト言語で提供されています。そこで、ユーザーのクエリと類似性に基づいて質問を順位付けするため、script_scoreクエリを使用します。

{
  "script_score": {
    "query": {"match_all": {}},
    "script": {
      "source": "cosineSimilarity(params.query_vector, doc['title_vector']) + 1.0",
      "params": {"query_vector": query_vector}
    }
  }

新しいクエリが来るたびにスクリプトが再コンパイルされるのを回避するため、クエリベクトルはかならずスクリプトパラメーターとして渡します。Elasticsearchは負のスコアを許容しないため、かならずコサイン類似度に1.0を追加する必要があります。

補足:本ブログ記事は元々の執筆時に、Elasticsearch 7.3が提供していたベクター関数向けの別種の構文を使用していましたが、同構文は7.6で廃止されました。

重要な制約

script_scoreクエリは、制約的なクエリをラップし、返されるドキュメントのスコアを修正するよう設計されています。しかしこの事例ではmatch_allクエリを使っています。つまり、スクリプトはインデックスの全ドキュメントに対して実行されます。この点は現在、Elasticsearchでベクトル類似性を実施する上での制約となっています。ベクトルはドキュメントのスコアリングに使うことはできますが、最初の取得の手順で使うことはできません。ベクトル類似性に基づく取得のサポートは、進行中の作業の中で重要課題に位置付けられています。

すべてのドキュメント全体をスキャンするプロセスを回避して高速なパフォーマンスを保つために、match_allクエリを、より選択的なクエリに替えることができるでしょう。取得に使用する適切なクエリは、個別のユースケースに応じて異なることがあります。

上には有望な事例をいくつか挙げていますが、一方で、ノイズが多く含まれたり、直感的でない結果になることがあるといった点の注意喚起も重要です。たとえば、"zipping up files"(ファイルのzip化)は"Partial .csproj Files"(部分的な.csprojファイル)や"How to avoid .pyc files?"(.pycファイルを回避する方法)にも高いスコアを割り当てています。このメソッドがびっくりするような結果を返すとき、その問題をデバッグする方法はかならずしも常に明確ではありません。各ベクトルコンポーネントの意味はしばしば不明瞭であったり、解釈可能な概念に相当しなかったりします。語の重複に依存する従来型のスコアリング技術の場合は、たいてい「なぜこのドキュメントが高い順位になっているか」という問いにもっと簡単に答えることができます。

上記にご説明した通り、このプロトタイプはベクターフィールドに埋め込みモデルを使用する手法の一例として示されており、運用段階のソリューションとして提示されているものではありません。新たな検索ストラテジーを開発する際は、そのアプローチが実際のデータにもたらすパフォーマンスをテストすることが不可欠です。かならず、マッチクエリなどの確固たるベースラインと比較する必要があります。確かな成果を達成するには、ターゲットのデータセット向けに埋め込みモデルを微調整する対応や、ワードレベルでのクエリ拡張のように、埋め込みを異なる方法で組み込む対応を検討するなど、ストラテジーを大きく変更する必要がある場合もあります。

まとめ

埋め込み技術は、テキストの断片に含まれる言語学的コンテンツをとらえるパワフルな方法です。埋め込みのインデキシングとベクトル距離に基づくスコアリングを実行すれば、“類似性”という、語の重複にとどまらない発想でドキュメントを順位付けることが可能になります。

今後は、ベクトルフィールドタイプに基づく関連機能を導入してゆく予定です。検索におけるベクトルの活用は、重要であり、さまざまな見方がある領域です。いつものように、私たちはみなさまから、ユースケースやエクスペリエンスについての投稿をGithubおよびディスカッションフォーラムでお待ちしています。