埋め込み(Embedding)とは何か

埋め込みとは、テキストの「意味」を固定長の数値ベクトルに変換したものです。 OpenAI の text-embedding-3-small に「猫がソファで寝ている」という文を渡すと、 1536 個の浮動小数点数が並んだ配列が返ってきます。この配列が、その文の 1536 次元空間上の「座標」です。

この変換の重要な性質はひとつだけで、意味が近い文は座標も近くなることです。 「猫がソファで寝ている」と「ネコがカウチで昼寝している」は、単語はほとんど一致しませんが、ベクトルとしては隣り合います。 逆に「決算短信の提出期限」は、文字数が似ていても遠く離れた場所に置かれます。

graph LR
    T1["猫がソファで寝ている"] --> E[Embeddings API]
    T2["ネコがカウチで昼寝している"] --> E
    T3["決算短信の提出期限"] --> E
    E --> V1["[0.021, -0.113, 0.087, ...]"]
    E --> V2["[0.019, -0.108, 0.091, ...]"]
    E --> V3["[-0.204, 0.056, -0.311, ...]"]
    V1 -. 近い .- V2
    V2 -. 遠い .- V3
意味が近い文は近いベクトルに、遠い文は遠いベクトルに写される

つまり埋め込みを使うと、「意味の近さ」という曖昧な概念がベクトル間の距離という機械的に計算できる量に置き換わります。 検索・クラスタリング・推薦・分類・異常検知——これらはすべて「近いものを探す」「遠いものを見つける」問題なので、 埋め込みが一枚あれば同じ道具で解けるようになります。

なぜ重要か — キーワード検索の限界と RAG

従来の全文検索(BM25 など)は文字列の一致を基準にします。 「返金したい」で検索しても、ヘルプ記事が「払い戻し手続き」と書かれていればヒットしません。 同義語辞書を育ててもキリがなく、多言語になるとさらに手に負えなくなります。 埋め込みによるセマンティック検索は、言い換え・同義語・言語の違いを「座標の近さ」として吸収します。

そしてこの性質が、LLM アプリケーションの基盤である RAG(Retrieval-Augmented Generation) を成立させています。 LLM に社内文書を答えさせたいとき、全文書をプロンプトに詰めることはできません。 そこで質問文を埋め込み、事前に埋め込んでおいた文書チャンクの中から近いものだけを数件取り出してプロンプトに渡します。 この「取り出し」部分が埋め込みの仕事です。

キーワード検索(BM25)セマンティック検索(埋め込み)
一致の基準単語・文字列の一致ベクトル間の距離(意味の近さ)
同義語・言い換え辞書を人手で整備しないと拾えないモデルが学習済みなので自然に拾う
多言語言語ごとに別インデックス日本語の質問で英語文書を拾える
固有名詞・型番・ID強い(完全一致)弱い(意味を持たない記号は苦手)
説明可能性どの語がマッチしたか示せるなぜ近いかは説明しにくい
実務での落としどころ両者を組み合わせるハイブリッド検索が定番

具体例で理解する — 近さはどう測るか

2 つのベクトルの「近さ」には複数の指標がありますが、OpenAI はコサイン類似度を推奨しています。 コサイン類似度は 2 つのベクトルがなす角度で近さを測る指標で、同じ方向なら 1、直交すれば 0、逆方向なら −1 になります。

OpenAI の埋め込みはあらかじめ長さ 1 に正規化されて返ってきます。 そのためコサイン類似度の分母(ベクトルの長さの積)は常に 1 になり、単純な内積で同じ値が得られます。 ランキングの結果も完全に一致するので、計算量を抑えたいときは内積を使って構いません。

// OpenAI の埋め込みは長さ 1 に正規化済みなので、内積 = コサイン類似度
function cosineSimilarity(a: number[], b: number[]): number {
  if (a.length !== b.length) {
    throw new Error('次元数が一致しません(同じモデル・同じ dimensions で生成したか確認)');
  }
  let dot = 0;
  for (let i = 0; i < a.length; i++) {
    dot += a[i] * b[i];
  }
  return dot;
}

