🍀

医薬品検索にベクトル検索を導入したら、デフォで検索ニーズをほぼ満たせそうだった話

どんな人向けの記事?

  • 医薬品のような難しい検索ニーズにこたえるためにベクトル検索を利用する知見を見てみたい
  • MySQLの全文検索と、ベクトル検索精度や速度を比較してみたい
  • ベクトルDBとEmbeddingモデルを利用した簡単なベクトル検索の実装方法を知りたい

医薬品の検索ニーズは多様なので、ベクトル検索で解決できるか試したい

1つの医薬品を指す名称は、複数存在するため医薬品検索は意外と面倒な問題です。

例えば、日本人なら頭痛や生理痛、発熱したときに「ロキソニン」を飲んだことがあるかもしれません。この名称は商品の名称ですが、成分の名称は「ロキソプロフェンナトリウム水和物」です。

さらに、ロキソプロフェンには錠剤以外にもテープやパップといった剤形の違いがあります。

そして最後に、ロキソプロフェンを作っている会社は複数あるので、末尾に「トーワ」や「ファイザー」などの組み合わせが存在します。ロキソニン以外だと、4mg, 8mg, 12mgなど複数の規格を持つ医薬品も多数存在します。

医師や薬剤師など、どんなユーザーが使うシステムかによってユースケースが異なるため、すべてを検索結果に出すべきでないケースもありますが、通常、薬剤師が関係するようなケースではこれらの複数名称を区別できるような検索システム が必要になります。

緊急性が要される現場のユーザーは「ロキソ 60 トーワ」のように一意に医薬品を特定できる最低限の検索ワードを構築しがちです。このクエリは3文字で医薬品の種類を限定し、60mgという規格と会社名を表現するため、現場の薬剤師が一般的に考えうる最短のクエリです。

しかし、何も考えずにこの検索クエリをSQLのLIKE句を使って実現するのは少し手間がかかります。

https://kakehashi-dev.hatenablog.com/entry/2024/09/11/110000

最近、カケハシさんのテックブログにて、MySQLの全文検索を使ってこの問題を解決するアプローチが公開されており、n-gramN=1に設定することで「4」のような1文字にも対応できるようにしていました。

実際にこの方法を試したところ、詰まるようなポイントは一つもなく、ほとんどのユースケースをこれで満たせそうでした。素晴らしい。私の記事はここで終了です。

とはいえ、医薬品検索と医薬品情報やカルテ情報などを組み合わせてRAGでサービスを作りたいケースがでてくるかもしれませんし、ベクトル検索ならユーザーのクエリへの柔軟性の高さを確保することもできそうです。

もし検索の精度が出ない場合はEmbeddingのモデルをファインチューニングすることもできるため、医薬品検索は面白い題材かもしれないと思ったので、試してみました。

この記事では、QdrantというベクトルDBを使ったベクトル検索の手順を丁寧に記載します。
さらに、実際の医薬品マスタを用いて現場の医療スタッフが利用しそうな検索ワードを用いて性能を評価。MySQLの全文検索と比較してメリット・デメリットを考えてみます。

コードの量としては数十行程度の簡単なものなので、ベクトル検索を実装したことがない方のチュートリアルとしても役立つかもしれません。

それでは早速実装していきましょう!

全医薬品のマスタを取得する

https://www.medis.or.jp/4_hyojyun/medis-master/index.html

医薬品のマスタとして、一般財団法人医療情報システム開発センター(MEDIS-DC) が公開している医薬品HOTコードマスターを利用していきます。

日本で医療用医薬品として告示されている品目数は1.6万程度なのですが、前述のとおり同成分でも多数の品目が存在するため、このマスタには6.4万行のデータが収載されています。

ダウンロードするとTXTファイルになっていますが、カンマ区切りのShift-JISデータなのでCSVとして読み込みます。

後ほどEmbeddingの工程が入るため、それも踏まえてPython + Pandasでやっていきます。

import pandas as pd
import re

