Qdrantとdoc2vecを用いた類似アイテムの抽出
本記事では、Qdrantというベクトル検索エンジンを用いてデータベースから類似アイテムの抽出を行った事例を紹介します。
やりたいこと
Mercari Price Suggestion Challenge を題材に、類似商品の抽出を行いたいと思います。
このコンテストは、メルカリに出品された商品の情報と販売額が提供され、新たな商品の販売額を予想するというものでした。
過去に売れている類似商品の情報を用いることで、新規出品の際の価格の推定に役立てられると考えています。
Qdrantとは
Qdrant(クアドラント/クワドラント)は、ベクトルの類似度に基づいてデータの検索ができる「ベクトル検索エンジン」です。
Qdrant - Vector Search Engine
ベクトル検索とは
テキストや画像などのデータを機械学習モデルを利用してベクトルとして表現し、そのベクトル同士の類似度に基づいてデータを返す仕組みです。
類似度の評価方法としては内積やコサイン類似度、ユークリッド距離などが利用できます。
ベクトル検索の嬉しさ
ベクトル検索ができると何が嬉しいのでしょうか。
まず、従来から使われている完全一致や部分一致に基づくキーワード検索の仕組みから考えていきましょう。
今回のメルカリの例など、入力の形式が決まっていないデータでは表記ゆれなども多く存在します。すると、キーワード検索では引っかからなくなってしまいます。
また、何らかの手段を講じて表記ゆれをなくす/表記を統一することができたとしましょう。しかし、過去のデータには名前は違うけれども似たような扱いができるものも存在すると思います。
同じように扱いたいものをすべて “or” で繋げて検索することもできますが、あらゆるものにルールを作っていくのは限りがあり、管理も大変になってしまいます。
このような時に役立つのがベクトル検索です。
機械学習の進歩によって、文章を意味を考慮してベクトル化できるようになってきました。
そのベクトルの類似度を元にマッチするものを返すことで、キーワード検索では拾いきれなかった情報を入手できます。
また、今回の例では文章をベクトル化して扱いますが、画像や音声、動画などあらゆるものがベクトル化できるようになってきており、扱える範囲はとても広いです。
ベクトル検索の詳細な仕組みや適用例は、以下のGoogleCloudBlogの記事にわかりやすく書かれています。
あらゆるデータの瞬時アクセスを実現する Google のベクトル検索技術
ペイロード
Qdrantではペイロードという形で、ベクトルとセットで持たせるデータを与えられます。
JSON形式で表現できるものであればほとんどなんでも持たせることができます。
これの嬉しさとして、マッチしたデータの詳細情報をそのままQdrantから取得することができる点が挙げられます。
フィルタ機能
先程、ペイロードとしてベクトルとセットで情報をもたせることができると述べました。Qdrantにはペイロードの情報を用いたフィルタ機能も備わっています。
ベクトルの類似度で検索すると言っても、やはり最低限これを含んでいてほしい、これは含まないでほしい、というようなルールを定義したくなることもあると思います。
その際にフィルタ機能が役に立ちます。
使い方
QdrantはOSSとして公開されており、githubのページからcloneして使うことができます。dockerで簡単に起動できるようになっているので、cloneしてすぐ立ち上げられます。
また、セルフホストする以外にもクラウドサービスとしても提供されており、自前で環境を作らなくとも利用できます。
呼び出しとしてRestAPI・gRPCに対応している他、Python・Go・Rustのクライアントライブラリも用意されています。
機械学習系の開発ではPythonが使われることが多いと思いますが、シームレスに繋げられるところも魅力です。
実際にやってみた
タイトルにもなっているように、Qdrantを用いて類似アイテムの抽出を行いました。
アイテムのデータとして、そのアイテムの名前とカテゴリ、ブランド名、説明文などが用意されています。この情報を用いて類似アイテムを探します。
ベクトル化
まずは商品の情報をベクトル化しないことには話が始まりません。
言語はPython、ベクトル化にはgensimのDoc2Vecを使ってやっていきます。
データの確認をします。
df = pd.read_table(os.path.join(DATA_PATH, "mercari/train.tsv"))
df.head()
name
、category_name
、brand_name
、item_description
を連結させたものを1つのアイテムを表現する文字列とし、これをベクトル化していきます。
gensimのDoc2Vecにデータを与えることで学習します。
doc_modelという名前で今回のデータから学習したDoc2Vecのモデルができました。
from gensim.models.doc2vec import Doc2Vec, TaggedDocument
df["text"] = df["name"] + "" + df["category_name"] + " " + df["brand_name"].fillna(" ") + " " + df["item_description"].fillna(" ")
# 形態素解析などを用いて、適当にトークン化
sentences = tokenize(df, "text")
documents = [TaggedDocument(doc, [i]) for i, doc in enumerate(sentences)]
doc_model = Doc2Vec(documents, dm=0, vector_size=200, window=5, min_count=1, epochs=20)
Qdrantクラスタの作成
今回はクラウドサービス版のQdrantを使っていきます。
Vector Search Database | Qdrant Cloud
まずはアカウントを作成します。
サインインできたら、左のタブの Clusters
からクラスタを作成しましょう。
クラウドのプロバイダが選択できるようになっていますが、現状はAWSのみで他は準備中のようです。
RAMやCPU、DISKについては扱いたいデータの量やリクエストの量から決めましょう。今回はテストなので最小構成にしました。
構成について詳しくは、公式ドキュメントの Capacity and sizing を参照してください。
Qdrantクライアントの作成
Pythonには qdrant_client
というクライアントライブラリが公式から用意されているので、この先の作業ではそちらを使っていきます。
ライブラリのインストールは pip install qdrant-client
でできます。
また、先程立てたクラスタのホスト名とAPIキーも同時に与えています。
from qdrant_client import QdrantClient
from qdrant_client.http.models import Distance, VectorParams, PointStruct
from qdrant_client.conversions.common_types import Record
collection_name = "mercari_collection"
client = QdrantClient(
host="YOUR_HOST_NAME",
api_key="YOUR_API_KEY",
)
Qdrantのコレクションの作成
Qdrantではコレクションという単位でデータベースを構築します。
ここでは mercari_collection
という名前で作成しました。
ベクトルの次元数は200、類似度計算にはコサイン類似度を用います。
client.recreate_collection(
collection_name=collection_name,
vectors_config=VectorParams(size=200, distance=Distance.COSINE),
on_disk_payload=True,
)
Qdrantへのデータの登録
QdrantにはRecord
という単位でデータが登録されます。
Record
には id
vector
payload
の情報を持たせ、 Record
の配列としてアップロードすることで、登録は完了です。
今回は payload
として、アイテムの名前や状態、ブランド名やカテゴリ名、価格などの情報を一通り持たせています。
names = df["name"].fillna("Null").values.tolist()
item_condition_ids = df["item_condition_id"].fillna("Null").values.tolist()
brand_names = df["brand_name"].fillna("Null").values.tolist()
category_names = df["category_name"].fillna("Null").values.tolist()
prices = df["price"].fillna("Null").values.tolist()
item_descriptions = df["item_description"].fillna("Null").values.tolist()
records = [
Record(
id=idx,
vector=doc_model.dv.vectors[idx].tolist(),
payload={
"name": names[idx],
"item_condition": item_condition_ids[idx],
"brand_name": brand_names[idx],
"category_name": category_names[idx],
"price": prices[idx],
"item_description": item_descriptions[idx],
},
)
for idx in tqdm(range(len(doc_model.dv.vectors)))
]
client.upload_records(
collection_name=collection_name, records=records, batch_size=64, parallel=8
)
ベクトル検索の実行
いよいよ、Qdrantに登録したデータに対して類似ベクトルの検索を実行していきます。
検索したい文字列をベクトル化し、 Qdrantクライアントの search
メソッドから検索をかけます。
from qdrant_client.http import models
input_text = "検索したい文字列"
split_text = text.split(" ")
query_vector = doc_model.infer_vector(split_text, epochs=20)
hits = client.search(
collection_name="mercari_collection",
query_vector=query_vector,
with_payload=True,
limit=10,
)
いくつか試してみましょう。
例として、メルカリの商品説明欄によく「禁煙」や「タバコ吸いません」などの記述が見受けられると思います。
英語では “no smoking / no smoke / smoking free / smoke free” など、いろんな言い回しがあります。
試しに、”no smoking”で検索した結果から抜粋しました。
同じ意味として扱いたい”Smoke free”が正しくヒットしていますね。
{
'brand_name': 'Null',
'category_name': 'Men/Tops/T-shirts',
'item_condition': 3,
'item_description': 'Size medium. No flaws in logo. Smoke free pet friendly home',
'name': 'Iron Maiden t Shirt',
'price': 14.0
}
これはほんの一例ですが、複数の表現がなされるものは探せばいくらでも出てくると思います。それらのルールをすべて人力で作成していたらとても追いつきません。
この例のように、意味を考慮して類似度の計算ができる点がベクトル検索の有用な点です。
まとめ
本記事ではベクトル検索によって類似商品を抽出する方法を紹介してきました。単純なキーワード検索と比べて意味を考慮した検索ができる点が非常に魅力的です。
また、今回はテキストをベクトル化して扱ってきましたが、機械学習の発展により画像や音声などあらゆるものがベクトルとして表現できるようになっている今、活用できる範囲はとても広いと考えています。
得られた知見を活用しながら、新たな適用例も考えていきたいと思います。
参考
Documentation - Qdrant
Elasticsearchのベクトルフィールドをテキスト類似性検索に活用する
Elasticsearch による近似最近傍探索
メルカリShopsのデータを用いて価格推定モデルを検証してみた | Souzoh Intern
Vertex AI Matching Engineをつかった類似商品検索APIの開発
類似商品レコメンド機能、その後
Qdrant ベクトル検索エンジン - 前編
Discussion