semantic_textを使ってElasticsearch 8.15以降で簡単にベクトル検索を実装する

# はじめに

ベクトル検索は目的のドキュメントをキーワードマッチ以外の意味的な近さで見つけたり、テキスト以外のマルチモーダルな画像や音声などの情報の検索に使ったりといったことが可能で、特にここ１・２年は生成AIと組み合わせたRAGというユースケースの登場で非常に広く利用されるようになりました。ただElasticsearchでベクトル検索を利用する場合、最初のセットアップをするまでに必要な工程が煩雑であることは否めません。ベクトル検索の詳しい手順は他の記事で書いていますのでそちらを参照してください。

https://qiita.com/daixque/items/931b8be343075b835097

そこでElasticsearchではバージョン8.15からsemantic_textというフィールド型を導入しました。このsemantic_textを利用するとベクトル検索のセットアップが格段に容易になりますので、この記事ではその方法を紹介したいと思います。

https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html

:::note warn
バージョン8.17の時点で、`semantic_text`フィールドはbetaです。また利用にはInference APIの利用が必要で、そのためEnterprise Licenseが必要です。[参照](https://www.elastic.co/subscriptions)
:::

# semantic_textフィールドの特徴

`semantic_text`フィールドは、テキストデータに対するセマンティック（意味）検索実現のために特化したフィールドタイプです。これまでベクトル検索を利用してセマンティック検索を実現しようと思うと、インデックスに対して`dense_vector`フィールドを定義し、利用するモデルが出力する次元数のベクトルを保存できるようにセットアップするなどの、比較的複雑な設定が必要でした。この定義にはある程度利用するMLモデルがどのようなものであるかの技術的な知識が求められます。しかし`semantic_text`フィールドを使うと、Inference Endpoint経由でどのMLモデル（エンドポイント）を利用するかを決めるだけで、適切な形でテキストをembeddingし、インデックスに保存したり検索したりすることができます。

また、それぞれのモデルは１回でembeddingできるテキストの大きさに上限があり、大きなテキストを扱うにはチャンキングという、テキストを小さなチャンクに分割するテクニックを独自に実装する必要がありました。しかし`semantic_text`フィールドを利用すると、このチャンキングもElasticsearchが自動で行ってくれるようになります。

# 実装の流れ

`semantic_text`フィールドを利用した検索機能実装の流れは以下のようになります。

1. Inference Endpointを作成
1. Index定義
1. データ投入
1. 検索実行

このように書くとほとんど通常の検索と同じで実装できそうなことがわかりますね。それぞれ見ていきましょう。

# Inference Endpointの作成

Elasticsearchの上でテキストデータをベクトルデータにembeddingするには、適切なMLモデルをElasticsearchのMLノードにデプロイする必要があります。具体的に利用したいモデルがHugging Faceで公開されている場合は、そのモデルをElandを使ってまずElasticsearchにアップロードする必要がありますが、ここではElasticがデフォルトでサポートしているE5モデルを利用することにします。E5モデルは日本語を含む多言語に対してembeddingをすることのできる大変便利なモデルです。Hugging Faceからのモデルアップロードについては[こちらの記事](https://qiita.com/daixque/items/931b8be343075b835097)を参照してください。

モデルがElasticsearchにデプロイされたら、[Create inference API](https://www.elastic.co/guide/en/elasticsearch/reference/current/infer-service-elasticsearch.html)を利用してInference Endpointを作成します。

```
PUT _inference/text_embedding/my-e5-model
{
  "service": "elasticsearch",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1,
    "model_id": ".multilingual-e5-small" 
  }
}
```

これで`my-e5-model`という名前のエンドポイントが作成されました。試しにこのエンドポイントを使って日本語テキストをembeddingしてみましょう。

```
POST _inference/text_embedding/my-e5-model
{
  "input": "試しにこのエンドポイントを使って日本語テキストをembeddingしてみましょう。"
}
```

すると以下のような結果が得られるはずです。

```
{
  "text_embedding": [
    {
      "embedding": [
        0.09948257,
        -0.040724847,
        -0.01904885,
        (...)
      ]
    }
  ]
}
```

Inference APIの詳細については以下の記事も参照してください。

https://qiita.com/daixque/items/df699e8a2b318d593128

上記の記事ではチャンキングの動作の詳細についても説明しています。

# Index定義とデータの投入

エンドポイントが作成できたら、これを使って`semantic_text`フィールドを定義します。今回は単に"content"というフィールドを持つだけのドキュメントを扱うことにしましょう。

Index定義は以下のようになります。

```
PUT semantic_text_test
{
  "mappings": {
    "properties": {
      "content": {
        "type": "text",
        "copy_to": "content_semantic"
      },
      "content_semantic": {
        "type": "semantic_text",
        "inference_id": "my-e5-model"
      }
    }
  }
}
```

`content_semantic`フィールドに`semantic_text`フィールドタイプを適用しています。したがってこの`content_semantic`フィールドを利用してセマンティック検索が実現できます。このフィールドに対して直接データを投入しても良いのですが、そうすると元の本文の情報が失われてしまうため、まずはtext型の`content`フィールドに対してデータをIngestして、そこからcopy_toを使ってテキストデータを`content_semantic`フィールドにコピーしています。

実際にデータを投入してみましょう。

```
POST semantic_text_test/_doc/1
{
    "content": """# はじめに
ベクトル検索は目的のドキュメントをキーワードマッチ以外の意味的な近さで見つけたり、テキスト以外のマルチモーダルな画像や音声などの情報の検索に使ったりといったことが可能で、特にここ１・２年は生成AIと組み合わせたRAGというユースケースの登場で非常に広く利用されるようになりました。
    """
}
```

内容を確認します。

```
GET semantic_text_test/_search
```

結果

```json
{
  "hits": {
    "hits": [
      {
        "_index": "semantic_text_test",
        "_id": "1",
        "_score": 1,
        "_source": {
          "content_semantic": {
            "inference": {
              "inference_id": "my-e5-model",
              "model_settings": {
                "task_type": "text_embedding",
                "dimensions": 384,
                "similarity": "cosine",
                "element_type": "float"
              },
              "chunks": [
                {
                  "text": """# はじめに
ベクトル検索は目的のドキュメントをキーワードマッチ以外の意味的な近さで見つけたり、テキスト以外のマルチモーダルな画像や音声などの情報の検索に使ったりといったことが可能で、特にここ１・２年は生成AIと組み合わせたRAGというユースケースの登場で非常に広く利用されるようになりました。
    """,
                  "embeddings": [
                    0.01843889,
                    -0.04999397,
                    -0.049595766,
                    -0.042462178,
                    0.08888733,
                    (...)
                  ]
                }
              ]
            }
          },
          "content": """# はじめに
ベクトル検索は目的のドキュメントをキーワードマッチ以外の意味的な近さで見つけたり、テキスト以外のマルチモーダルな画像や音声などの情報の検索に使ったりといったことが可能で、特にここ１・２年は生成AIと組み合わせたRAGというユースケースの登場で非常に広く利用されるようになりました。
    """
        }
      }
    ]
  }
}
```

embedding結果が`chunks`という配列に含まれていることがわかります。適切にチャンキングされた上でembeddingされてインデックスされているようですね。

# 検索

`semantic_text`フィールドに対する検索には[semanticクエリー](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html)が利用できます。

```
GET semantic_text_test/_search
{
  "query" : {
    "semantic": {
      "field": "content_semantic", 
      "query": "イメージを見つける方法" 
    }
  }
}
```

とても自然な書き方なので、この記事で初めてElasticsearchのベクトル検索を実装する方には何の驚きもないかもしれないですが（それは良いことなのですが）、これまではベクトル検索が実現できるようになったときにはknnクエリーを使う必要があり、外部でembeddingした数値の配列を渡したり、あるいはここでクエリーをembeddingするためのモデル名を書いたりする必要があったことを考えると、かなりAPIも洗練されてきたように感じます。

ちなみに上記と同等のクエリーをknnで書くと以下のようになります。

```
GET semantic_text_test/_search
{
  "query": {
    "nested": {
      "path": "content_semantic.inference.chunks",
      "query": {
        "knn": {
          "field": "content_semantic.inference.chunks.embeddings",
          "query_vector_builder": {
            "text_embedding": {
              "model_id": "my-e5-model",
              "model_text": "イメージを見つける方法"
            }
          }
        }
      }
    }
  }
}
```

# おわりに

この記事では`semantic_text`フィールドを使った検索について紹介しました。これまで比較的設定の複雑だったベクトル検索が、かなり簡単に利用できるようになったことがわかると思います。Elasticsearch 8.15以降で利用できますので、ぜひ活用してください。

ただ、生成AIの影響でNLP周りの機能についてはElasitcsearchでも活発に開発が続いています。`semantic_text`フィールドも8.16の時点でまだbetaです。新しいバージョンでは新機能が追加されたり、より効率的な実装・設定方法が提供されたりすることがありますので、ぜひElastic Search labsブログなどをウォッチしてキャッチアップしてください。

https://www.elastic.co/search-labs