Embeddingを用いたOpenSearchでのベクトル検索

# はじめに
使いやすさと普及度が高い**OpenSearch**を用いたベクトル検索に焦点を当て、まずはベクトル検索や、テキストや他の複雑なデータをベクトル表現に変換する**Embedding**について解説します。次に、ベクトル検索のindexを生成する際に活用するEmbeddingのモデルに関してそれぞれの用途別で解説します。最後にOpenSearchでのベクトル検索のIndex構築・実行などについて書いていきます。

# ベクトル検索とは
**ベクトル検索**は、データを**ベクトル空間内の点**として表現し、点間の**距離や類似性**を計算して関連性の高い結果を見つけ出す手法です。従来のキーワードベースの全文検索とは異なり、データ間の**抽象的な類似性**を考慮できるため、意味的な関連性を反映した検索が可能です。

ベクトル検索の基本的な仕組みは、**最近傍探索**（Nearest Neighbor Search）として知られる最適化問題に基づいています。具体的には、コサイン類似度、内積計算、ユークリッド距離、ハミング距離などを使ってデータ間の距離を計算し、最も近しいデータを探索します。特に大規模なデータセットに対しては、検索速度を向上させるために**近似最近傍探索**（Approximate Nearest Neighbor Search, ANN）などの高速化技術が使われています。

具体的な計算方法や実装に興味がある方は、**最近傍探索**や**近似最近傍探索**といったキーワードで検索すると、アルゴリズムや実装に関する詳細な情報を得ることができると思います。


# Embeddingとは
**Embedding**は、テキストや音声などの**非構造化データ**を**実ベクトル空間にマッピングし、数値的なベクトル表現に変換する技術**です。これにより、データの意味的・文脈的な関係性や類似性を数値として表現できるようになります。代表的な機械学習モデルには**Word2Vec**や**BERT**があり、これらは単語や文、音声といったデータを高次元ベクトルとして表現し、データ同士の関係をベクトル空間上で捉えます。このプロセスによって生成されるベクトルは**埋め込み表現**と呼ばれ、単に**Embedding**とも言われます。

データが単に点として配置されるだけでなく、その点が空間内で他のデータとの意味的な関係を反映するように配置されるため、「**埋め込み**」という言葉は、外部から利用可能な形でデータが空間内に配置されるというプロセスをうまく表現していますね。


### 具体例:
　単語("king")　⇒　embedding([0.25, -0.75, 1.2, 0.5, -0.4, ...])


### 特徴:
- **多次元データの次元削減**: 高次元のデータ（例えば、単語や文章など）を低次元の実数値ベクトルに変換します。これによりデータの処理が容易になり、計算資源の節約が可能になります。

- **意味的類似性の捕捉**: 単語や文のEmbeddingは、意味的な類似性を数値的に表現します。単語Embeddingでは、学習の結果、意味的に似た単語がベクトル空間内で近い位置に配置されます。

# Embeddingモデルの選定
ベクトル検索において、意味的類似性の捕捉の観点から適切なEmbeddingモデルの選定をすることは重要です。以下に選定の基本的な流れについて書いていきます。

### 選定の流れ

1. **タスクの特性**：ベクトル検索のターゲットとなるデータの種類（例えば、単語、文章、文書など）と、求められる言語処理のレベル（単語レベルの意味理解、文脈的理解、複雑な文書理解など）を考慮する。
　　

2. **リソースと制約**: 利用可能な計算リソースや処理速度の要件を考慮し、APIなどの外部で処理をするのか、自前でモデルをデプロイするのかを決めます。
　　

3. **データの種類と量**: 使用するデータセットの特性（例えば、専門性、多様性、量）に基づいて、適切と思われるモデルを選択します。
　　

4. **実験と評価**: 前の段階で用意した異なるモデルを実際のデータセットに適用し、そのパフォーマンスを評価します。

1〜3については、各機械学習モデルの特性を理解する必要があります。次にそれぞれの機械学習モデルのベクトル検索での適している場面、長所・短所など特性について軽く書いておきます。参考にしてください。

### モデルの特性

- **Word2Vec**:基本的な単語レベルの類似性を検索する場合に適しています。計算効率が良く、小規模なデータセットで有効です。ただし、文脈依存性を捉えられないため、意味の多様性を扱うには限界があります。
　　