df_origin = pd.read_csv('./{file_name}.csv', encoding='cp932')
df_origin.rename(columns={
    '基準番号(HOTコード)': 'hot_code',
    '告示名称': 'generic_name', # 厳密には一般名ではないのですが許してください
    '販売名': 'sell_name',
    '規格単位': 'unit_size',
    '包装形態': 'package',
    '包装単位単位': 'package_unit',
    '包装総量単位': 'package_total_unit',
    '包装単位数': 'package_unit_amount',
    '包装総量数': 'package_total_amount',
    '製造会社': 'manufacturer',
    '販売会社': 'seller',
    }, inplace=True)

df_origin = df_origin[['hot_code', 'generic_name', 'sell_name', 'unit_size', 'package', 'package_unit', 'package_total_unit', 'package_unit_amount', 'package_total_amount', 'manufacturer', 'seller']]

# 全角数字を半角数字に変換する関数
def zenkaku_to_hankaku(text):
    if isinstance(text, str):
        # 全角数字を半角数字に変換
        text = re.sub(r'[0-9]', lambda x: chr(ord(x.group(0)) - 0xFEE0), text)
        # 全角アルファベットを半角アルファベットに変換
        text = re.sub(r'[A-Za-z]', lambda x: chr(ord(x.group(0)) - 0xFEE0), text)
        # 全角ピリオドを半角ピリオドに変換
        text = text.replace('.', '.')
        # 全角パーセント記号を半角パーセント記号に変換
        text = text.replace('%', '%')
    return text

df = df_origin.applymap(zenkaku_to_hankaku)
df.head()

今回は、マスタは半角で持っておくようにしました。ユーザーの入力が全角だとしても半角に補正して利用する想定です。

全角じゃないと保険請求上ダメとかいろいろ問題はあるかもしれないのですが、検索を実現するために、まずは半角で持っておく方が個人的に好みなのでこうしておきました。関係者の方はお許しください。

ベクトルDB (Qdrant) のセットアップ

https://qdrant.tech/

PostgreSQLやSQLiteの拡張などを利用すれば、ベクトルデータを手軽に保存できるのですが、私はQdrantというベクトルDBが好きなので使っていきたいと思います。

Qdrantを使うと、ベクトルデータに付随してメタデータを持たせることができます。

また、類似度の計算についてもQdrantの方で面倒を見てくれます。QdrantはRust実装であることもあって速度も速いです。

デフォルトでWebの管理画面も用意してくれるので、簡単な確認はそちらから行うこともできます。

さらに、Qdrantはクライアントライブラリが豊富に用意されているほか、RESTfulなエンドポイントも生えているため、問い合わせ用のバックエンドサーバーの実装言語を選ばない利点があります。

私は正直なところPythonがそんなに好きではなく、バックエンドサーバーはGoでサクッと書きたい派なので、これらの特徴は非常にうれしいです。

セットアップもDockerでサクッとやれちゃいます。

services:
  qdrant:
    image: qdrant/qdrant
    ports:
      - "6333:6333" # rest
      - "6334:6334" # grpc
    volumes:
      - ./db:/qdrant/storage
     

あとはおもむろに docker compose up -d してください。
localhost:6333/dashboard でダッシュボードにアクセスできます。
6334はgrpc用で、Goのクライアント通信に使うのですが不要なら消してもOKです。

Embeddingはintfloat/multilingual-e5-baseでサクッとやる

https://huggingface.co/intfloat/multilingual-e5-base

精度が悪かった場合、手元でサクッとファインチューニングまでやりたかったので、ローカルでいろいろ試せるモデルかつ、日本語の埋め込み性能が高いモデルとして intfloat/multilingual-e5-base を選定しました。

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("intfloat/multilingual-e5-base", device="cuda")
vector_sze = len(model.encode("test"))

もっとEmbeddingの速度が速いモデルとしては pkshatech/GLuCoSE-base-ja などもあるので、Embeddingの速度を求めるケースなどでは利用を検討しても良いかもしれません。

また、精度を出したい場合はintfloat/multilingual-e5-large を使うと、さらに安定した結果を得られる可能性があります。

Qdrantに医薬品のベクトルデータを入れ込んでいく

from qdrant_client import QdrantClient, models
from qdrant_client.models import PointStruct

client = QdrantClient(url="http://localhost:6333")

# Collectionの作成(SQLのテーブルのようなもの)
client.create_collection(
    collection_name=collection_name,
    vectors_config=models.VectorParams(size=vector_size, distance=models.Distance.COSINE, on_disk=True),
)

