本文へ移動
ハック!でAI

AI技術大全 第5回

日本語の埋め込みモデルを比較 社内資料の検索で 7 つを測った

佐々木 航@w_sasakiwith Claude Code読了 約29分
左に題。右に、7 つの埋め込みモデルと BM25 の Recall@5・MRR@10・半角カナ 15 問の結果・binary の Recall@5・100 万件を埋め込む時間と費用をまとめた表

この記事でわかること

  • 7 つの Recall@5 と MRR@10、差が言えた組と言えない組
  • 半角カナ・前置き・binary で、検索の正しさがどれだけ落ちるか
  • 1 万件・100 万件を埋め込む CPU の時間と API の費用

確認した環境: sentence-transformers 6.1.0・faiss-cpu 1.15.1 を CPU で実行、API は YOBITSUGI AI で text-embedding-3-large・gemini-embedding-2 / 2026年10月15日に確認

日本語の社内資料を検索するとき、埋め込みモデルはどれを選べばよいのか。架空の会社の社内資料と 287 問で、公開のモデル 5 つと API 2 つを測ると、答えの箇所が上位 5 件に入った割合(Recall@5)は 94.4〜98.6% で、95% の幅が重ならずに差が言えたのは、Ruri v3 30m との 2 組だけでした。はっきり差が出たのはモデルより使い方の所で、半角カナで聞くと Ruri v3 と OpenAI は 15 問中 3〜4 問を落とし、binary に縮めると multilingual-e5-large は Recall@5 が 12.2 ポイント下がりました。

結果の早見

表。Recall@5、MRR@10、半角カナ 15 問で上位 5 件に入った数(元の問から写し)、binary の Recall@5、100 万件を埋め込む時間か費用の順に、Ruri v3 30m は 94.4%・0.821・15 から 11・89.2%・CPU 3.1 時間。Ruri v3 130m は 97.9%・0.838・15 から 12・93.0%・CPU 18.0 時間。Ruri v3 310m は 96.2%・0.832・15 から 12・95.1%・CPU 43.5 時間。multilingual-e5-large は 98.6%・0.880・15 から 15・86.4%・CPU 57.4 時間。BGE-M3 は 97.9%・0.861・15 から 14・95.5%・CPU 54.5 時間。OpenAI text-embedding-3-large は 96.9%・0.866・15 から 12・98.6%・24.7 ドル。Gemini Embedding 2 は 98.3%・0.893・15 から 15・98.3%・22.3 ドル(バッチ 11.2 ドル)。BM25(参考)は 92.3%・0.832・14 から 14。下の帯に、幅が重ならないのはどれも Ruri v3 30m との組で、multilingual-e5-large(Recall@5 と Recall@10)と Gemini Embedding 2(Recall@10)の 2 組だけ
7 つのモデルと参考の BM25 の、Recall@5・MRR@10(小さい字は 95% の幅)・半角カナの 15 問・binary にしたときの Recall@5・100 万件を埋め込む時間と費用撮影・作図: 編集部

埋め込みとベクトル検索

埋め込みは、文を数百から数千個の数の並び(ベクトル)に変える処理です。意味の近い文ほど、ベクトルの向きが近くなるように学習されています。資料を切った小さな部分(チャンク)は前もって埋め込んで索引に入れておき、問はそのつど埋め込んで、内積の大きい順に並べます。第1回の RAG の記事で使った Ruri v3 310m を、今回はほかのモデルと入れ替えて比べます。

上の段は、チャンク「検索文書: ### 在宅勤務の日数 … 在宅勤務ができるのは、週 3 日までとする。」を埋め込みのモデルに通すと、768 個の数のベクトル [-0.019, 0.076, -0.011, …] になり、長さを 1 にそろえて索引に入れる。下の段は、問「検索クエリ: 正社員がリモートワークをするのは、1 週間に最大で何日までですか。」を同じモデルで [-0.023, 0.074, -0.013, …] にし、内積の大きい順に並べると、1 位 0.898 正社員就業規則の在宅勤務の日数、2 位 0.886 契約社員の規則の対象者と日数。下に、決めること 3 つ。① 前置き、② 保存の形(float32 は 1 次元 4 バイト、int8 は 1 バイト、binary は 1 ビット)、③ 索引(今回は全件と比べる FAISS の索引)
埋め込みとベクトル検索の流れ。数は Ruri v3 310m で実際に出た値撮影・作図: 編集部