- **GloVe**:単語の共起情報に基づく意味の関連性を捉えるのに適しています。大規模なコーパスからの統計情報を活用できます。BERTなどの最新モデルと比べると文脈理解が限られます。
　　

- **BERT**:高度な文脈依存の言語理解が必要な場合に適しています。深い文脈的理解が可能で、精度の高い検索結果を提供できます。ただし、計算資源を多く消費し、処理が時間を要することがあります。
　　

- **RoBERTa/DeBERTa**:BERTを基にした高度な言語理解タスクに最適化されています。BERTより精度が高く、より広範なデータセットで学習されています。BERTと同様にリソース集約的であり、word2vecなどに比べると計算コストが高いです。
　　

- **SimCSE**:文章レベルの類似性を捉えるのに特化しています。自己教師あり学習による高品質な文脈埋め込みで、精度もBERTより高い場合が多いと思います。ただし、特定のタイプのタスクやデータセットに依存することがあります。

### BERTの異方性について
BERTやその他の深層トランスフォーマーモデルは、**異方性**(anisotropy)の問題を抱えることが知られています[1]。**異方性**とは、埋め込み空間において、ベクトルが特定の方向に集中する傾向を指します。この結果、ベクトル間の意味的な類似性や差異を捉えるのが難しくなり、特にベクトル検索の文脈で問題となることがあります。

- **影響**: 異方性により、異なる単語や文が似たような埋め込みを持つことがあり、これが検索精度の低下につながる可能性があります。
- **対策**: 異方性を緩和するために、特定の事後処理技術や異なる埋め込み方法を採用することが提案されています。例えば、埋め込み空間を正規化することや、より均一な空間分布を持つ他のモデル（例えばSimCSEなど）を利用するなどの対策があります。

BERTベースのモデルをベクトル検索に使用する際は、異方性の問題を認識し、適切な対策を講じることが大事です。実践できる具体的な対策としては、Whitening操作などがあります。以下にpytorchでの実装があるので、BERTの事前学習モデルなどを用意してデプロイする人などは見て実践してみるとより良いかもしれないです。
https://github.com/autoliuweijie/BERT-whitening-pytorch

### その他

BERTよりSimCSEのほうが精度が良いと示す論文[2]もあるので、BERTを使うならいっそSimCSEを使うのが良いかもしれないです。embeddingをAPIでやる場合は、OpenAIのtext-embedding-ada-v2やGoogleのPALMのembeddingで事足りる場合も多いかと思いますが、専門性の高いデータを扱う際は思うようにパフォーマンスが上がらないと言う場合もあると思います。そのような時は上記のモデル特性踏まえてモデルを選定して適宜ファインチューニングしたり、上手く検索ができるようなデータの意味を反映したembeddingの構築ができると良いかもしれません。

[追記]SimpleCSEなど様々な日本語モデルを網羅的に実験・評価した調査もあるみたいなので、自前でembeddingする際はこちらも参考にすると良いかもしれません。

https://arxiv.org/abs/2310.19349

上記の実際の検証結果やコードはこちらにあるみたいです。

https://github.com/hppRC/simple-simcse-ja

# インデックスの構築と実行
Embedding(ベクトル)の生成について話しましたが、どうやってそれを元にベクトル検索の実行するかについて話します。今回は汎用的なベクトル検索エンジンとして、OpenSearchを利用します。OpenSearchにおいては、ベクトル検索を実行するために専用のインデックスを構築する必要があります。以下に、ベクトルインデックス構築の一般的な例と手法について説明します。

### 1. **ベクトルフィールドのマッピング**
OpenSearchでのベクトル検索を行うためには、まずベクトルデータを格納するフィールドをマッピングする必要があります。例えば、次のようにベクトルフィールドを持つインデックスを定義できます。

```json
PUT /my_vector_index
{
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "knn_vector",
        "dimension": 128
      }
    }
  }
}
```
この例では、128次元のベクトルを持つmy_vectorフィールドを定義しています。設定する次元数はEmbeddingのモデルが持つ次元数と同じにしてください。例えばOpenAIのtext-embedding-ada-v2をembeddingに利用する場合は1536次元なので、その通りにすべて同じ次元数を設定してください。

- **k-NN (k Nearest Neighbors) インデックス**
OpenSearchでは、k-NNアルゴリズムを使用して、類似度に基づいて最も近いベクトルを検索します。k-NN検索のためには、インデックス作成時にはknn_vectorタイプを使用します。

