マガジン

日本語資料向けの埋め込みモデルの選び方:比較観点と小さな比較手順

AIを使いこなす

Claudeで執筆し、GPTで自動校閲・修正しています。

分割した資料カードと質問カードを並べ、担当者が検索候補を照合する様子
記事の内容をもとに制作したAI生成イメージです。実際の製品画面や検証結果の写真ではありません。

結論から言うと、日本語資料向けの埋め込みモデルは、公開ランキングで一つに決めるのではなく、「観点表で候補を2〜3個に絞り、自社の資料と質問20問で確かめる」順で選ぶのが現実的です。理由は二つあります。一つは、公式資料に書かれている「多言語対応」が、あなたの資料(規程、議事録、マニュアル、表記ゆれの多い文書)での検索精度を保証するものではないことです。もう一つは、モデルを変えると全資料のベクトルを作り直す必要があり、後から乗り換えるコストが小さくないことです。 この記事では、言語対応・入力長・費用・再計算の手間を見る比較観点表と、20問で比べる手順、結果を集計するコード例を示します。モデルの優劣そのものは断定しません。

この記事の目次
  1. 比較観点表:公式資料で確認することと、自分で測ること
  2. 質問20問で比べる手順
  3. 集計コードの例
  4. 結果の記録表(説明用の例)
  5. 決め方の目安
  6. 参考にした公式資料

比較観点表:公式資料で確認することと、自分で測ること

次の表は、2026年10月27日に確認した2つの公式ページ(OpenAIの埋め込みガイド、Anthropicの埋め込みガイド)で読み取れた内容と、各観点で自分で測るべきことを並べたものです。モデルの仕様や価格は変わるため、採用前に各社の最新ページを確認してください。

  • 言語対応、公式資料で確認できた例:Anthropicのガイドで紹介されているVoyage AIのvoyage-4は「汎用および多言語の検索品質に最適化」と説明されています。ただし日本語での評価値はそのページにはありません。、自分で測ること:日本語の自社資料で検索が当たるか(後述の20問)
  • 入力長、公式資料で確認できた例:OpenAIのtext-embedding-3-small/largeは最大8192トークン。Voyageのvoyage-4は32,000トークンのコンテキスト長です。、自分で測ること:日本語1チャンクが何トークンになるか。長く入れられても、検索単位としては短く分けた方が当たる場合があるため、チャンク長も比較対象にします。
  • 次元数、公式資料で確認できた例:OpenAIは既定で1536次元(small)と3072次元(large)。dimensionsパラメータで短縮可能です。Voyageは既定1024次元で、256・512・2048も選べます。、自分で測ること:次元を減らした時の精度低下と、保存容量・検索速度の差
  • 費用、公式資料で確認できた例:OpenAIは入力トークン数に応じて課金されます。Voyage AIの料金はMongoDBのドキュメントで確認できます。、自分で測ること:自社資料の総トークン量×各モデルの単価×(比較時の計算回数+再計算回数)
  • 再計算の手間、公式資料で確認できた例:Voyageのガイドは旧世代モデルの扱いについて、ライフサイクルと推奨後継モデルを別ページで確認するよう案内しています。、自分で測ること:モデルが終了・世代交代した時に、全資料を再埋め込みする時間と費用
  • 文書と質問の区別、公式資料で確認できた例:Voyageは検索用途でinput_typeにquery/documentを必ず指定するよう案内しています。、自分で測ること:候補モデルが同様の指定を求めるか。指定を間違えると比較自体が不公平になります。
  • 動かす場所、公式資料で確認できた例:Voyageのvoyage-4-nanoは重みが公開されたモデル(Apache 2.0)で、自分で動かす形です。APIでは提供されないと書かれています。、自分で測ること:資料を外部APIに送ってよいか(社内規程の確認が先です)。自前運用なら実行環境の維持コスト

表の「再計算の手間」は、編集上の補足があります。モデルが違えばベクトルの空間も違うため、新旧のベクトルを同じ検索インデックスに混ぜて比べることはできません。乗り換えるなら、全チャンクの再計算とインデックスの作り直しが前提になります。次元数だけ変える場合も同様に、インデックスの再構築が要ります。

費用の見積もり方(説明用の例)

式は「総トークン量 ÷ 100万 × 100万トークンあたり単価 × 計算回数」です。説明用の例として、総量が2,000万トークン、単価を仮に100万トークンあたりp円とすると、1回で20p円です。モデル3つを比べて、選んだ1つを本番用に再計算し、半年後にもう1回やり直すなら、合計は約100p円(20p×5回)になります。実際の単価は各社の料金ページで確認してください。トークン量は、数十件をAPIに通して返ってくる使用量から平均を出し、全体に掛けて見積もるのが手軽です。入力トークン量の取得方法は、利用するAPIの応答仕様を確認してください。

Anthropicのガイドは、選定時の観点として「学習データの規模と領域への適合」「推論性能(速度・遅延)」「自社データでの追加学習などのカスタマイズ」を挙げています。日本語の専門用語が多い資料でどの程度効くかは、自分の資料で測って確かめます。

質問20問で比べる手順

1. 資料を用意する

実際に検索させたい資料から、30〜50文書を選びます。機密情報は、固有名詞や数値を置き換えたダミーに差し替えてください。チャンク分割の方法・長さは全モデルで同じにします。ここが違うと、モデルの差なのかチャンクの差なのか分からなくなります。