作った物と測り方

資料とチャンク

  • 第1回と同じ、架空の会社「株式会社ソラマド計測」の社内資料 10 本(就業規則・経費・製品 FAQ など・合わせて 50,101 字)です。
  • 第1回でいちばん良かった「見出しで切る」切り方で、271 個のチャンク(平均 185 字)にし、7 つに同じ文を渡しました。

問 287 問

3 つの箱。編集部 50 問は第1回のまま(素直 10・言い換え 15・数字 15・複数の箇所 10)。AI 177 問は Claude Sonnet 5.5 が資料 1 本ごとに 18 問 × 10 回(素直 50・言い換え 80・数字 47)、180 問から機械で 3 問を外し、人が 20 問を読み 2 問を直し、紛らわしい 7 問も直す。校閲で 177 問を全部読み、22 問に別の答えの箇所を足し、答えが決まらない 2 問は両方の箇所を正解にした。写し 60 問は機械で書き替え(全角・半角カナ・英語とカタカナ・漢数字が 15 ずつ)。例は、全角「1 人いくらまで」、半角カナ「怪しいメールが届きました」、英語とカタカナ「なくしたsmartphoneが…remoteで消された」、漢数字「新幹線で五時間かかる出張先へ」。下の帯に、問と証拠の文字の 2-gram の重なりは素直 0.30・数字 0.18・言い換え 0.10
問の内訳と、表記ゆれの写しの例。どの問も、答えの決め手の文が資料のどこにあるかを記録してある撮影・作図: 編集部
  • 編集部の 50 問: 第1回の 60 問のうち、答えのある 50 問です。
  • AI の 177 問: 資料 1 本ずつ、Claude Sonnet 5.5 に 18 問を作らせました(10 回・131.1 クレジット・プロンプトは記事の最後)。答えの決め手の文(証拠)を資料から写させ、資料にそのままあるかを機械で確かめ、すでにある問と同じ箇所の 3 問を外しました。
  • 人の確かめ: 決まった種で選んだ 20 問を編集部(Claude Code を使用)が資料と見比べ、正社員か契約社員かが問に無く答えが 1 か所に決まらない 2 問を直しました。同じ型の 7 問も直しました。
  • 校閲の見直し: 結果を見たあと、校閲で AI の 177 問を全部資料と照らし、同じ答えが別の所にもある 22 問は、その箇所も正解に足しました(写しを含め 28 問)。国内か海外か、正社員か契約社員かが問に無く答えが決まらない 2 問は、文を変えると API の埋め込みを作り直すことになるので、どちらの読み方の箇所も正解にしました(写しを含め 3 問)。7 つに同じく当てています。
  • 表記ゆれの写し 60 問: 元の問を機械で全角・半角カナ・英語とカタカナ(ロガーを logger に など)・漢数字に書き替えた写しを、15 問ずつ作りました。

モデルと設定

モデル ライセンスなど 次元 前置き(問 / チャンク) 長さの上限
Ruri v3 30m・130m・310m(cl-nagoya) Apache 2.0 256・512・768 「検索クエリ: 」/「検索文書: 」 8,192 トークン
multilingual-e5-large(intfloat) MIT 1,024 「query: 」/「passage: 」 512 トークン
BGE-M3(BAAI・密なベクトルだけ) MIT 1,024 なし 8,192 トークン
OpenAI text-embedding-3-large API(各社の規約) 3,072 なし 8,192 トークン
Gemini Embedding 2(gemini-embedding-2) API(各社の規約) 3,072 「task: search result | query: 」/「title: none | text: 」 8,192 トークン
  • ライセンス・次元・前置き・長さの上限は、各モデルのカードと公式のドキュメントで確かめました(2026年10月15日)。multilingual-e5-large のカードは前置きを付けないと性能が落ちると書き、BGE-M3 のカードは問に指示は要らないと書いています。
  • 長さの上限で切れたチャンクはありませんでした。CPU は AMD EPYC-Milan(スレッド 8 本・ほかの作業と共用)です。

次の版で動かしました(Python 3.12・PyTorch 2.14.1 の CPU 版)。BGE-M3 は重みが pytorch_model.bin だけなので、safetensors だけを取る設定では読み込めませんでした。

ターミナル
pip install "sentence-transformers==6.1.0" "transformers==5.19.0" "faiss-cpu==1.15.1" "numpy==2.5.4"
Python
import faiss
from sentence_transformers import SentenceTransformer