// 例: 0.92 → ほぼ同じ意味、0.75 → 関連あり、0.3 以下 → 無関係
// ※ 閾値はモデルとドメインで変わるため、必ず自分のデータで実測して決める

OpenAI Embeddings API の使い方

モデルの選択肢

2026 年 8 月時点で利用できるのは次の 3 モデルです。現行世代は 2024 年 1 月に登場した text-embedding-3 系で、 初代の text-embedding-ada-002 は互換性のために残っているだけです。新規開発で選ぶ理由はありません。

モデル既定の次元数最大入力料金(1M トークン)MTEBMIRACL(多言語)
text-embedding-3-small15368,192 トークン$0.0262.344.0
text-embedding-3-large30728,192 トークン$0.1364.654.9
text-embedding-ada-002(旧世代)15368,192 トークン$0.1061.031.4

MTEB は英語中心の総合ベンチマーク、MIRACL は 18 言語での検索ベンチマークです。 注目すべきは MIRACL の伸び幅で、ada-002 → 3-large で 31.4 から 54.9 へ跳ね上がっています。日本語の文書を扱うなら、この多言語性能の差が体感精度に直結します。料金面では 3-small が ada-002 の 5 分の 1、性能はそれを上回るので、まずは 3-small から始めるのが定石です。

リクエストとレスポンス

import OpenAI from 'openai';

const client = new OpenAI(); // OPENAI_API_KEY を環境変数から読む

const response = await client.embeddings.create({
  model: 'text-embedding-3-small',
  input: [
    '猫がソファで寝ている',
    'ネコがカウチで昼寝している',
    '決算短信の提出期限',
  ],
  // dimensions: 512,            // 任意: 次元を縮める(後述)
  // encoding_format: 'base64',  // 任意: 転送量を減らしたいとき
});

// data[i].index が input の順番に対応する
for (const item of response.data) {
  console.log(item.index, item.embedding.length); // 0 1536 / 1 1536 / 2 1536
}
console.log(response.usage.total_tokens); // 課金対象のトークン数

レスポンスは次の形で返ってきます。input に配列を渡すと 1 リクエストで複数のテキストをまとめて埋め込めるので、 実務では必ずバッチで送るのが基本です。

{
  "object": "list",
  "data": [
    { "object": "embedding", "index": 0, "embedding": [0.0213, -0.1134, 0.0871, ...] },
    { "object": "embedding", "index": 1, "embedding": [0.0192, -0.1081, 0.0912, ...] },
    { "object": "embedding", "index": 2, "embedding": [-0.2041, 0.0563, -0.3112, ...] }
  ],
  "model": "text-embedding-3-small",
  "usage": { "prompt_tokens": 31, "total_tokens": 31 }
}

パラメータと制限

パラメータ内容制限・注意点
input文字列、または文字列の配列1 入力あたり 8,192 トークンまで。配列は最大 2,048 要素、リクエスト全体で 300,000 トークンまで
modelモデル名text-embedding-3-small / -large / ada-002
dimensions出力ベクトルの次元数text-embedding-3 系のみ対応。既定値より小さい値を指定する
encoding_formatfloat(既定)または base64base64 は JSON の転送量を大幅に減らせる。大量埋め込み時に有効
userエンドユーザー識別子不正利用モニタリング用。任意

8,192 トークンを超える文書はエラーになります。一般的な日本語の文書はこの上限に容易に達するため、 実務では埋め込む前にチャンク分割が必須になります(後述)。

dimensions パラメータ — Matryoshka 埋め込みで容量を 12 分の 1 に

text-embedding-3 系の最大の実務的な特徴が dimensions パラメータです。text-embedding-3-large の既定 3072 次元を、256 や 512 に縮めて受け取ることができます。

これを可能にしているのが Matryoshka Representation Learning(MRL) という学習手法です。 マトリョーシカ人形のように、ベクトルの先頭の次元ほど粗くて重要な情報を、後ろの次元ほど細かい情報を持つように訓練します。 そのため先頭だけを切り出しても意味の大枝は保たれ、公式の報告では3-large を 256 次元に縮めたものが、1536 次元のフル ada-002 を上回る性能を出しています。

