日本語の社内文書やFAQをベクトル検索するとき、「多言語モデルなら十分なのか」「日本語専用モデルにすると本当に精度が上がるのか」「モデルサイズによるコスト差はどれくらいか」が気になります。
今回は、ローカル環境で次の3モデルを同じデータ、同じ評価コードで比較しました。
cl-nagoya/ruri-v3-310mintfloat/multilingual-e5-largeBAAI/bge-m3
結論から言うと、日本語だけの検索が中心なら、まずRuri v3を候補にする価値があります。一方、日本語以外も含む場合や多言語検索では、E5やBGE-M3のほうが扱いやすいケースがあります。最終的には公開ベンチマークではなく、自分の検索データでRecall@kとMRRを測るのが安全です。
比較したモデル
Ruri v3は日本語向けに学習されたモデルで、310Mパラメータ版は768次元のベクトルを出力します。最大8192トークンに対応し、検索用には「検索クエリ:」「検索文書:」という接頭辞を使います。
multilingual-e5-largeは94言語に対応する多言語モデルです。検索ではクエリにquery:、文書にpassage:を付ける使い方が基本です。日本語専用ではありませんが、言語が混在するデータでは比較対象にしやすいモデルです。
BGE-M3も多言語モデルです。今回は複雑なハイブリッド検索は使わず、Sentence Transformers経由のdense embeddingだけを比較しました。
評価データと指標
評価用のCSVは、次の2種類を用意しました。
# documents.csv
id,text
doc-001,パスワードを変更するには設定画面を開きます。
doc-002,請求書は毎月末にメールで送付されます。
# queries.csv
query,relevant_id
パスワードの変更方法,doc-001
請求書はいつ届きますか,doc-002
実運用では、検索クエリと正解文書の組をできるだけ多く用意します。私は評価時に、文書の分割方法、重複率、前処理をモデル間で変えないようにしました。ここを変えると、モデル性能ではなくデータ加工の差を測ることになるためです。
主な指標は次の2つです。
- Recall@k:正解文書が上位k件に含まれた割合
- MRR:正解文書が何位に出たかを重視する指標
RAGの候補文書取得ではRecall@5やRecall@10、ユーザーに直接検索結果を見せる場合はMRRも確認すると判断しやすくなります。
同じコードで比較する
3モデルを同じ評価条件で比較する流れは次の通りです。
まず依存ライブラリをインストールします。
python -m venv .venv
.venv\Scripts\activate
pip install -U sentence-transformers numpy
次のコードで、モデルを差し替えながら評価できます。
import csv
import time
import numpy as np
from sentence_transformers import SentenceTransformer
MODELS = {
"ruri": {
"name": "cl-nagoya/ruri-v3-310m",
"query_prefix": "検索クエリ: ",
"doc_prefix": "検索文書: ",
},
"e5": {
"name": "intfloat/multilingual-e5-large",
"query_prefix": "query: ",
"doc_prefix": "passage: ",
},
"bge": {
"name": "BAAI/bge-m3",
"query_prefix": "",
"doc_prefix": "",
},
}
def read_csv(path):
with open(path, encoding="utf-8-sig", newline="") as f:
return list(csv.DictReader(f))
documents = read_csv("documents.csv")
queries = read_csv("queries.csv")
doc_ids = [row["id"] for row in documents]
doc_texts = [row["text"] for row in documents]
for key, spec in MODELS.items():
print(f"\n=== {key} ===")
model = SentenceTransformer(spec["name"])
start = time.perf_counter()
doc_vecs = model.encode(
[spec["doc_prefix"] + text for text in doc_texts],
normalize_embeddings=True,
batch_size=32,
show_progress_bar=False,
)
query_vecs = model.encode(
[spec["query_prefix"] + row["query"] for row in queries],
normalize_embeddings=True,
batch_size=32,
show_progress_bar=False,
)
elapsed = time.perf_counter() - start
scores = query_vecs @ doc_vecs.T
ranked = np.argsort(-scores, axis=1)
hits = 0
reciprocal_rank = 0.0
for i, row in enumerate(queries):
answer = row["relevant_id"]
ranking = [doc_ids[j] for j in ranked[i]]
if answer in ranking[:5]:
hits += 1
if answer in ranking:
reciprocal_rank += 1 / (ranking.index(answer) + 1)
recall_at_5 = hits / len(queries)
mrr = reciprocal_rank / len(queries)
dimension = doc_vecs.shape[1]
print(f"dimension: {dimension}")
print(f"encode_seconds: {elapsed:.3f}")
print(f"recall@5: {recall_at_5:.4f}")
print(f"mrr: {mrr:.4f}")
E5の接頭辞を省略したり、Ruri v3に通常の文章をそのまま渡したりすると、正しい比較にならない場合があります。モデルごとの入力形式は、必ずモデルカードで確認しました。
精度とコストの見方
今回の検証で重要だったのは、単純に「最大モデルが最良」と判断しないことです。
日本語の言い換え、助詞の違い、製品名や社内用語を含む検索では、日本語向けに調整されたRuri v3が候補になります。一方、英語のエラーメッセージと日本語の説明文を横断して検索する場合は、多言語モデルも有力です。
コストはAPI料金だけではありません。ローカル実行では、次の要素を分けて考えます。
- モデルのダウンロード容量
- 推論時のRAM・VRAM
- 文書をベクトル化する時間
- ベクトルDBに保存する容量
- モデル更新や運用の手間
ベクトルの保存容量は、概算で次の式で求められます。
文書数 × ベクトル次元数 × 4バイト
たとえば、768次元のfloat32ベクトルを100万件保存すると、ベクトル本体だけで約3GBです。インデックスやメタデータの容量は別に必要になります。
モデル比較では、検索精度だけでなく、dimension、encode_seconds、プロセスのメモリ使用量も記録しました。精度が少し高くても、更新処理が遅くて運用できなければ実システムには採用しにくいためです。
また、モデルのリビジョンを固定しておくことも重要です。モデルが更新されると、同じ文書でもベクトルが変わる可能性があります。モデルを変更した場合は、既存ベクトルを再生成し、同じ評価データで再計測します。
まとめ
日本語文書のEmbeddingモデルを選ぶときは、公開スコアだけで決めず、次の手順で比較するのがおすすめです。
- 同じ文書分割・前処理で評価データを作る
- モデルごとの接頭辞や入力形式を正しく適用する
- Recall@k、MRR、推論時間、メモリを測る
- ベクトル次元数から保存容量を見積もる
- 採用モデルのリビジョンを固定する
私の初期候補は、日本語中心ならRuri v3、多言語対応が必要ならE5またはBGE-M3です。ただし、検索品質は文書の種類やクエリの癖で大きく変わります。最終的には、実際の検索ログから作った評価セットで判断するのが最も確実です。