# モデルごとの前置き(問, チャンク)。各モデルのカードの決まり
PREFIX = {
    "cl-nagoya/ruri-v3-30m":  ("検索クエリ: ", "検索文書: "),
    "cl-nagoya/ruri-v3-130m": ("検索クエリ: ", "検索文書: "),
    "cl-nagoya/ruri-v3-310m": ("検索クエリ: ", "検索文書: "),
    "intfloat/multilingual-e5-large": ("query: ", "passage: "),
    "BAAI/bge-m3": ("", ""),          # 問に指示は要らない。密なベクトルだけを使う
}

name = "cl-nagoya/ruri-v3-130m"
qp, dp = PREFIX[name]
model = SentenceTransformer(name, device="cpu")

# chunks はチャンクの文のリスト(271 個)
D = model.encode([dp + c for c in chunks], batch_size=16, normalize_embeddings=True)
index = faiss.IndexFlatIP(D.shape[1])   # 長さ 1 にしたので、内積 = コサイン類似度
index.add(D)

def search(q, k=10):
    v = model.encode([qp + q], normalize_embeddings=True)
    return index.search(v, k)[1][0]      # 近い順のチャンクの番号

API の 2 つは、YOBITSUGI AI の中継から各社の API の形(OpenAI は embeddings、Gemini は batchEmbedContents)のまま、64 件ずつ送りました。上が OpenAI、下が Gemini の形です(Gemini はチャンクと問を別の回で送った)。

JSON
{"model": "text-embedding-3-large", "input": ["チャンク 1 の文", "チャンク 2 の文"], "encoding_format": "float"}
JSON
{"requests": [
  {"model": "models/gemini-embedding-2", "content": {"parts": [{"text": "title: none | text: チャンク 1 の文"}]}},
  {"model": "models/gemini-embedding-2", "content": {"parts": [{"text": "task: search result | query: 問の文"}]}}
]}

数え方

  • Recall@k: 答えに要る箇所が全部、上位 k 件のチャンクに入った問の割合です。k は 1・5・10 です。複数の箇所の問(10 問)は 2 つ以上のチャンクが要るので、上位 1 件には入りません。
  • MRR@10: 要る箇所が全部そろった最初の順位 r の、1/r の平均です。1 位なら 1、2 位なら 0.5 で、10 位までにそろわなければ 0 です。
  • 95% の幅: 問を選び直して計算し直すブートストラップ(10,000 回・乱数の種 20261015)で出しました。表記ゆれの写しは、元の問とひとまとまりにして選び直しています。幅が重なる差では順位を付けず、前置きや量子化の差は同じ問の対で幅を出しました。

7 つの結果: 差が言えたのは 2 組

2 つの図。Recall@5 は、Ruri v3 30m 94.4(91.7〜96.9)、Ruri v3 130m 97.9(96.1〜99.3)、Ruri v3 310m 96.2(93.8〜98.3)、multilingual-e5-large 98.6(97.1〜99.7)、BGE-M3 97.9(96.1〜99.3)、OpenAI text-embedding-3-large 96.9(94.7〜98.6)、Gemini Embedding 2 98.3(96.2〜99.7)、BM25 92.3(88.5〜95.6)。MRR@10 は、0.821(0.779〜0.860)、0.838(0.799〜0.875)、0.832(0.791〜0.871)、0.880(0.848〜0.910)、0.861(0.826〜0.894)、0.866(0.832〜0.898)、0.893(0.859〜0.924)、BM25 0.832(0.789〜0.872)
Recall@5 と MRR@10 の値(点)と 95% の幅(線)。線が重なる差では順位を付けない撮影・作図: 編集部
  • Recall@10 は 96.5〜99.7% で、どのモデルでも答えの箇所が上位 10 件にほぼ入りました。幅が重ならないのは、Ruri v3 30m と multilingual-e5-large・Gemini Embedding 2 の組です。
  • Recall@1 は 73.2%(Ruri v3 30m)から 82.2%(Gemini Embedding 2)まででしたが、どの組も幅が重なりました。
  • Ruri v3 30m との組のほかは、130m と 310m の差も、API と公開のモデルの差も言えませんでした。参考の BM25(語の一致で探す方法)は、Recall@5 が 92.3% でした。