- **HNSW (Hierarchical Navigable Small World) インデックス**
HNSWは、効率的な近似最近傍検索アルゴリズムです。OpenSearchでは、HNSWアルゴリズムを使用して、高次元ベクトル空間での近傍探索を高速化します。階層的なグラフ構造を使って高速な検索を実現しますが、インデックス作成には比較的多くのリソースが必要になる可能性があります。OpenSearchではCPU負荷75％を超えるとガベージコレクタが作動して非常に処理が重くなるので、これを超えないように注意してください。以下はHNSWインデックスの例です。methodの部分で指定します。

```json
PUT /my_hnsw_index
{
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "knn_vector",
        "dimension": 128,
        "method": {
          "name": "hnsw",
          "space_type": "cosinesimil",
          "engine": "nmslib",
          "parameters": {
            "ef_construction": 512
          }
        }
      }
    }
  }
}
```
- **Faiss(Facebook AI Similarity Search)インデックス**
Faissは、大規模なデータセットに対して効率的な近似最近傍検索を提供するライブラリです。OpenSearchでは、Faissを使用して高次元ベクトルのインデックスを構築し、類似度検索を行うことができます。Faissは、量子化やパーティショニングなどのテクニックを用いて、大規模データセットに対する検索性能を向上させます。hnswと同じくmethodの部分で指定できます。

```json
"method": {
    "name": "hnsw",
    "space_type": "innerproduct",
    "engine": "faiss",
    "parameters": {
        "ef_construction": 512,
        "m": 40,
    },
}
```

parametersには、インデックス構築時の詳細なパラメータを設定します。
- **ef_construction** : 探索効率を決定するパラメータ。数値が大きいほど、インデックス作成時の精度が高くなりますが、それに伴ってリソースの消費が増加します。
- **m** : HNSWアルゴリズムにおいて、各ノードが保持するエッジ（つながり）の最大数。通常、これはノード間の接続の密度を表し、値が大きいほどインデックス構築に時間がかかりますが、検索精度が向上する可能性があります。

### 2. **インデックスデータの追加**
インデックスが作成されたら、次のようにEmbeddingの生成結果であるベクトルデータをインデックスに追加します。

```json
PUT /my_vector_index/_doc/1
{
  "my_vector": [0.5, 0.6, 0.7, ..., 0.128]
}
```
これにより、ID 1のドキュメントに128次元のベクトルが追加されます。

### 3. ベクトル検索の実行

OpenSearchでのベクトル検索クエリは、使用する検索エンジンによって異なる形式をとることがあります。以下に、cosinesimilとnmslibを使用した場合と、faissを使用した場合のクエリの形式を示します。

#### Cosinesimilを使用した場合
この形式では、cosineSimilarity関数を使用して、クエリベクトルとインデックス内のドキュメントのベクトル間のコサイン類似度を計算します。以下はその例です。

```json
GET /my_vector_index/_search
{
  "query": {
    "script_score": {
      "query": {
        "match_all": {}
      },
      "script": {
        "source": "cosineSimilarity(params.query_vector, doc['my_vector']) + 1.0",
        "params": {
          "query_vector": [0.1, 0.2, 0.3, ..., 0.128]
        }
      }
    }
  }
}
```

#### Faissを使用した場合
Faissを使用した場合、k-NNクエリを直接実行して、最も類似したベクトルを持つ上位k個のドキュメントを返すことができます。以下はその例です。

```json
GET /my_vector_index/_search
{
  "query": {
    "knn": {
      "my_vector": {
        "vector": [0.1, 0.2, 0.3, ..., 0.128],
        "k": k
      }
    }
  },
  "size": k
}
```
ここでvectorには検索するベクトルを入力します。kは返されるドキュメントの数を指定します。
"my_vector"はベクトルデータが格納されているフィールド名です。

# インデックスの調整

インデックスの構築や実行はここまでである程度理解できたと思いますが、HNSWアルゴリズムをインデックス構築に用いた時のパラメータ「ef_construction」と「m」は、調整が必要なことがわかると思います。そこで、調整について少し掘り下げます。

### ef_constructionパラメータの調整：
ef_construction は、インデックス作成時の探索効率と精度を制御するパラメータ。
この値は、インデックス構築時に隣接ノードを探索する際の深さや幅を決定します。