def safe_get(row, key, default=''):
    return row[key] if pd.notna(row[key]) else default

for i, row in df.iterrows():
    point = PointStruct(
        id=i,  
        vector=model.encode(f'query: 商品名:{safe_get(row, "sell_name")} 一般名:{safe_get(row, "generic_name")}').tolist(),
        payload={
            'hot_code': safe_get(row, 'hot_code'),
            'generic_name': safe_get(row, 'generic_name'),
            'sell_name': safe_get(row, 'sell_name'),
            'unit_size': safe_get(row, 'unit_size'),
            'package': safe_get(row, 'package'),
            'package_unit': safe_get(row, 'package_unit'),
            'package_total_unit': safe_get(row, 'package_total_unit'),
            'package_unit_amount': safe_get(row, 'package_unit_amount'),
            'package_total_amount': safe_get(row, 'package_total_amount'),
            'manufacturer': safe_get(row, 'manufacturer'),
            'seller': safe_get(row, 'seller'),
        },
    )
    info = client.upsert(collection_name=collection_name, points=[point])
    print(info)

Qdrantにベクトルデータを入れるのは上記のようにするだけ。とても簡単ですね!

私は何も考えずに全部ぶっこんじゃえ!と軽い気持ちで実行したら45minくらいかかってしまったので、検証はもっと小さいデータセットで行うことをお勧めします。

ダッシュボードから500件をビジュアライズしてみると、しっかり同じ成分の薬を近い箇所にクラスターにできていそうなことがわかります。カーソルを合わせてみるとメタデータが確認できるのでぜひお試しください。

見せてもらおうか、ベクトルの性能とやらを…!!

Qdrantを使った検索は下記のように簡単に行うことができます。

search_word = "query: "
search_word += "カンデサルタン"

# 検索ワードをEmbedding
search_vector = model.encode(search_word)