6 つ以上が上位 5 件を外したのは、要る箇所が 3 つある M10(海外出張の支度金と日当)と、言い換えの G10-13(承認の前に、注文の意思を取引先へ伝えてよいか)でした。

問の種類ごとの差

表。素直・言い換え・数字・複数の箇所・表記ゆれの Recall@5 と、言い換えの MRR@10。Ruri v3 30m は 100.0・91.6・98.4・70.0・93.3・0.772。Ruri v3 130m は 100.0・97.9・98.4・100.0・95.0・0.788。Ruri v3 310m は 98.3・93.7・100.0・90.0・95.0・0.814。multilingual-e5-large は 100.0・96.8・100.0・90.0・100.0・0.856。BGE-M3 は 100.0・95.8・100.0・90.0・98.3・0.813。OpenAI text-embedding-3-large は 100.0・95.8・100.0・80.0・95.0・0.850。Gemini Embedding 2 は 100.0・97.9・100.0・80.0・98.3・0.898。BM25 は 98.3・85.3・98.4・80.0・93.3・0.750
種類ごとの Recall@5(%)と、言い換えの問の MRR@10。赤は 92% 未満撮影・作図: 編集部
  • 素直と数字の問は、7 つとも 98〜100% でした。
  • 言い換えは 91.6〜97.9% で、BM25 は 85.3% でした。言い換えの MRR@10 は 0.772〜0.898 で、幅が重ならないのは Gemini Embedding 2(0.898)と Ruri v3 30m(0.772)の組だけです。
  • 複数の箇所は 70〜100% ですが、10 問なので 1 問で 10 ポイント動きます。
  • 出どころで分けると、編集部の問(1 か所で決まる 40 問)の Recall@1 は 77.5〜90.0% で、AI の 177 問(72.3〜84.7%)より低くはありませんでした。

表記ゆれ: 半角カナで落ちた

表。全角・半角カナ・英語とカタカナ・漢数字の、元の問から写しへの上位 5 件の数。半角カナは、Ruri v3 30m が 15 から 11、Ruri v3 130m と 310m が 15 から 12、multilingual-e5-large が 15 から 15、BGE-M3 が 15 から 14、OpenAI text-embedding-3-large が 15 から 12、Gemini Embedding 2 が 15 から 15、BM25 が 14 から 14。ほかの 3 種は埋め込みの 7 つで下がった問なし。NFKC で正規化すると 6 つとも 15。例の問「社内システムのパスワードは、一定の期間ごとに変えなければいけませんか。」の答えの箇所の順位は、Ruri v3 30m が 1 から 147、130m が 2 から 79、310m が 3 から 11、e5-large が 1 から 2、BGE-M3 が 1 から 8、OpenAI が 1 から 6、Gemini が 1 から 1。Ruri v3 のトークナイザーは半角カナを 1 字ずつ別のトークンにし、multilingual-e5-large は全角に寄せてから分けるが濁点と半濁点は離れる
表記ゆれの写しで、上位 5 件に入った問の数(元の問から写し)と、半角カナの写しを NFKC で正規化したとき。下は、半角カナの問の 1 つの順位と、トークナイザーの分け方撮影・作図: 編集部
  • 全角・英語とカタカナ・漢数字の写しでは、7 つとも上位 5 件から落ちた問はありませんでした。
  • 半角カナでは、Ruri v3 の 3 つが 15 問中 3〜4 問、OpenAI が 3 問、BGE-M3 が 1 問を落としました。multilingual-e5-large と Gemini Embedding 2 は 0 問です。図の下の例では、Ruri v3 30m で答えの箇所が 1 位から 147 位に下がりました。
  • 理由は、文を語の単位に分ける部品(トークナイザー)にありました。Ruri v3 は文字を正規化する設定が無く、半角カナを 1 字ずつ別のトークンにします。multilingual-e5-large と BGE-M3 は、分ける前に半角カナを全角に寄せます。
  • 問を Unicode の NFKC(互換の文字をそろえる正規化)で直してから埋め込むと、半角カナの 15 問は試した 6 つとも全部戻り、Ruri v3 30m の 287 問の Recall@5 は 94.4% から 95.8% になりました(差の幅 +0.3〜+2.8)。
Python
import unicodedata

q = unicodedata.normalize("NFKC", "社内システムのパスワードは")   # "社内システムのパスワードは" になる
# NFKC は全角の英数字を半角にし、「℃」を「°C」に変える。今回は問だけを直し、チャンクは元のまま