2. 質問20問を作る

資料の担当者が実際に聞かれそうな質問を、次のように配分します。これは編集上の提案する配分です。

  • 言い換え質問 8問:資料の文言とは違う言い方で聞く(例:資料は「年次有給休暇の付与」、質問は「休みはいつ増える?」)
  • 固有表現・型番・規程番号 4問:完全一致に近い語で聞く
  • 表記ゆれ 3問:全角・半角、略称、「申請」と「届出」のような近い語
  • 表や箇条書きの内容 3問
  • 資料に答えがない質問 2問:誤って関連資料を強く拾わないかを見る

各質問に「正解チャンクのID」を人が付けます。この正解付けは、AIに任せず担当者が行ってください。答えのない2問は正解を空にします。

3. 全モデルを同じ条件で動かす

上位5件を取得し、正解チャンクの順位を記録します。文書側と質問側のどちらを埋め込むかは、モデルごとの指定(input_typeなど)に従ってください。正解チャンクが複数ある場合は、そのうち最上位の順位を記録します。

4. 判定する

見るのは次の3点です。

  1. 正解チャンクが1位に入った数(hit@1)と、上位5件に入った数(hit@5)
  2. 外れた質問の種類(言い換えに弱いのか、型番に弱いのか)
  3. 答えのない質問の1位の類似度が、答えのある質問の1位の類似度と区別できるか

質問が20問なので、1問の差が5ポイントに相当します。1〜2問差なら、モデルの差と断定せず、質問を追加するか、費用・入力長・再計算の手間で決めるのが安全です。類似度の値そのものはモデル間で比較できません。比べるのは順位と、モデル内での分かれ方です。

集計コードの例

次のコードは説明用の例で、実行して検証したものではありません。embed_fnに各社のAPI呼び出しを包んで渡す想定です。kindが"document"または"query"で渡されるので、モデルが区別を求める場合はそこで切り替えてください。

python
import numpy as np

def normalize(m):
    m = np.asarray(m, dtype=float)
    return m / np.linalg.norm(m, axis=1, keepdims=True)

def evaluate(embed_fn, chunks, questions):
    """chunks: {chunk_id: text}
    questions: [{"id", "type", "q", "gold": [chunk_id, ...]}]
    gold が空なら「資料に答えがない質問」
    embed_fn(texts, kind) -> 2次元配列"""
    ids = list(chunks)
    doc = normalize(embed_fn([chunks[i] for i in ids], "document"))
    qry = normalize(embed_fn([q["q"] for q in questions], "query"))
    sims = qry @ doc.T
    rows = []
    for q, s in zip(questions, sims):
        order = [ids[j] for j in np.argsort(-s)]
        ranks = [order.index(g) + 1 for g in q["gold"] if g in order]
        rows.append({
            "id": q["id"], "type": q["type"],
            "best_rank": min(ranks) if ranks else None,
            "top1_score": round(float(s.max()), 3),
        })
    return rows

def summarize(name, rows):
    ans = [r for r in rows if r["best_rank"] is not None]
    no_ans = [r for r in rows if r["best_rank"] is None]
    h1 = sum(r["best_rank"] == 1 for r in ans)
    h5 = sum(r["best_rank"] <= 5 for r in ans)
    print(f"{name}: hit@1 {h1}/{len(ans)}  hit@5 {h5}/{len(ans)}")
    for r in ans:
        if r["best_rank"] > 5:
            print("  外れ:", r["id"], r["type"])
    if ans and no_ans:
        print("  答えあり1位スコア最小:", min(r["top1_score"] for r in ans),
              " 答えなし1位スコア最大:", max(r["top1_score"] for r in no_ans))
    return h1, h5, len(ans)

最後の行は、答えのある質問の最小スコアが、答えのない質問の最大スコアより高ければ、しきい値で「該当なし」を判定できそうだという目安になります。

結果の記録表(説明用の例)

下の表の数値は形式を示すための架空の値で、実測ではありません。

  • 候補A、hit@1:○/18、hit@5:○/18、外れた質問の傾向:(記入)、次元:(記入)、再計算の想定:(記入)
  • 候補B、hit@1:○/18、hit@5:○/18、外れた質問の傾向:(記入)、次元:(記入)、再計算の想定:(記入)

答えのある質問が18問なので、分母は18になります。

決め方の目安

数値の差が小さい場合は、次の順で判断すると迷いにくくなります。

  1. 外れた質問が、運用上よく聞かれる種類に集中していないモデルを残す
  2. 資料の送信先が社内ルールを満たすモデルに絞る
  3. 再計算の総費用と所要時間が許容範囲のモデルを選ぶ
  4. 同点なら、次元数が小さく保存・検索が軽い方を選ぶ

選んだ後も、資料の追加や質問の傾向の変化に備え、同じ20問を定期的に回し直せるようにしておくと、乗り換え時の判断材料になります。

参考にした公式資料

  • OpenAI:Vector embeddings:モデル名、入力上限、次元数、dimensions、課金方式
  • Anthropic:Embeddings:Anthropic自身は埋め込みモデルを提供していないこと、Voyage AIのモデル一覧、input_type、選定観点

出典・参考資料