client.search(
    collection_name=collection_name,
    query_vector=search_vector,
    limit=7,
)
ScoredPoint(id=49461, version=49862, score=0.8670473, payload={'generic_name': '(局)カンデサルタン錠12mg「DSEP」', 'hot_code': 1237160010201, 'manufacturer': '第一三共エスファ', 'package': 'PTP', 'package_total_amount': 140.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 14.0, 'sell_name': 'カンデサルタン錠12mg「DSEP」', 'seller': '第一三共', 'unit_size': '12mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=49460, version=49861, score=0.8670473, payload={'generic_name': '(局)カンデサルタン錠12mg「DSEP」', 'hot_code': 1237160010101, 'manufacturer': '第一三共エスファ', 'package': 'PTP', 'package_total_amount': 100.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 10.0, 'sell_name': 'カンデサルタン錠12mg「DSEP」', 'seller': '第一三共', 'unit_size': '12mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=49462, version=49863, score=0.8670473, payload={'generic_name': '(局)カンデサルタン錠12mg「DSEP」', 'hot_code': 1237160010301, 'manufacturer': '第一三共エスファ', 'package': 'バラ', 'package_total_amount': 500.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 500.0, 'sell_name': 'カンデサルタン錠12mg「DSEP」', 'seller': '第一三共', 'unit_size': '12mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=50141, version=50542, score=0.8666179, payload={'generic_name': '(局)カンデサルタン錠8mg「科研」', 'hot_code': 1239638010301, 'manufacturer': 'シオノケミカル', 'package': 'バラ', 'package_total_amount': 500.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 500.0, 'sell_name': 'カンデサルタン錠8mg「科研」', 'seller': '科研製薬', 'unit_size': '8mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=50138, version=50539, score=0.8666179, payload={'generic_name': '(局)カンデサルタン錠8mg「科研」', 'hot_code': 1239638010101, 'manufacturer': 'シオノケミカル', 'package': 'PTP', 'package_total_amount': 100.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 10.0, 'sell_name': 'カンデサルタン錠8mg「科研」', 'seller': '科研製薬', 'unit_size': '8mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=50139, version=50540, score=0.8666179, payload={'generic_name': '(局)カンデサルタン錠8mg「科研」', 'hot_code': 1239638010102, 'manufacturer': 'シオノケミカル', 'package': 'PTP', 'package_total_amount': 500.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 10.0, 'sell_name': 'カンデサルタン錠8mg「科研」', 'seller': '科研製薬', 'unit_size': '8mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=50140, version=50541, score=0.8666179, payload={'generic_name': '(局)カンデサルタン錠8mg「科研」', 'hot_code': 1239638010201, 'manufacturer': 'シオノケミカル', 'package': 'PTP', 'package_total_amount': 700.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 14.0, 'sell_name': 'カンデサルタン錠8mg「科研」', 'seller': '科研製薬', 'unit_size': '8mg1錠'}, vector=None, shard_key=None, order_value=None),

カンデサルタンというのは血圧の薬の成分名なのですが、これはまぁ普通に当てられています。

では、 「カンデ 4 武田」 のように現場で使いそうなワードの結果を見てみましょう。これはMySQLの全文検索でカケハシさんのテックブログで紹介されていたものと同じ検索ワードです。

ScoredPoint(id=58606, version=59007, score=0.87933934, payload={'generic_name': 'カンデサルタンシレキセチル4mg錠', 'hot_code': 1287677010102, 'manufacturer': '武田テバファーマ', 'package': 'PTP', 'package_total_amount': 500.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 10.0, 'sell_name': 'カンデサルタン錠4mg「武田テバ」', 'seller': '武田薬品', 'unit_size': '4mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=58607, version=59008, score=0.87933934, payload={'generic_name': 'カンデサルタンシレキセチル4mg錠', 'hot_code': 1287677010103, 'manufacturer': '武田テバファーマ', 'package': 'PTP', 'package_total_amount': 1000.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 10.0, 'sell_name': 'カンデサルタン錠4mg「武田テバ」', 'seller': '武田薬品', 'unit_size': '4mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=58605, version=59006, score=0.87933934, payload={'generic_name': 'カンデサルタンシレキセチル4mg錠', 'hot_code': 1287677010101, 'manufacturer': '武田テバファーマ', 'package': 'PTP', 'package_total_amount': 100.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 10.0, 'sell_name': 'カンデサルタン錠4mg「武田テバ」', 'seller': '武田薬品', 'unit_size': '4mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=58608, version=59009, score=0.87933934, payload={'generic_name': 'カンデサルタンシレキセチル4mg錠', 'hot_code': 1287677010201, 'manufacturer': '武田テバファーマ', 'package': 'PTP', 'package_total_amount': 140.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 14.0, 'sell_name': 'カンデサルタン錠4mg「武田テバ」', 'seller': '武田薬品', 'unit_size': '4mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=58609, version=59010, score=0.87933934, payload={'generic_name': 'カンデサルタンシレキセチル4mg錠', 'hot_code': 1287677010301, 'manufacturer': '武田テバファーマ', 'package': 'バラ', 'package_total_amount': 500.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 500.0, 'sell_name': 'カンデサルタン錠4mg「武田テバ」', 'seller': '武田薬品', 'unit_size': '4mg1錠'}, vector=None, shard_key=None, order_value=None),

このワードは sell_name: カンデサルタン錠4mg「武田テバ」 を当てられているのでOK。

この商品には10錠、14錠のシートや500錠のバラ品などもあるので、正しく期待した品目が返ってきています。

たまたまカンデサルタンだけ当てられている可能性もあるので、毎年何百億も稼ぐ高血圧の治療薬の代表成分であるアムロジピンについて調べてみましょう。代表的なジェネリックメーカーの「トーワ」が作ったものを当てられるかみてみます。

アムロジピン 5 トーワ

ScoredPoint(id=40392, version=40793, score=0.9320942, payload={'generic_name': 'アムロジピン内用ゼリー5mg「トーワ」', 'hot_code': 1197884010101, 'manufacturer': '東和薬品', 'package': '分包', 'package_total_amount': 50.0, 'package_total_unit': '包', 'package_unit': '包', 'package_unit_amount': 1.0, 'sell_name': 'アムロジピン内用ゼリー5mg「トーワ」', 'seller': '東和薬品', 'unit_size': '5mg1包'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=37562, version=37963, score=0.92869896, payload={'generic_name': 'アムロジピンベシル酸塩5mg錠', 'hot_code': 1185805020102, 'manufacturer': '東和薬品', 'package': 'PTP', 'package_total_amount': 1000.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 10.0, 'sell_name': 'アムロジピン錠5mg「トーワ」', 'seller': '共創未来ファーマ', 'unit_size': '5mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=37560, version=37961, score=0.92869896, payload={'generic_name': 'アムロジピンベシル酸塩5mg錠', 'hot_code': 1185805010301, 'manufacturer': '東和薬品', 'package': 'バラ', 'package_total_amount': 500.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 500.0, 'sell_name': 'アムロジピン錠5mg「トーワ」', 'seller': '東和薬品', 'unit_size': '5mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=37561, version=37962, score=0.92869896, payload={'generic_name': 'アムロジピンベシル酸塩5mg錠', 'hot_code': 1185805020101, 'manufacturer': '東和薬品', 'package': 'PTP', 'package_total_amount': 100.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 10.0, 'sell_name': 'アムロジピン錠5mg「トーワ」', 'seller': '共創未来ファーマ', 'unit_size': '5mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=37559, version=37960, score=0.92869896, payload={'generic_name': 'アムロジピンベシル酸塩5mg錠', 'hot_code': 1185805010202, 'manufacturer': '東和薬品', 'package': 'PTP', 'package_total_amount': 140.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 14.0, 'sell_name': 'アムロジピン錠5mg「トーワ」', 'seller': '東和薬品', 'unit_size': '5mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=37556, version=37957, score=0.92869896, payload={'generic_name': 'アムロジピンベシル酸塩5mg錠', 'hot_code': 1185805010101, 'manufacturer': '東和薬品', 'package': 'PTP', 'package_total_amount': 100.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 10.0, 'sell_name': 'アムロジピン錠5mg「トーワ」', 'seller': '東和薬品', 'unit_size': '5mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=37558, version=37959, score=0.92869896, payload={'generic_name': 'アムロジピンベシル酸塩5mg錠', 'hot_code': 1185805010201, 'manufacturer': '東和薬品', 'package': 'PTP', 'package_total_amount': 700.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 14.0, 'sell_name': 'アムロジピン錠5mg「トーワ」', 'seller': '東和薬品', 'unit_size': '5mg1錠'}, vector=None, shard_key=None, order_value=None),

これは「内用ゼリー」というあまり使われない剤形が1位に出てきていますが、名称としては確かにクエリに適したものが返ってきています。

Qdrantはpayloadに含まれる値をフィルタすることもできるため、ユースケースによっては利用を検討するべきかもしれません。

最後に、医薬品名には類似しているものが多く存在します。たとえば、テオドール(喘息治療薬)とテグレトール(抗てんかん薬)は取り違えが起こりやすい薬剤です。

しかし、今回利用した intfloat/multilingual-e5-base ではこれらを単語としてしっかり区別できているようで、特に問題はなさそうでした。

テオドールという検索ワードに対し、テグレトールは含まれていません。

テオドール

ScoredPoint(id=7600, version=8001, score=0.8737909, payload={'generic_name': 'テオドール錠100mg', 'hot_code': 1039658020102, 'manufacturer': '三菱ウェルファーマ', 'package': 'PTP', 'package_total_amount': 500.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 10.0, 'sell_name': 'テオドール錠100mg', 'seller': '三菱ウェルファーマ', 'unit_size': '100mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=7603, version=8004, score=0.8737909, payload={'generic_name': 'テオドール錠100mg', 'hot_code': 1039658020301, 'manufacturer': '三菱ウェルファーマ', 'package': 'バラ', 'package_total_amount': 1000.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 1000.0, 'sell_name': 'テオドール錠100mg', 'seller': '三菱ウェルファーマ', 'unit_size': '100mg1錠'}, vector=None, shard_key=None, order_value=None),
ScoredPoint(id=7605, version=8006, score=0.8737909, payload={'generic_name': 'テオドール錠100mg', 'hot_code': 1039658030102, 'manufacturer': '田辺三菱製薬', 'package': 'PTP', 'package_total_amount': 500.0, 'package_total_unit': '錠', 'package_unit': '錠', 'package_unit_amount': 10.0, 'sell_name': 'テオドール錠100mg', 'seller': '田辺三菱製薬', 'unit_size': '100mg1錠'}, vector=None, shard_key=None, order_value=None),

ほとんどのケースで大丈夫だったものの、 ロキソ トーワ 錠 のようなワードで検索したときに、 ロキシスロマイシン錠 が引っかかってくることはあるので、実用する場合は、このようなマイナーケースは認識しておく必要がありそうです。

(ロキシスロマイシンはRoxithromycin, ロキソプロフェンはLoxoprofenなのですが、英字における ox を E5は同じものと判別しているのかもしれません)

MySQLの全文検索と比較したQdrant x multilingual-e5-base のベクトル検索の性能

ベクトル検索は、ほとんどデフォルト設定のまま構築しただけで、ほぼ医薬品検索に必要な要件を満たせそうだという所感でした。

Qdrantを利用すると様々な包装単位やメーカーを含むメタデータを使ったフィルタリングも容易に行えることから、とにかくカンタンに実装できます。

SQL系のエクステンションを用いたベクトル検索の場合、検索を行うためにテーブルのJOINが必要だったりするのですが、それすら必要ないお手軽さは特筆すべき便利さであると言えるでしょう。

一方、速度ではMySQLの全文検索の方が有利と言えるでしょう。

SELECT * FROM Medicine
WHERE MATCH(sell_name, generic_name)
AGAINST(:search_term IN BOOLEAN MODE)

Dockerでローカルに構築した **MySQLで上記のクエリで全文検索を行い10回ほど実行した平均をとったところ、 73msec/回**程度でした。

一方、Qdrantの検索(Embeddingの時間含む)の10回実行時間の平均値は 91msec/回 でした。

ほとんどはEmbeddingにかかる時間なので、Embedding用のサーバーをどのような構成にするかによってこの数値は変わってきますが、医薬品検索のケースではおそらくMySQLの方が早いと言えるでしょう。

また、そもそもQdrant, Embeddingなど用意することは多いので、MySQLに比べると構成が複雑になるため、メンテのコストもMySQLの方が少なく済むと言えるでしょう。

ただし、MySQLの全文検索のn-gramのトークン数の設定は以下のように my.cnf への記載が必要です。

[mysqld]
ngram_token_size=2 #医薬品検索では1文字の単位を区別する必要があるため1にする

この設定はサーバー単位でしか設定できないため、そのサーバーが持つすべてのデータベース・テーブルに影響します。

したがって、他の全文検索と共存するような状況では、MySQLによる医薬品の全文検索は導入しづらいことが予想されます。

最後に、Embeddingしたベクターデータは、RAGなどによってLLMとのコラボレーションさせる用途に向くことにも注目しておくべきかもしれません。

たとえば、医療従事者の問い合わせ内容に医薬品名が含まれる場合に、ベクトルDBに医薬品名で問い合わせて補足情報として使うといったユースケースでは、MySQLよりもベクトル検索の方が優れたシステムを構築しうるでしょう。

何事にもトレードオフがあります。自分のシステムのユースケースを考慮して、必要なものを選べる感覚を養っておきたいですね!

まとめ

ざっくりとした内容でしたが、医薬品検索を実際のマスタを用いてベクトル検索で実装し、MySQLの全文検索と比較してみました。

多くのケースではMySQLの全文検索で十分であると言えるでしょう。

一方で、ベクトル検索でも十分な精度が出せるうえ、今後LLMによる医薬品情報の活用を行うという面ではこちらを利用するメリットが出る可能性も高く、ユースケースによっては検討の余地がありそうです。

マインディアでは毎日AI活用のナレッジをBizサイド、Devサイドで相互にシェアして知見を深めています。EngeneerはAI関連アプリケーションを開発する専用の時間を設けていることで、非常にスピード感を持った開発を進めることができます。

もし興味のある方は、カジュアル面談等お待ちしておりますので、ぜひXのDM等からお気軽にご連絡をお願いします!

あとお友達になってくれる人も強く募集しているのでXよろしくです!
https://twitter.com/hagakun_yakuzai

株式会社マインディア テックブログ

Discussion