前置きを外すと

表。付けたときから外したとき。Ruri v3 30m は Recall@5 94.4 から 94.1(差 -0.3、幅 -1.8〜+1.1)、MRR@10 0.821 から 0.815。Ruri v3 130m は 97.9 から 95.5(差 -2.4、幅 -4.3〜-0.7)、0.838 から 0.802(差 -0.036、幅 -0.059〜-0.013)。Ruri v3 310m は 96.2 から 96.2、0.832 から 0.844。multilingual-e5-large は 98.6 から 97.6(差 -1.0、幅 -2.4〜+0.0)、0.880 から 0.865。Gemini Embedding 2 で前置きを外すと 98.3 から 98.6、0.893 から 0.873(差 -0.020、幅 -0.038〜-0.003)。Gemini Embedding 2 で title: none を title: 資料名 に替えると 98.3 から 99.3、0.893 から 0.929(差 +0.036、幅 +0.014〜+0.059)。BGE-M3 と OpenAI には決まった前置きが無い
決まりの前置きを付けたときと、外した・替えたときの Recall@5 と MRR@10。差の幅は同じ問の対で出した撮影・作図: 編集部
  • Ruri v3 130m は、外すと Recall@5 が 97.9% から 95.5%(差の幅 -4.3〜-0.7)、MRR@10 が 0.838 から 0.802 に下がりました。
  • Ruri v3 30m と 310m、multilingual-e5-large(-1.0 ポイント、幅の上の端が 0)は、差が言えませんでした。
  • Gemini Embedding 2 は、Recall@5 は変わらず、MRR@10 が 0.893 から 0.873 に下がりました(幅 -0.038〜-0.003)。

量子化と次元: 小さくするとどれだけ落ちるか

表。float32・int8・binary・binary と並べ直し・binary(中央値で分ける)の Recall@5。Ruri v3 30m は 94.4・94.4・89.2・91.3・90.6。Ruri v3 130m は 97.9・97.9・93.0・95.8・95.1。Ruri v3 310m は 96.2・96.2・95.1・96.2・95.5。multilingual-e5-large は 98.6・98.6・86.4・88.9・96.2。BGE-M3 は 97.9・97.9・95.5・96.9・95.1。OpenAI text-embedding-3-large は 96.9・96.9・98.6・97.9・96.9。Gemini Embedding 2 は 98.3・98.3・98.3・98.3・97.9。下の帯に、multilingual-e5-large はチャンクの 9 割以上で符号が同じ次元が 62%(ほかの 6 つは 4〜16%)
保存の形ごとの Recall@5(%)。右の列は、あとから足した、次元ごとの中央値を境にした binary撮影・作図: 編集部
  • int8(1 次元 1 バイト・float32 の 4 分の 1)は、7 つとも Recall@5 が float32 と同じでした。MRR@10 の差も 0.001 以下です。
  • binary(1 次元 1 ビット・32 分の 1)は、モデルで大きく分かれました。multilingual-e5-large は 98.6% から 86.4% に下がり(差の幅 -16.4〜-8.3)、Ruri v3 30m と 130m も 5.2・4.9 ポイント下がりました。OpenAI は 96.9% から 98.6%、Gemini Embedding 2 は 98.3% のままでした。
  • binary + 並べ直し(binary で 40 件に絞り、float32 の問で並べ替える)では、Ruri v3 130m が 95.8%、BGE-M3 が 96.9% まで戻りましたが、multilingual-e5-large は 88.9% でした。
  • multilingual-e5-large は、1,024 次元のうち 62% が、チャンクの 9 割以上で同じ符号でした(ほかは 4〜16%)。0 を境にすると、多くの次元がどのチャンクでも同じビットになります。次元ごとの中央値を境にすると 96.2% まで戻ったので、この偏りが下がった主な理由と見ています。
Python
import numpy as np
import faiss

d = D.shape[1]   # D: チャンクのベクトル、Q: 問のベクトル(どちらも長さ 1・float32)

# int8: 次元ごとの最小と最大をチャンクのベクトルで学び、256 段に丸める(1 次元 1 バイト)。問は float32 のまま比べる
sq = faiss.IndexScalarQuantizer(d, faiss.ScalarQuantizer.QT_8bit, faiss.METRIC_INNER_PRODUCT)
sq.train(D)
sq.add(D)