100万ベクトルを float32 で保存した場合の容量(次元数 × 4 バイト)

ベクトル DB のコストはほぼ「ベクトル数 × 次元数」で決まります。 100 万件を 3072 次元で保存すると約 12.3 GB、256 次元なら約 1.0 GB です。 検索の計算量も次元数に比例するので、次元を落とすことはレイテンシとメモリの両方に効きます。 「まず 3-large で埋め込み、dimensions で予算に合う次元まで縮める」という判断が、精度を大きく落とさずにできるのが MRL の恩恵です。

埋め込みを実運用に載せる流れは、どのベクトル DB を使ってもほぼ同じです。 ここでは追加インフラなしで始められる PostgreSQL + pgvector を例にします。

flowchart LR
    subgraph ingest["インデックス作成(事前・バッチ)"]
      D[文書] --> C[チャンク分割<br/>数百トークン単位]
      C --> E1[Embeddings API<br/>配列でまとめて送信]
      E1 --> DB[(pgvector<br/>content + embedding)]
    end
    subgraph query["検索(リクエスト時)"]
      Q[ユーザーの質問] --> E2[Embeddings API<br/>同じモデル・同じ次元]
      E2 --> S["類似度検索<br/>コサイン距離の昇順"]
      DB --> S
      S --> K[上位 k 件]
      K --> LLM[LLM に渡して回答生成]
    end
RAG の全体像。左が事前処理、右がリクエスト時。埋め込みは両方で同じモデル・同じ次元を使う

テーブルとインデックス

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE document_chunks (
  id          bigserial PRIMARY KEY,
  document_id bigint NOT NULL,
  content     text   NOT NULL,
  -- 埋め込みモデルを列名か別カラムで明示しておく(モデル変更時の全件再生成に備える)
  embedding   vector(1536) NOT NULL,
  model       text NOT NULL DEFAULT 'text-embedding-3-small'
);

-- HNSW インデックス。<=> がコサイン距離なので vector_cosine_ops を指定
CREATE INDEX ON document_chunks
  USING hnsw (embedding vector_cosine_ops);

検索クエリ

import OpenAI from 'openai';
import { pool } from './db'; // node-postgres (pg) の Pool

const client = new OpenAI();
const MODEL = 'text-embedding-3-small';

export async function searchChunks(question: string, k = 5) {
  // 1. 質問を「文書と同じモデル・同じ次元」で埋め込む
  const res = await client.embeddings.create({ model: MODEL, input: question });
  const q = res.data[0].embedding;

  // 2. コサイン距離(<=>)が小さい順に上位 k 件
  //    pgvector は '[0.1,0.2,...]' 形式の文字列を受け取る
  const vectorLiteral = '[' + q.join(',') + ']';
  const { rows } = await pool.query(
    'SELECT id, content, 1 - (embedding <=> $1::vector) AS similarity ' +
    'FROM document_chunks ' +
    'WHERE model = $2 ' +
    'ORDER BY embedding <=> $1::vector ' +
    'LIMIT $3',
    [vectorLiteral, MODEL, k],
  );
  return rows;
}

<=> はコサイン距離(0 が最も近い)なので、類似度として見せたいときは 1 - 距離 に変換します。 正規化済みベクトルなら <#>(負の内積)でも同じ順位になり、こちらのほうがわずかに速いです。

コスト見積とバッチ処理

埋め込みの費用は入力トークン数だけで決まります。1 ページを約 800 トークンとすると、 1 万ページの社内文書を埋め込む費用は 800 万トークンで、3-small なら約 $0.16、3-large でも約 $1.04 です。 「初期インデックスの費用はほぼゼロ、コストの主役はベクトル DB の保存・検索側」という構造を押さえておくと、 設計判断で迷いません。

