Milvusを使った類似文章検索入門

# はじめに
株式会社パレットリンクの[@t-yonefu](https://qiita.com/t-yonefu)です。
今回はMilvusを使った類似文章検索入門ということで敷居が高くならないように説明を省いている箇所が多々ありますのでご承知ください。

# 類似文章検索とは？
類似文章検索とはそのままの意味で『似ている文章を検索する』ことを指し、主な使用例は検索エンジンや問い合わせの仕分けなどが挙げられます。
具体的な説明は後で記載しますが、先に簡単に処理の各工程を並べると
1. 文章の意味や表現をコンピュータに判断できるように数値化(ベクトル化)
2. ベクトル化された文章同士の比較
3. 比較結果で順位を決める

のざっくり３工程で表され、今回使用するMilvusを使えば実装に手間がかかるのは１番の工程ぐらいで他は簡単に実装できます。


# 文章のベクトル化
人間が文章(いわゆる自然言語)を認識する時、脳は今までの経験を基に意味や文脈を判断します。それに対してコンピュータでは、あたえられた文章はただの記号でしかありません。もちろん意味も文脈もコンピュータは人間のように認識できていないわけですが、とりあえず文章に対して数値化(ベクトル化)を行い、コンピュータで計算できる形にする必要があります。この時の数値化処理を<b>Embedding処理</b>と呼び、類似文章検索を実装するときにどのようなEmbedding処理を行うか<b>Embeddingモデル</b>を選択する必要があります。
# ベクトル化された文章同士の比較
例えば「今日は名古屋で雨が降った」という文章を与えられた場合に以下の文章でどれが似ているかを考えてみましょう。
* 「今日は愛知で雨が降った」
* 「今日は北海道で雨が降った」
* 「昨日は北海道で雨が降った」

まずはEmbedding処理を行い、文字列をベクトル化します。ベクトルは実数値を並べた配列になり、配列の要素数をこのベクトルの<b>次元数</b>と言います。次元数は採用するEmbeddingモデルによって異なります。
* 「今日は名古屋で雨が降った」 -> [0.035,-0.0385,-0.005,...]
* 「今日は愛知で雨が降った」 -> [0.059,-0.027,-0.008,...]
* 「今日は北海道で雨が降った」 -> [3.682,-1.270,-2.992,...]
* 「昨日は北海道で雨が降った」 -> [3.882,-5.812,6.247,...]

次にこのベクトル同士を比較するために、どれだけ類似しているか測定するために<b>類似性尺度</b>を用いて計算を行います。類似性尺度にはいくつか種類がありますが、Milvusで扱っているものだけ表に記載しています。
| 尺度名 | 説明 | 範囲 | 特徴 |
|:-----------|:------------|:------------:|:------------|
| L2距離(ユークリッド距離)       | ベクトル間の直線距離 | [0,∞) | 値が小さいほど類似 |
| コサイン類似度(Cosine Similarity)     | ベクトル間の角度の余弦値 | [-1,1] | 値が1に近いほど類似 |
| 内積(Inner Product / Dot Product)       | ベクトルの要素ごとの積の和 | (-∞,∞) | 値が大きいほど類似 |

各類似性尺度を用いて計算した結果は値の取りうる範囲や「0に近いほど似ている」、「1に近いほど似ている」など性質が異なるので注意が必要です。

# Embeddingモデルの選択は慎重に
先ほどの例ではほとんどの人が「今日は愛知で雨が降った」が一番似ていると感じたと思います。多くの場合は「時系列を表す〈今日〉」と「場所を表す〈名古屋〉」の部分で判断しており、この文章であれば、どのEmbeddingモデルも大抵は同じ部分で類似度に差が出ます。モデルを選択する際に気を付けるべきなのは〈場所〉の部分で、名古屋は愛知県にあるという地理的な部分はただ単純に文字を記号として比較した場合では判断できません。類似文章検索で扱う文章の分野固有の単語、言い回しによってはモデルが似ていると判断できず、モデルの精度として掲載されている内容と実際に実装して精度検証した結果でギャップが生まれることがあるのでいろいろなモデルで検証することが必須となります。

# Milvusとは？
[Milvus](https://milvus.io/ja "Milvus")とはZilliz社によって開発されたオープンソースの<b>ベクトルデータベース</b>です。普通のデータベースと比較して、ベクトルを扱うのに特化しているぐらいの認識で大丈夫です。何故ベクトルデータベースを使うのかですが、処理対象が文章にEmbedding処理を施した高次元のベクトルデータとなるため、普通のデータベースのように完全に条件に一致したものを検索するのではなく、「ちょっと誤差があってもいいから速く見つける」方法を取る必要があるためです。この方法を近傍探索（ANN）と言いますが、詳しい説明を書くと長くなるので省略します。導入方法に関してはMilvusのHPにクイックガイドがあるのでDockerが自環境で使用可能であれば手軽に導入できます。
# Embedding処理の実装
### 必要なライブラリのインストール
まずは必要なパッケージをインストールします。ちなみにMilvusはJava、Go、Node.js(JavaScript)でもSDKを提供していますが、Embedding処理の実装のしやすさを考慮して今回はPythonで実装します。
``` bash: bash
pip install pymilvus sentence-transformers pandas
```
### Embedding処理
今回はモデルにintfloat/multilingual-e5-smallを採用していますが、どのモデルを使用するか[Hugging Face](https://huggingface.co/)を参照して良い精度が出せるモデルを選択してください。また、モデルによってEmdedding処理の負荷が異なるので、許容される処理時間や実装環境のハードウェアスペック等の考慮も必要です。(今回のモデル選定理由は負荷が少なくPCに優しいからです。その分、精度は悪くなりますが...)

```python:python
from sentence_transformers import SentenceTransformer

# モデルのロード
# 初回実行時にモデルをダウンロードして~/.cache/huggingface/に保存する
model = SentenceTransformer('intfloat/multilingual-e5-small')

def text_embedding(text_list):
    embeddings = model.encode(text_list)
    return embeddings
```

# Milvusへデータ登録
### Milvusのコレクション作成
Milvusにデータを登録する前にコレクション(SQLデータベースのテーブルのようなもの)の作成とベクトルフィールドにインデックスの設定が必要になります。手順の記載は省きますが、事前に公式HPを参照してDockerでMilvusのコンテナを起動してください。特に設定を変えていなければlocalhost:19530から接続可能になります。
また、インデックスの設定をFLAT(すべてのベクトルを線形走査)にしていますが、データ数、次元数、ハードウェアスペック等に合わせて適宜変更してください。

```python:python
from pymilvus import Collection, connections, CollectionSchema, FieldSchema, DataType

# コレクションのスキーマ定義
# 主キー(idフィールド)の設定
primary_key = FieldSchema(
    name="id",
    dtype=DataType.INT64,
    is_primary=True,
)

# 原文(textフィールド)の設定
# 文字列の最大長は200
text = FieldSchema(
    name="text",
    dtype=DataType.VARCHAR,
    max_length=200
)

# Embeddingデータ(vectorフィールド)の設定
# intfloat/multilingual-e5-smallの次元数は384
# 採用するモデルごとに変更すること
vector = FieldSchema(
    name="vector",
    dtype=DataType.FLOAT_VECTOR,
    dim=384
)

# スキーマの設定
schema = CollectionSchema(
    fields=[primary_key, text, vector],
    description="テストコレクション"
)

# Milvusに接続
connections.connect(
    alias="default", 
    host='localhost', 
    port='19530')

# コレクションの作成
collection = Collection(
    name="test_collection",
    schema=schema
)

# インデックスパラメータ
index_params = {
    "index_type": "FLAT", # すべてのベクトルを線形走査で検索
    "metric_type": "L2",  # L2で検索
    "params": {}
}

# インデックスの設定
collection.create_index(
    field_name="vector",
    index_params=index_params
)

# コレクションの詳細を表示
print(collection.describe())
```


### Milvusへデータ登録
前述したEmbedding処理を使用し、テキストをベクトル化してinsertするデータを作成してデータを登録します。今回はデータ整形にpandasを使用していますが、データクレンジング等を必要とするときにDataFrameにしておくと便利なので使用しています。

```python:python
from pymilvus import Collection, connections
import pandas as pd

# Milvusに接続
connections.connect(
    alias="default", 
    host='localhost', 
    port='19530')

# コレクションの作成
collection = Collection(
    name="test_collection"
)

# 登録テキスト
text_list = ["今日は愛知で雨が降った","今日は北海道で雨が降った","昨日は北海道で雨が降った"]
# Embedding処理
vector = text_embedding(text_list)
# 登録データを作成
data = pd.DataFrame({"id":[1,2,3],"text":text_list,"vector":[vec.tolist() for vec in vector]})
# データ登録
collection.insert(data)
```
# Milvusで類似文章検索
類似文章検索も前述したEmbedding処理を使用し、検索ベクトルデータを作成して検索を行います。今回はL2(ユークリッド距離)で検索しています。

```python:python
from pymilvus import Collection, connections

# Milvusに接続
connections.connect(
    alias="default", 
    host='localhost', 
    port='19530')

# コレクションの作成
collection = Collection(
    name="test_collection"
)

# 検索パラメータ
param = {"metric_type": "L2"}

# 検索テキスト
text_list = ["今日は名古屋で雨が降った"]
# Embedding処理
vector = text_embedding(text_list)

# コレクションをロード
collection.load()

# コレクションを検索
result = collection.search(
    data=vector,
    anns_field="vector",
    param=param,
    limit=10,
    output_fields=["id", "text" ,"vector"]
)

text_list = [hit.entity.get('text') for hit in result[0]]
distance_list = [hit.distance for hit in result[0]]

# 類似順に表示
print(text_list)
# 類似度を表示
print(distance_list)
```

# さいごに
今回、Milvusを題材に類似文章検索の入門を書かせていただきました。なるべく混乱を招かないように説明するべき内容のみ記載したつもりなので難しくはないはずですが、そもそもこの分野自体が若干敷居が高いので私の記事を踏み台にしてみなさんに学んでもらえればと思っています。
以上、最後まで読んでいただきありがとうございました。


:::note info
パレットリンクでは、日々のつながりや学びを大切にしながら、さまざまなお役立ち記事をお届けしています。よろしければ、ぜひ「Organization」のページもご覧ください。
また、私たちと一緒に未来をつくっていく仲間も募集中です。ご興味をお持ちの方は、ぜひお気軽に[お問い合わせ](https://www.palettelink.co.jp/recruit/ "お問い合わせ")ください。一緒に新しいご縁が生まれることを楽しみにしています。
:::