**小さい値**(100~200程度): 計算リソースの消費を抑えたい場合、小さい値を設定します。しかし、これは検索精度の低下を引き起こす可能性があります。
**大きい値**(200~2000程度): より高い精度を求める場合、大きい値を設定します。これにより、より多くの候補ノードが探索され、検索の精度が向上する可能性がありますが、リソースの消費も増加します。
**バランス**: 実際のデータセットと使用環境に応じて、パフォーマンスとリソース消費のバランスを取りながら値を調整します。検索対象10万以下であれば、個人的に512辺りが無難で良いのではないかと思っています。

### mパラメータの調整：
HNSWアルゴリズムにおける各ノードが保持するエッジの最大数を指します。
これはノード間の接続密度を決定し、間接的に検索精度とインデックスのサイズに影響します。

**小さい値**(5~15程度): インデックスのサイズを小さく保ちたい場合や、インデックス構築時間を短縮したい場合は、小さい値を設定します。ただし、これは検索精度を低下させる可能性があります。
**大きい値**(30〜100程度): より高い検索精度を求める場合は、大きい値を設定します。ただし、これによりインデックスのサイズが増加し、構築時間も長くなります。
**バランス**: m の値もデータセットの特性や要求される検索精度、利用可能なリソースに応じて慎重に選択します。検索対象10万以下であれば、個人的に30~40辺りが無難で良いのではないかと思っています。

### 総合的な調整:
**目的の定義**: 検索精度とパフォーマンスのどちらを重視するかを明確にします。
**パラメータの調整**: ef_construction と M の値を変更して、実際のデータを使い異なる構成を試します。
**実験**: 構築時間、メモリ使用量、検索速度、検索精度などの指標を計測します。
**比較**: 異なるパラメータ設定で得られた結果を比較し、最適なバランスを見つけます。
**最適化**: 最もバランスが取れたパラメータ設定を選択します。必要に応じて、パラメータを微調整します。


# 最後に
ベクトル検索技術の領域において、ValdやPineconeのような専門的なソリューションが注目されがちですが、faissやhnswを利用できるなどOpenSearchでも十分にベクトル検索を実現できることが理解できたと思います。さらに、queryの部分にpost_filterやmatchクエリを使用することで、ベクトルの類似度だけでなく、複合条件を組み合わせた検索も可能です。テキストデータとベクトルデータを組み合わせた複雑な検索条件も、OpenSearchでは簡単に扱うことができます。

この点は、ValdやPineconeのような専門的なベクトル検索エンジンと比較しても特筆すべきことだと思います。これらの専門的なソリューションは、高度なベクトル検索機能に焦点を当てていますが、OpenSearchはこれらの機能を、より広範な検索エンジンの文脈で提供できます。AWS上で検索DBを簡単に構築できるため、コストやパフォーマンスの管理も容易です。

OpenSearchはベクトル検索だけでなく、全文検索や複合条件検索にも対応できる柔軟なプラットフォームです。これは、要件が不確かな場合やサービス要求が変化する場合でも対応できるため、多様な検索要件を持つ組織やプロジェクトにとって大きな利点になり得ます。

一方で、ValdやPineconeはベクトル検索を重視し、特定の用途では非常に高いパフォーマンスを発揮します。サービスの要件がベクトル検索に特化している場合、これらのソリューションは良い選択肢になるでしょう。

OpenSearch、Vald、Pinecone、それぞれには独自の強みと制約があり、最適な選択は使用状況と目的によって異なる思います。検索精度、パフォーマンス、スケーラビリティ、操作の容易さを総合的に考慮し、プロジェクトの具体的な要件に基づいて、その時々のプロジェクトに合う検索エンジンを使っていけると良いかもしれないですね！


# 参考文献

［1］Ethayarajh, Kawin. How contextual are contextualized word representations? comparing the geometry of BERT, ELMo, and GPT-2 embeddings. In Proceedings of the 2019 Conference on Empirical Methods in Natural Language Processing and the 9th International Joint Conference on Natural Language Processing (EMNLP-IJCNLP), 2019, pages 55–65.

［2］ Gao, Tianyu and Yao, Xingcheng and Chen, Danqi. SimCSE: Simple contrastive learning of sentence embeddings. In Proceedings of Conference on Empirical Methods in Natural Language Processing (EMNLP), 2021.