やり方向いている場面ポイント
input に配列でまとめて送るリアルタイムでない全ての埋め込み1 リクエスト最大 2,048 件・300,000 トークン。往復回数が減りレート制限にも当たりにくい
Batch API/v1/embeddings 対応)初期インデックス作成・定期的な再埋め込み料金 50% オフ。24 時間以内に非同期完了。1 バッチ最大 50,000 入力・ファイル 200 MB
encoding_format: "base64"大量のベクトルを受け取るときJSON の float 表記より転送量が大幅に減る。デコードはクライアント側で行う
dimensions を縮めるDB 容量・検索レイテンシがボトルネックのときAPI 料金は変わらないが、DB 側のコストは次元数に比例して下がる

よくある誤解と落とし穴

① 異なるモデルの埋め込みは比較できない

最も多い事故です。埋め込みベクトルの座標系はモデルごとに全く別物で、ada-002 で埋め込んだ文書を 3-small で埋め込んだ質問で検索しても、意味のある結果は返りません。 同じモデルでも dimensions が違えば次元数が合わずエラーになります。モデルを変える=全文書を再埋め込みするということなので、ベクトルと一緒にモデル名を保存し、 移行時は新旧 2 系統を並走させてから切り替える手順を最初から想定しておきます。

② 埋め込みモデルにも知識のカットオフがある

text-embedding-3 系は 2021 年 9 月以降の出来事を知りません。 ただしこれは「最新ニュースを埋め込めない」という意味ではなく、新しい固有名詞の意味を捉えにくいという意味です。 2023 年以降に登場した製品名やモデル名は、モデルから見ると意味不明の記号に近くなるため、 こうした語を含む検索はキーワード検索(BM25)で補うハイブリッド構成が有効です。

③ チャンク分割が精度の大半を決める

8,192 トークン以内に収まるからと文書を丸ごと 1 ベクトルにすると、複数の話題が平均化されてぼやけたベクトルになり、 どの質問にも中途半端に近い「何にでも引っかかる文書」が生まれます。 逆に細かすぎると文脈が失われます。数百トークン前後を目安に、見出しや段落など意味の境界で切り、前後を少し重ねるのが定石です。 チャンクの先頭に文書タイトルや見出しを付与しておくと、切り出された断片だけでは分からない文脈が補われて精度が上がります。

④ 次元数が多いほど良いわけではない

3-large の 3072 次元と、3-large を 1024 次元に縮めたものの精度差は小さく、 一方で DB 容量と検索コストは 3 倍違います。MTEB の数ポイントの差が自分のユースケースで意味を持つかは、自分のデータで検索精度(Recall@k など)を測ってから判断します。 「大は小を兼ねる」で 3072 を選ぶと、pgvector のインデックス上限にも引っかかります。

⑤ クローズドモデルへの依存はリスクとして把握する

OpenAI の埋め込みモデルはウェイトが公開されていないため、モデルが廃止されると保存済みベクトルは全て作り直しになります。 実際に ada-002 の前世代(davinci 系など)は 2024 年 1 月に停止されました。 数千万件規模のインデックスを持つなら、再埋め込みの所要時間と費用(Batch API で半額)を試算し、 必要ならオープンウェイトのモデルも比較候補に入れておくのが健全です。

まとめ

埋め込みは「テキストの意味を座標にする」変換で、意味の近さを距離計算に置き換えることで、 キーワード一致では拾えない検索・分類・推薦を可能にします。RAG の「取り出し」部分を担う、LLM アプリケーションの基礎部品です。

OpenAI で使うなら、まず text-embedding-3-small から始め、日本語を含む多言語の精度が必要なら 3-large に上げる。dimensions パラメータで容量を予算に合わせ、類似度はコサイン(=正規化済みなので内積)で測る。 そしてモデル名を保存する・チャンク分割に投資する・閾値ではなく上位 k 件で取る——この 3 点を守れば、 セマンティック検索の初期実装で踏む落とし穴はほとんど避けられます。

参考リンク

理解度チェック

問題 0 / 50%
Q1

OpenAI が埋め込みベクトルの近さを測る指標として推奨しているのは「____類似度」です。返却されるベクトルは長さ 1 に正規化されているため、内積で計算しても同じ順位になります。