# binary: 0 より大きいかで 1 ビット(1 次元 1/8 バイト)。ハミング距離で並べる
bi = faiss.IndexBinaryFlat(d)            # d は 8 の倍数
bi.add(np.packbits(D > 0, axis=1))
I = bi.search(np.packbits(Q > 0, axis=1), 40)[1]

# binary + 並べ直し: 上位 40 件を、float32 の問と ±1 にしたチャンクの内積で並べ替える
B = np.where(D > 0, 1.0, -1.0).astype("float32")
order = [row[np.argsort(-(B[row] @ q), kind="stable")] for q, row in zip(Q, I)]

# あとから足した確かめ: 0 でなく、次元ごとの中央値を境にする
med = np.median(D, axis=0)
bm = faiss.IndexBinaryFlat(d)
bm.add(np.packbits(D > med, axis=1))
I2 = bm.search(np.packbits(Q > med, axis=1), 10)[1]

各社の口で次元を減らす形も試しました。OpenAI の dimensions を 1,024 にすると Recall@5 は 96.9% のまま、MRR@10 は 0.866 から 0.855(差の幅 -0.023〜-0.001)、256 では 95.5% と 0.827(MRR@10 の差の幅 -0.063〜-0.018)でした。Gemini Embedding 2 の output_dimensionality を 768 にしても、差は言えませんでした(97.9%・0.892)。

JMTEB の順位と比べる

左は JMTEB の Retrieval(11 の資料の平均・nDCG@10)で、1 位 Ruri v3 130m 76.45、2 位 Ruri v3 310m 76.03、3 位 Ruri v3 30m 72.84、4 位 BGE-M3 72.15、5 位 OpenAI text-embedding-3-large 71.95、6 位 multilingual-e5-large 67.65、Gemini Embedding 2 は載っていない。右は今回の MRR@10 の値を大きい順に並べたもので、順位の番号は付けていない。Gemini Embedding 2 0.893、multilingual-e5-large 0.880、OpenAI 0.866、BGE-M3 0.861、Ruri v3 130m 0.838、Ruri v3 310m 0.832、Ruri v3 30m 0.821。下の帯に、幅が重ならずに差が言えたのは Ruri v3 30m との 2 組だけで、JMTEB では e5-large は 6 つの中でいちばん下、Ruri v3 30m は 3 番目
JMTEB の順位表の Retrieval の平均(左)と、今回の MRR@10 の値(右)。右は値の大きい順に並べただけで、多くは幅が重なる撮影・作図: 編集部

JMTEB は、日本語の埋め込みを測る公開のベンチマークです。順位表は MTEB の順位表に移ったので、GitHub に残る最後の版(2025年10月2日(UTC))の Retrieval の平均と比べました。

  • JMTEB の Retrieval では、Ruri v3 の 130m(76.45)と 310m(76.03)が上で、multilingual-e5-large(67.65)は 6 つの中でいちばん下です。Gemini Embedding 2 は載っていません。
  • 今回の MRR@10 の値は、multilingual-e5-large が 0.880、Ruri v3 の 3 つが 0.821〜0.838 で、値の並びは JMTEB とほぼ逆でした。ただし多くは幅の重なる差で、6 つで幅が重ならないのは multilingual-e5-large と Ruri v3 30m の組(Recall@5・@10)だけです。この 1 組は、JMTEB と逆の向きの差でした。JMTEB は 11 の資料の nDCG@10 の平均で、資料の種類も数え方も違います。
  • 今回に近い行政の FAQ の資料(jagovfaqs_22k)では、Ruri v3 310m(76.49)・130m(75.32)・multilingual-e5-large(72.98)・OpenAI(72.41)・30m(70.21)・BGE-M3(69.07)の順でした。

費用と時間

