はじめに
Elasticsearchではバージョン8.17からSparseベクトルを生成するモデルをElandからアップロードし、Denseベクトルと同様にベクトル検索に利用することができるようになりました。この記事ではまずSparseベクトルの概要を説明してから、実際にElasticsearchを使ってSparseベクトルによるセマンティック(意味)検索を実装する方法について紹介します。
ライセンス
この記事の内容を試すにはElasticsearchのPlutinam以降、あるいはTrialライセンスが必要です。
ベクトル検索の種類
現在、キーワード検索に代わるインテリジェントな検索手法としてベクトル検索が広く利用されるようになってきました。単語単位の転置インデックスを走査することですばやく目的のドキュメントを見つける従来のTF/IDFベースの手法(BM25など)と異なり、ベクトル検索とは入力テキストを数値の列(ベクトル)に変換しておき、クエリーのベクトルとの幾何学的な近さ(角度や距離など)で、より意味的に近いドキュメントを見つける手法です。
そんなベクトル検索ですが、実は実用化されているベクトル検索の実装には以下の二つの種類があります。(これ以外にもあるかもしれませんが、ここでは議論しません。)
- 密ベクトル(Dense vector)を使った検索
- 疎ベクトル(Sparse vector)を使った検索
通常、ベクトル検索という場合には前者の密ベクトル(Denseベクトル)を使った検索を意味していることがほとんどだと思います。Elasticsearchで密ベクトルを使った日本語検索を実現する方法については以下で紹介しています。
しかし疎ベクトル(Sparseベクトル)を使った検索については、特に日本語での情報はあまり聞かれません。疎ベクトル(Sparseベクトル)による検索とはどんなものなのでしょうか。
(この記事では以降、Elasticsearch内での機能の呼び方に合わせてDenseベクトルおよびSparseベクトルと英名で表記します。)
Sparseベクトルとは何か
前述の通り、ベクトルにはDenseベクトルとSparseベクトルという二つの表現方法があるわけですが、これらはどのように違うのでしょうか。学術的な詳細は私の手に余るので、プラグマティックに解説したいと思います。
あるテキストを特定のルールで一定の長さの数値の配列に変換するとき、その出力された数値の配列のことDense vectorと言います。またこの変換処理のことを通常embed(埋め込み)と呼びます。以下、イメージコードです。
embed("Dense vectorとは何か")
# => [2,5,2,7,8,2,3...]
配列のそれぞれの要素は、何らかの特徴量を表していると考えることができます(通常N番目の要素が何の特徴量を表しているかは、ユーザーから見ると不明です)。入力されたテキストの意味内容を、決められた長さの特徴量の列に圧縮しているわけですね。この例では入力テキストは非常に短いですが、長いテキストを入力しても同じ次元のベクトルが出力されます。
それに対してSparseモデルは、無数にありうる意味成分のうち、そのテキストが持っている成分のみを抽出します。とにかく例を見てみましょう。
expand("Sparse vectorとは何か")
# => {
"用語": 0.57665896,
"て": 0.57438034,
"語": 0.4170748,
"技術": 0.103720225,
...
}
見てわかる通り、キーワードに対してそのスコア、という形で出力されます。また同時に分かるようにここでリストアップされるキーワードは、元のテキストに含まれているとは限りません。利用したモデルが、このテキストに対して評価し、その入力から推測されるキーワード展開してスコアをつけています。このキーワードの方はモデルが持っている語彙の分だけ成分としてはありうるはずですが、そのうちスコアをつけたものだけが出力され、それ以外は全て0のスコアという意味になります。したがって出力された要素以外の全ての要素の値はゼロであるという、「疎」なベクトルが出力されたわけですね。
Elasticsearchで使えるSparseモデル: ELSER
元々Sparse vectorによる検索はNaverが開発したモデルであるSPLADEによって広まったようです。
SPLADEは特に英語では高い関連性のスコアを発揮することがわかっています。そこでElasticもこのモデルの論文をベースに独自にELSERというモデルを開発しました。
このELSERは極めて高性能なモデルで、Elasticではセマンティック検索を実装する際のファーストチョイスとして推奨しています。
現在ではElasticsearchにはこのELSERのv2がバンドルされているため、Plutinam以降(あるいはTrial)のライセンスを使っている場合はすぐに使い始めることができます。ただし、このELSERは英語でトレーニングされているため、英語に対してしか適切な性能を発揮することができません。
日本語によるSparseベクトル検索
これまでElasticsearchでSparseベクトル検索を実装する際はELSERを利用することがほとんどでした。そして上述の通りELSERは英語以外に対応していません。したがってこれまで日本語などの非英語圏のベクトル検索といえばDenseベクトルを用いる方法が取られてきました。
しかしElasticsearch 8.17から、SparseモデルもDenseモデルと同様に外部からアップロードできるようになりました。これには以下の条件を満たす日本語のモデルも含まれます。この制限はテキストのトークナイズに関連するもので、Dense vectorのモデルと同様です。
- BERTモデルであること
- tokenizer_config.jsonファイルのword_tokenizer_typeの値がmecabであること
以前のポストも参考にしてください。
この条件に該当してElasticsearchで利用できることを確認しているSparseベクトルモデルには、例えば以下がありました。
hotchpotch/japanese-splade-v2
aken12/splade-japanese-v3
これらのモデルを利用することで、Elasticsearch上でSparseベクトルを使ったセマンティック検索を実装することができるようになります。
SparseベクトルをElasticsearchの外部で生成し、Elasticsearchにsparse_vector/rank_featureとしてインデックスする方法であれば、以前からSparseベクトルによる検索はRank feature queryを使って実装可能でした。8.17から可能になったのは、MLモデル自体をElasticsearchにアップロードし、Elasticsearch上でエンべディングを実行することです。
日本語Sparseベクトル検索の実装
それでは実際にElasticsearchで日本語Sparseベクトルを使った検索を実装してみましょう。基本的な利用方法はELSERと全く同じです。ELSERでのチュートリアルは以下を参照してください。モデル名の部分を読み替えれば、ほぼそのまま他のSparseモデルでも同じ手順になります。
ただしELSERはElasticsearchにバンドルされていますが、それ以外のSparseモデルはElandを使ってマニュアルでElasticsearchにアップロードする必要があるところが異なります。
Sparseモデルのアップロード
Denseモデルと同様、選択したモデルをElasticsearch上で動かしてエンべディングに利用するには、そのモデルをElandというツールを使ってHugging FaceからElasticsearchにアップロードする必要があります。また、SparseモデルについてはElandの方もバージョンが8.17以降である必要があります。
まずはElandをインストールします。
$ pip install fugashi
$ pip install unidic_lite
$ pip install 'eland[pytorch]'
テキストのSparseベクトルへの展開はtext_expansion
というタスクタイプになります(このタスクタイプがサポートされるのがEland 8.17からです)。
elandをインストールすると利用できるようになるeland_import_hub_model
コマンドでHugging FaceにあるモデルをElasticsearchにアップロードします。
$ eland_import_hub_model \
--url "https://your.elasticserach" \
--es-api-key "your_api_key" \
--hub-model-id aken12/splade-japanese-v3 \
--task-type text_expansion \
--start
ところで、現在テキストをSparseベクトルに変換するタスクのことをtext_expansion
と呼んでいますが、今後sparse_embedding
に変更になるかもしれません。以下のIssueで管理されています。
マッピングの作成
Sparseベクトルはsparse_vector
型か、あるいはrank_features
型としてインデックスされる必要があります。ここではわかりやすいsparse_vector
型の例で説明します。
PUT sparse-test
{
"mappings": {
"properties": {
"content_embedding": {
"type": "sparse_vector"
},
"content": {
"type": "text"
}
}
}
}
Ingest Pipelineを使ったSparseベクトルの展開
Elasticsearchへのデータ投入時には、入力されたテキストをSparseベクトルに変換する必要があります。この処理はIngest PipelineでInference Processorを使って行います。
この例では、content
フィールドに入力されたテキストデータを、先ほどアップロードしたモデルであるaken12__splade-japanese-v3
を利用してSparseベクトルにエンベッドしてそれをcontent_embedding
フィールドに保存するパイプラインを定義しています。
PUT _ingest/pipeline/sparse-test-pipeline
{
"processors": [
{
"inference": {
"model_id": "aken12__splade-japanese-v3",
"input_output": [
{
"input_field": "content",
"output_field": "content_embedding"
}
]
}
}
]
}
データ投入時はこのパイプラインを通せば、content_embedding
フィールドにSparseベクトル表現の値が展開されます。
POST sparse-test/_doc/1?pipeline=sparse-test-pipeline
{
"content": "Elasticsearchへのデータ投入時には、入力されたテキストをSparseベクトルに変換する必要があります。この処理はIngest PipelineでInference Processorを使って行います。"
}
単にベクトル化を試したい場合はInfer trained model APIが利用できます。
POST _ml/trained_models/aken12__splade-japanese-v3/_infer
{
"docs": [{"text_field": "Elasticsearchへのデータ投入時には、入力されたテキストをSparseベクトルに変換する必要があります。この処理はIngest PipelineでInference Processorを使って行います。"}]
}
レスポンスは以下の通り。
{
"inference_results": [
{
"predicted_value": {
"は": 7.6684365,
"に": 7.377538,
"の": 7.1618214,
"が": 6.860925,
...
}
}
]
}
検索
検索時にはSparse vector queryを利用します。クエリーに対してもSparseベクトルにエンベッドする必要があるため、Ingest Pipelineと同様に利用するモデルを指定する必要があります。
GET sparse-test/_search
{
"query":{
"sparse_vector":{
"field": "content_embedding",
"inference_id": "aken12__splade-japanese-v3",
"query": "セマンティック検索のためのデータ投入"
}
}
}
終わりに
ElasticsearchでELSER以外のSparseベクトル検索を実装する方法について紹介しました。検索の精度はモデルのエンべディング結果に完全に依存しますが、これまでの密ベクトル以外の選択肢も出てきたのでぜひ試してみてください。
また今回はエンべディング結果をsparse_vectorとして利用していますが、これをfeature rankとして捉えれば、アイディア次第では入力されたテキストの特徴量抽出の一つの機能として利用することもできそうです。新しい利用方法も併せて考えてもらえると嬉しいです。
Trouble Shooting
Elasticsearchのバージョンが8.17未満の場合、以下のエラーが発生します。Elasticsearchのバージョンを8.17以降にあげてください。
elasticsearch.BadRequestError: BadRequestError(400, 'x_content_parse_exception', 'text expansion models must be configured with BERT tokenizer, [bert_ja] given')
またElandが8.17未満の場合、以下のエラーが発生します。同様にElandのバージョンを8.17以降にあげてください。(エラーメッセージ的にはtext_expansion
がサポートされるように出力されていますがバグです。)
TypeError: Unknown task type text_expansion, must be one of: fill_mask, ner, pass_through, question_answering, text_classification, text_embedding, text_expansion, text_similarity, zero_shot_classification