Elasticのセマンティック検索をMultilingual E5でやってみた

こちらのElastic公式ブログで、Elasticsearchでセマンティック検索（ベクトル検索）を行う方法が紹介されています。

https://www.elastic.co/jp/blog/elasticsearch-nlp-ja

上のブログでは、日本語の文章を数値列にエンベッド（ベクトル化）するためのモデルとして `cl-tohoku/bert-base-japanese-v2` をHugging Faceからインポートして使用していますが、この記事では Elastic 8.12 から Built-in NLP model となり、Elasticsearchインストール後すぐに使える `multilingual-e5-small` (正確には、linux-x86_64にオプティマイズされた `.multilingual-e5-small_linux-x86_64` を使用) を使用して同等のことをしてみます。

## セマンティック検索を実行するまでの流れ

参照ブログでは「セマンティック検索を実行するまでの流れ」は、次のようになっていますが、今回 Built-in NLP model を使うため、1と2のステップは省略できます。ただし、`.multilingual-e5-small_linux-x86_64` モデルのStateが `Deployed` になっていない場合は、`Add trainde model` からdownload後、startしておく必要があります。

> Elasticsearchでセマンティック検索を実行する場合、以下のようなステップを踏む必要があります。
>  1.  （事前準備）作業端末へのElandおよび関連ライブラリのインストール
>  2.  自然言語処理タスクを実現するための機械学習モデルのインポート
>  3.  インポートした機械学習モデルでのテキスト分析結果のインデキシング
>  4.  機械学習モデルを利用したkNN検索

## モデルの確認

参照ブログと同様のモデルの確認作業を `.multilingual-e5-small_linux-x86_64` で実施してみます。

> BertなどのNLPタスクでは、入力されたテキストを単語レベルで分割するpre-tokenizeという処理が行われます。この際に日本語のpre-tokenizeに利用されるのが、日本語形態素解析エンジンです。Elasticsearch v8.9では、MeCabによる形態素解析をサポートしています。Hugging FaceのモデルページからFiles and versionsタブを開き、tokenizer_config.jsonファイルの内容を確認してください。ここでword_tokenizer_typeの値がmecabとなっていることを確認してください。


[Huggingface上のintfloat/multilingual-e5-smallのtokenizer_config.json](https://huggingface.co/intfloat/multilingual-e5-small/blob/main/tokenizer_config.json)
```
{
  "bos_token": "
",
  "clean_up_tokenization_spaces": true,
  "cls_token": "
",
  "eos_token": "
",
  "mask_token": {
    "__type": "AddedToken",
    "content": "<mask>",
    "lstrip": true,
    "normalized": true,
    "rstrip": false,
    "single_word": false
  },
  "model_max_length": 512,
  "pad_token": "<pad>",
  "sep_token": "
",
  "sp_model_kwargs": {},
  "tokenizer_class": "XLMRobertaTokenizer",
  "unk_token": "<unk>"
}
```

### XLMRobertaTokenizer
リリースノートによると、Elasticsearch v8.9.0 で xlm_roberta Tokenized モデルがサポートされたようです。
> Add support for xlm_roberta tokenized models #94089

https://www.elastic.co/guide/en/elasticsearch/reference/8.9/release-notes-8.9.0.html


Kibana の Machine Learning > Trained Models にて、`.multilingual-e5-small_linux-x86_64` の Config を確認すると、tokenizationとして `xlm_roberta` が使用されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2164968/c2f4ee70-4dc3-e4a9-d59c-536d6b9928dd.png)
Actions から、`Test model` を実行してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2164968/b3f0433b-45b4-7d34-7cb8-7aa6a46042d6.png)
このモデルを使って入力された日本語のテキストが数値列にベクトル化できています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2164968/8f0d304f-9509-f77b-ac61-954401f23902.png)

## ベクトル埋め込みを利用したセマンティック検索の実装 

このステップから、モデルの変更にともなう最低限の変更のみをして、参照ブログと同じことを実行します。

### パイプラインの作成

`.multilingual-e5-small_linux-x86_64` モデルを使って（注：model_id を変更しています）、インデックスに投入される前に日本語テキストをベクトル化するinferenceプロセッサーを含むパイプラインを作成します。

```
PUT _ingest/pipeline/japanese-text-embeddings
{
  "description": "Text embedding pipeline",
  "processors": [
    {
      "inference": {
        "model_id": ".multilingual-e5-small_linux-x86_64",
        "target_field": "text_embedding",
        "field_map": {
          "title": "text_field"
        }
      }
    }
  ],
  "on_failure": [
    {
      "set": {
        "description": "Index document to 'failed-<index>'",
        "field": "_index",
        "value": "failed-{{{_index}}}"
      }
    },
    {
      "set": {
        "description": "Set error message",
        "field": "ingest.failure",
        "value": "{{_ingest.on_failure_message}}"
      }
    }
  ]
}
```

### インデックスの作成

パイプラインが登録できたら、これを利用してインデックスを作成します。作成するインデックスにはベクトルを保存するフィールドが必要なため、適切にmappingを定義しておきます。以下の例では text_embedding.predicted_value というフィールドに384次元（注：768次元から変更しています）のdense_vector（密ベクトル）型のデータを保持できるように設定しています。
```
PUT japanese-text-with-embeddings
{
  "mappings": {
    "properties": {
      "text_embedding.predicted_value": {
        "type": "dense_vector",
        "dims": 384,
        "index": true,
        "similarity": "cosine"
      }
    }
  }
}
```
### ドキュメントの登録

作成したパイプラインを指定して直接ドキュメントを登録します。

```
POST japanese-text-with-embeddings/_doc?pipeline=japanese-text-embeddings
{
  "title": "日本語のドキュメントをベクトル化してインデックスに登録する。"
}
```

### 機械学習モデルを利用したkNN検索の実行
query_vector_builderで、model_id として `.multilingual-e5-small_linux-x86_64` を指定し、model_textで指定したテキストをエンベッドしたベクトルをクエリーに変換します。

```
GET japanese-text-with-embeddings/_search
{
  "knn": {
    "field": "text_embedding.predicted_value",
    "k": 10,
    "num_candidates": 100,
    "query_vector_builder": {
      "text_embedding": {
        "model_id": ".multilingual-e5-small_linux-x86_64", 
        "model_text": "日本語でElasticsearchを検索したい"
      }
    }
  }
}
```
以下のようなレスポンスを得られます。`.multilingual-e5-small_linux-x86_64` を使用して、セマンティック検索を実行できました。

```
{
  "took": 36,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.92906845,
    "hits": [
      {
        "_index": "japanese-text-with-embeddings",
        "_id": "yhBagpQBf7FxvlQBU7U4",
        "_score": 0.92906845,
        "_source": {
          "title": "日本語のドキュメントをベクトル化してインデックスに登録する。",
          "text_embedding": {
            "predicted_value": [
              0.06868297606706619,
              -0.028167251497507095,
              -0.004564001690596342,
              -0.020321227610111237,
              :
              (略、384次元のベクトルが表示される)
            ],
            "model_id": ".multilingual-e5-small_linux-x86_64"
          }
        }
      }
    ]
  }
}
              
```

## 環境
Elasticsearch 8.15.0