上の表は自分の CPU(8 スレッド)。1 個の秒、1 万件、100 万件、問 1 つの秒、モデルの重さの順に、Ruri v3 30m は 0.011・2 分・3.1 時間・0.015・0.15 GB。Ruri v3 130m は 0.065・11 分・18.0 時間・0.038・0.54 GB。Ruri v3 310m は 0.157・26 分・43.5 時間・0.088・1.27 GB。multilingual-e5-large は 0.207・34 分・57.4 時間・0.133・2.26 GB。BGE-M3 は 0.196・32 分・54.5 時間・0.101・2.29 GB。中の表は API。単価(100 万トークン)、1 個のトークン、1 万件、100 万件、問 1 万回の順に、OpenAI text-embedding-3-large は 0.13 ドル・190・0.25 ドル・24.7 ドル・0.05 ドル。Gemini Embedding 2 は 0.20 ドル(バッチ 0.10 ドル)・112・0.22 ドル・22.3 ドル・0.06 ドル。下の表は 100 万件の索引の大きさで、float32・int8・binary の順に、256 次元は 1.02 GB・0.26 GB・32 MB、768 次元は 3.07 GB・0.77 GB・96 MB、1024 次元は 4.10 GB・1.02 GB・128 MB、3072 次元は 12.29 GB・3.07 GB・384 MB
自分の CPU で埋め込む時間、API で埋め込む費用、100 万件の索引の大きさ。平均 185 字のチャンクとして計算撮影・作図: 編集部
  • API: チャンク 1 個は、OpenAI で 190 トークン、Gemini Embedding 2 で 112 トークン(前置きを含む・countTokens で数えた)でした。公式の単価(100 万トークンあたり OpenAI $0.13、Gemini $0.20・バッチ $0.10。2026年10月15日に確認)から、100 万件で $22.3〜24.7 です。
  • YOBITSUGI AI の記録: 271 個と 287 問の埋め込みは、OpenAI 1.42 クレジット、Gemini 1.02 クレジットでした。
  • 自分の CPU: 8 スレッド・ほかの作業と共用のホストで、271 個を 16 個ずつ埋め込んだ秒から計算した目安です。1 個は Ruri v3 30m が 0.011 秒、multilingual-e5-large が 0.207 秒で、100 万件では 3.1〜57.4 時間です。電気代やサーバー代は入れず、GPU は試していません。

どれを選ぶか

ここからは編集部の見立てです。今回の 271 個のチャンクでは、7 つのどれでも Recall@5 は 94% 以上で、差の多くは幅の中でした。決め手は、資料を社外に出せるか・費用・速さ・手間です。

  • 資料を社外に出せない: 公開のモデルを自分のサーバーで動かします。Ruri v3 130m は Recall@5 が 97.9% で、CPU で 100 万件 18 時間、重さ 0.54 GB でした。前置きを付け、問を NFKC で正規化してください。
  • CPU の時間を短くしたい: Ruri v3 30m は 100 万件 3.1 時間ですが、今回は multilingual-e5-large より Recall@5 が低いと言える結果でした。
  • multilingual-e5-large: 今回は Recall@5 が 98.6% でしたが、CPU では BGE-M3 と並んで遅く(100 万件 57.4 時間)、長さの上限は 512 トークンです。binary には向きませんでした。
  • API でよい: Gemini Embedding 2 の MRR@10 は 0.893 でしたが、幅はほかの 6 つと重なります。OpenAI と費用は近く、どちらも binary で下がりませんでした。

気をつけること

  • 資料が小さい: 271 個では、答えの箇所は上位 10 件にほぼ入りました。数十万件で差が開くかは確かめていません。
  • 問の多くは AI が作った: 校閲で 177 問を全部読み直しましたが、見落としが残っているかもしれません。
  • 架空の 1 社の資料: 業界の言葉が多い資料では、結果が変わることがあります。

まとめ

  • 271 個のチャンクと 287 問では、7 つの Recall@5 は 94〜99% で、差が言えたのは Ruri v3 30m との 2 組だけでした。
  • 差がはっきり出たのは、半角カナ(問を NFKC で正規化すると戻る)・前置きの付け忘れ・binary にしたときの落ち方で、使い方の所でした。
  • 公開の順位表の並びは、自分の資料での並びと合わないことがあります。自分の資料で問を作り、答えの箇所を決めてから測ってください。

渡した文(全文)

問を作る AI(Claude Sonnet 5.5)

YOBITSUGI AI の ask_ai で、tier は standard、system は使っていません。資料 1 本ごとに 1 回、10 回とも同じ形で、{資料名}・{すでにある質問}・{資料 1 本の全文} だけを替えました。{すでにある質問} には、その資料に答えがある編集部の問を「- 」で並べました。

テキスト
次の文章は、架空の会社「株式会社ソラマド計測」の社内資料「{資料名}」の全文です。社内資料を検索する仕組みを試すための質問を作ってください。

作る質問(合わせて 18 問):
- 素直 5 問: 資料と同じ言葉を使って聞く質問。
- 言い換え 8 問: 社員がふだん使う言葉で聞く質問。答えの箇所にある言葉(制度の名前・見出しの言葉・専門の言葉)を、できるだけ別の言い方に替える。
- 数字 5 問: 答えが金額・日数・時間・回数・割合などの数字になる質問。条件を具体的に書く(例: 片道 12 km の自転車通勤)。

決まり:
- 1 問の答えは、資料の 1 か所(1 文か、箇条書きの 1 行)で決まるようにする。
- 「evidence」には、答えの決め手になる部分を、資料から 1 字も変えずに写す(10〜60 字。前後を切ってよいが、途中を略さない。空白や記号もそのまま)。
- 質問は 1〜2 文。ほかの資料と区別するのに要るとき(正社員か契約社員か、S1 か S2 か など)は、その言葉を質問に入れる。
- 同じ箇所から 2 問以上作らない。下の「すでにある質問」と同じ事柄は聞かない。
- 資料に無いことを聞かない。

すでにある質問:
{すでにある質問}

出力は JSON の配列だけにする(前置き・説明・コードの枠は書かない)。1 問は次の形にする。
{"type": "素直|言い換え|数字", "q": "質問", "answer": "答え(30 字以内)", "evidence": "資料から写した部分"}

資料:
{資料 1 本の全文}
数え方と 95% の幅のコード
Python
import numpy as np

def first_full_rank(ranking, evidence, chunks):
    # 要る箇所(グループ)が全部そろう最初の順位。グループの中は、同じ事実のどれか 1 つの位置でよい
    pos = {i: r for r, i in enumerate(ranking, 1)}
    worst = 0
    for group in evidence:
        hit = [pos[i] for i, c in enumerate(chunks)
               if any(c["doc"] == o["doc"] and c["start"] <= o["start"] and o["end"] <= c["end"] for o in group)]
        worst = max(worst, min(hit))
    return worst

# I[k] は k 番の問で、271 個のチャンクを近い順に並べた番号
r = np.array([first_full_rank(I[k], q["evidence"], chunks) for k, q in enumerate(questions)])
recall5 = (r <= 5).mean()
mrr10 = np.where(r <= 10, 1 / r, 0).mean()

# 95% の幅: 元の問と写しを同じまとまりにして、まとまりを 10,000 回選び直す
key = [q.get("orig", q["id"]) for q in questions]      # 写しは元の問の id を orig に持つ
CL = sorted(set(key))
cl = np.array([CL.index(k) for k in key])                # まとまりの番号(0 から 226)
n = len(CL)
rng = np.random.default_rng(20261015)
pick = rng.integers(0, n, (10000, n))

def interval(v):
    s = np.bincount(cl, weights=v, minlength=n)
    c = np.bincount(cl, minlength=n)
    m = s[pick].sum(1) / c[pick].sum(1)
    return np.percentile(m, [2.5, 97.5])

lo, hi = interval((r <= 5).astype(float))

参考にした公式の情報

XFacebookはてブLINE

執筆者

佐々木 航@w_sasaki

with Claude Code

プロフィール

株式会社Game 代表取締役。工学部出身で、21歳で起業してからシステム会社の経営一筋で8期目。アプリの受託開発から始め、いまはAIを使ったシステムソリューションを手がけています。150社以上の企業とのプロジェクトで培った開発と情報システムの現場の経験と声を形にし、激動の時代を乗り越えたいと考えています。

この記事は、Claude Code を使って執筆しました。画面の画像は、検証環境で実際に操作して撮影したものです(引用の画像は出典を記載しています)。内容の誤りはお問い合わせからお知らせください。

連載

AI技術大全
全 6 回の目次
  1. 1RAG の精度を上げるには? チャンク・ハイブリッド検索・リランクを順に足して測った
  2. 2LoRA の作り方 8GB の GPU で画像の LoRA を学習し、枚数と時間を測った
  3. 3ファインチューニングと RAG の違い 同じ 60 問で学習・検索・長いプロンプトを比べた
  4. 4MCP と Function Calling の違い MCP サーバーを自作して測った
  5. 5日本語の埋め込みモデルを比較 社内資料の検索で 7 つを測った(この記事)
  6. 7プロンプトキャッシュとは? 仕組みと料金を Claude・GPT・Gemini で測った