Pythonで文章をembedding(埋め込みベクトル)に変換する方法は、OpenAIのEmbeddings APIを呼ぶか、sentence-transformersでモデルを手元で動かすかの2つが中心です。この記事では、2026年10月時点の最新版であるopenai 3.40.0とsentence-transformers 6.2.0のコードで、入力上限・料金の見積もり・日本語モデルの接頭辞の扱いまでを実装の順に説明します。sentence-transformers側のコードは実際にモデルを動かして出力を確かめ、OpenAI側はSDKが送るリクエストをローカルのモック(本物のAPIの代わりに応答する検証用サーバー)で確認しています。
まとめ:Pythonでembeddingを作る手段の選び方と実装の要点
- 手早く始めるならOpenAI Embeddings APIの
text-embedding-3-small(100万トークンあたり0.02ドル・1,536次元)。本記事のサンプルで測った1文字あたり約1.1トークンで換算すると、日本語500字×10万件の埋め込み生成は約1.1ドルです。 - 文書を社外に出せない、または同じ文書を何度も埋め込み直すならsentence-transformersでローカル実行。日本語は
multilingual-e5系か、最大8,192トークンを扱えるruri-v3系が候補です。 - e5とruri-v3は、sentence-transformers 6.2.0の
encode_query()を呼んでも接頭辞が付きません。prompt=で明示します。 - ruri-v3の出力は正規化されておらず、そのまま内積を取ってもコサイン類似度と一致しません。
model.similarity()かnormalize_embeddings=Trueを使います。 - 型番やエラーコードの完全一致検索はembeddingだけでは取りこぼすため、キーワード検索と組み合わせます。
Pythonでembeddingを作る3つの手段と選び方
手段の違いは、計算をどこで行うかです。OpenAI APIは計算を外部に任せ、sentence-transformersは自分のCPUやGPUで計算し、Ollamaは手元に立てたサーバーに計算させます。どのモデルを選ぶか(日本語ベンチマークの読み方やライセンス)は埋め込みモデルとは?仕組みと日本語モデルの選び方・RAG実装での判断基準【2026年版】で扱い、この記事はPythonで動かす手順に絞ります。
| 手段 | 計算場所 | 費用 | 入力上限 | 向く条件 |
|---|---|---|---|---|
| OpenAI Embeddings API | OpenAIのサーバー | 0.02〜0.13ドル/100万トークン | 8,192トークン | 件数が少ない・すぐ試したい |
| sentence-transformers | 自分のCPU/GPU | 計算資源のみ | モデル次第(e5は512) | 社外送信不可・再計算が多い |
| Ollama | 手元のサーバー | 計算資源のみ | モデル次第 | 複数アプリから共通で呼ぶ |
判断の分かれ目は、データを外へ送れるかと、埋め込み直しの頻度です。APIはモデルの準備が要らない代わりに、文書を再びAPIへ送って埋め込むたびに課金されます。社内規程や契約などで文書を社外へ送れないなら、費用に関係なくローカルが前提です。送れる場合は、月あたりの埋め込み直しのトークン数に単価を掛けたAPI費用と、ローカル実行に使うサーバーの費用を比べて決めます。精度を確かめる段階では、まずCPUかAPIで始め、処理が期限に間に合わなくなってからGPUを足す順番で十分です。
OpenAI Embeddings APIでベクトル化する手順
OpenAIのPython SDKは2026年10月27日に3.40.0が公開され、Python 3.10以上が必要です(PyPIのopenai)。0.x系のopenai.Embedding.create()は現行SDKでは動かないため、古い記事のコードをそのまま貼ると失敗します。
OpenAI APIキーの発行と環境変数への設定
APIキーはOpenAIのダッシュボード(platform.openai.com)のAPI keysページで発行します。表示は発行時の1回だけなので、その場で控えて環境変数OPENAI_API_KEYに入れます。SDKは引数を省略するとこの環境変数を読むため、コードにキーを書く必要はありません。
pip install openai==3.40.0
export OPENAI_API_KEY="sk-..." # Windows PowerShellは $env:OPENAI_API_KEY="sk-..."
client.embeddings.createによる1件・複数件のベクトル化
from openai import OpenAI
client = OpenAI() # OPENAI_API_KEY を自動で読む
texts = ["有給休暇の申請方法", "経費精算の締め日", "社内Wi-Fiにつながらない"]
resp = client.embeddings.create(model="text-embedding-3-small", input=texts)
vectors = [d.embedding for d in resp.data] # list[list[float]]
print(len(vectors), len(vectors[0])) # 3 1536
print(resp.usage.total_tokens) # 課金対象のトークン数
inputにリストを渡すと1回のリクエストでまとめて埋め込めます。APIリファレンスの上限は、1入力あたり8,192トークン、配列は2,048要素以下、1リクエストの合計300,000トークンです。返ってくるdataにはindexが付くので、大量に送るときは入力順との対応をindexで確認します。
なおencoding_formatを指定しない場合、SDK 3.40.0は内部で"base64"を要求し、受け取った値をfloatのリストに戻します(ローカルのモック相手にリクエスト本文を記録して確認)。戻り値の型は変わらないため、コード側で対応する必要はありません。
text-embedding-3-small・large・ada-002の料金と次元
| モデル | 既定の次元 | 料金/100万トークン | MTEB |
|---|---|---|---|
| text-embedding-3-small | 1,536 | 0.02ドル | 62.3% |
| text-embedding-3-large | 3,072 | 0.13ドル | 64.6% |
| text-embedding-ada-002 | 1,536 | 0.10ドル | 61.0% |
料金はOpenAIの料金表、次元とMTEBの値は公式の埋め込みガイドの記載です。ada-002はsmallより単価が高く、ガイド掲載のMTEBでも下回るため、新規構築ではsmallかlargeを選びます。既存のada-002のベクトルと混ぜて検索する必要がある場合だけ、全件を埋め込み直すまでada-002を使い続けます。課金は入力トークンだけです。
トークン数はtiktokenのcl100k_baseで数えます(ガイドが3世代目の埋め込みモデルに指定)。日本語の社内規程風の文12本(452字)を数えると495トークンで、1文字あたり約1.1トークンでした。この比率で見積もると、500字の文書10万件(約5,500万トークン)はsmallで約1.1ドル、largeで約7.1ドルです。
dimensionsで次元を減らすときの注意
text-embedding-3系はdimensionsで出力を短くできます。ガイドによると、largeを256次元に縮めてもMTEBでada-002の1,536次元を上回ります。ベクトルDBの次元上限が1,024なら、largeにdimensions=1024を指定すれば収まります。
resp = client.embeddings.create(
model="text-embedding-3-large",
input="次元を1024に縮める例",
dimensions=1024,
)
print(len(resp.data[0].embedding)) # 1024
保存済みの3,072次元を後から先頭だけ切り出す場合は、長さが1でなくなるため正規化し直す必要があります。ガイドも、生成時にdimensionsを渡す方法を推奨しています。
sentence-transformersでローカルにembeddingを作る手順
sentence-transformersは2026年10月21日に6.2.0が公開され、Python 3.10以上で動きます(PyPI)。初回にHugging Faceからモデルをダウンロードし、以降はキャッシュから推論できます。ただし既定ではモデルの読み込み時にHubへ問い合わせるため、外部通信を止めたい環境ではSentenceTransformer(..., local_files_only=True)を指定してキャッシュだけを読ませます。
インストールと日本語モデルの読み込み
pip install sentence-transformers==6.2.0
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("cl-nagoya/ruri-v3-130m", device="cpu")
queries = ["休みを取りたいときはいつまでに言えばいい?"]
docs = [
"有給休暇の申請は、取得日の3営業日前までに勤怠システムから上長へ提出してください。",
"経費精算は月末締めで、翌月5日までに申請します。",
]
q = model.encode_query(queries, prompt="検索クエリ: ")
d = model.encode_document(docs, prompt="検索文書: ")
print(model.similarity(q, d)) # tensor([[0.8981, 0.8237]])
日本語で候補になる2系統の仕様を並べます。数値はモデルカード(multilingual-e5-large、ruri-v3-130m)の記載です。
| モデル | 次元 | 最大長 | 接頭辞(検索時) | ライセンス |
|---|---|---|---|---|
| multilingual-e5-small | 384 | 512トークン | query: / passage: | MIT |
| multilingual-e5-large | 1,024 | 512トークン | query: / passage: | MIT |
| ruri-v3-30m | 256 | 8,192トークン | 検索クエリ: / 検索文書: | Apache 2.0 |
| ruri-v3-130m | 512 | 8,192トークン | 検索クエリ: / 検索文書: | Apache 2.0 |
e5は512トークンを超えた部分を切り捨てます。長い規程やマニュアルをそのまま入れると後半が検索に効かないため、e5を使うなら文書を分割してから埋め込みます。分割の単位とサイズの決め方はチャンクとトークンの違いとは?チャンク分割の手法とRAGのサイズ設計を日本語で実測【2026年版】で扱っています。
e5・ruri-v3でのencode_queryと接頭辞の指定
sentence-transformersのリファレンスでは、encode_query()はモデルに登録されたqueryプロンプトを使うと説明されています。ところが6.2.0でe5とruri-v3を読み込むと、model.promptsは{'query': '', 'document': ''}で中身が空でした。実際にencode_query("テスト")とencode("テスト")の結果は一致し、接頭辞は付いていません。公式クイックスタートが例に使うQwen/Qwen3-Embedding-0.6Bのようにプロンプトを設定済みのモデルと、e5・ruri-v3では挙動が違います。
e5・ruri-v3では、上のコードのようにprompt="query: "やprompt="検索クエリ: "を渡します。prompt=付きのencode_query()と、文字列を手で連結してencode()した結果が一致することも確認しました。e5のモデルカードは、類似文検索のように左右が対等な用途では両側にquery:を付けるよう指示しています。
実測:日本語12問で比べた接頭辞の有無と処理速度
接頭辞を付け忘れるとどの程度ずれるのかを、社内FAQ風の文書12本と、言い換えた質問12問で測りました(「立て替えたお金はどうやって戻してもらう?」→経費精算の文、など語の重なりを減らした組)。検証環境はDocker上のPython 3.12(CPU 4コア・Intel Core i9-9880H)、torch 2.14.1のCPU版、transformers 5.21.0です。各質問の類似度はnormalize_embeddings=Trueで埋め込んだうえでmodel.similarity()(コサイン類似度)で計算しました。12問は小さな試験なので、1問の差からモデルや条件の優劣は確定できません。
| モデル | 接頭辞の付け方 | 正解が1位 | 平均の余裕 |
|---|---|---|---|
| e5-small | query: / passage: | 10/12 | 0.031 |
| e5-small | なし | 10/12 | 0.029 |
| e5-small | 両側に passage: | 8/12 | 0.010 |
| e5-large | query: / passage: | 11/12 | 0.036 |
| e5-large | なし | 11/12 | 0.028 |
| e5-large | 両側に passage: | 9/12 | 0.024 |
| ruri-v3-130m | 検索クエリ: / 検索文書: | 11/12 | 0.054 |
| ruri-v3-130m | なし | 12/12 | 0.046 |
| ruri-v3-30m | 検索クエリ: / 検索文書: | 10/12 | 0.054 |
| ruri-v3-30m | なし | 11/12 | 0.046 |
「平均の余裕」は、正解文書の類似度から、正解以外の11本のうち最も高い類似度を引いた値の平均です。正解が1位でない質問では負の値になります。正しい接頭辞を付けると、どのモデルでも余裕は広がりました。ただし1位の正答数はほぼ変わらず、接頭辞なしで大きく崩れることはありませんでした。害が大きかったのは、質問側にもpassage:を付ける誤った組み合わせで、e5はsmallとlargeの両方で正答が2問減りました。接頭辞はモデルカードどおり付け、少なくとも質問側と文書側を取り違えないことが優先です。
同じ環境で短文256本(1文40字前後)をbatch_size=32で埋め込んだ速度は、ruri-v3-30mが毎秒196文、e5-smallが毎秒71文、ruri-v3-130mが毎秒14〜23文(2回計測)、e5-largeが毎秒5文でした。たとえば短文10万件なら、ruri-v3-30mは約9分、e5-largeは約5時間半の計算になります。文書が長いほど時間は延びるため、自分の文書で数百件を測って全件の所要時間を見積もり、取り込みの期限に収まらないときにGPUを検討します。
生成したembeddingの類似度計算と保存
OpenAIの埋め込みは長さ1に正規化されているため、内積を取ればそのままコサイン類似度になります(埋め込みガイドのFAQ)。e5の出力も長さ1でした。一方、ruri-v3は正規化されておらず、「検索クエリ: 休みの申請」を埋め込んだベクトルの長さは130mで84、30mで54でした(長さは入力ごとに変わります)。この状態の内積は、文の意味の近さに加えてベクトルの長さの影響を受けるため、モデルカードが前提とするコサイン類似度とは順位が食い違うことがあります。
q = model.encode("検索クエリ: 休みの申請")
d = model.encode("検索文書: 有給休暇の申請は、取得日の3営業日前までに勤怠システムから上長へ提出してください。")
print(q @ d) # 6002.9(未正規化の内積。コサイン類似度ではない)
print(float(model.similarity(q, d))) # 0.9005(既定はコサイン類似度)
qn = model.encode("検索クエリ: 休みの申請", normalize_embeddings=True)
dn = model.encode("検索文書: 有給休暇の申請は、取得日の3営業日前までに勤怠システムから上長へ提出してください。", normalize_embeddings=True)
print(qn @ dn) # 0.9005(正規化後は内積=コサイン類似度)
ベクトルDBの距離関数を内積(dot product)にするなら、保存前にnormalize_embeddings=Trueで揃えておきます。計算式と、ユークリッド距離との関係はコサイン類似度とは?計算式・仕組みと類似検索での実装を実装目線で解説で詳しく説明しています。
件数が少ないうちは、NumPy配列をnp.save()で保存し、1回の検索ごとに全件と総当たりで計算する方法で始められます。1検索あたりの応答時間を測り、許容できる時間(画面表示なら数百ミリ秒など)を超えた時点で近似最近傍探索に切り替えます。その段階では、Pythonから扱いやすいChroma(ChromaDB)とは?読み方・使い方と日本語検索の実測、pgvectorとの違いや、ベクトルDBとは?仕組み・インデックス構造・製品比較とpgvectorで動かす手順【2026年版】で紹介している製品へ移ります。
Ollamaで手元の埋め込みサーバーを呼ぶ方法
同じモデルを複数のアプリから使うなら、Ollamaにモデルを置いてHTTPで呼ぶ構成も取れます。pip install ollamaで入るのはPythonクライアントだけなので、先にOllama本体をインストールしてサーバーを起動しておく必要があります。Pythonクライアントのollama.embed()は文字列か文字列のリストを受け取り、embeddingsにベクトルの配列を返します。Ollamaの公式ドキュメントによると返却ベクトルはL2正規化済みです。
# Ollama本体のインストールと起動を済ませたうえで
pip install ollama
ollama pull embeddinggemma # Googleの300Mパラメータの埋め込みモデル
import ollama
resp = ollama.embed(model="embeddinggemma", input=["有給休暇の申請方法", "経費精算の締め日"])
print(len(resp.embeddings), len(resp.embeddings[0]))
注意点は、/api/embedのtruncateが既定でtrueなことです。コンテキスト長を超えた入力はエラーにならず切り詰められるため、長い文書が後半を失っても気づけません。truncate=Falseを渡すとエラーで止まるので、取り込み処理ではこちらを使い、分割漏れを検知します。
embeddingで検索を作るときに失敗しやすい場面
embeddingは言い換えに強い反面、文字列の完全一致が必要な検索には向きません。型番(「XJ-2041」)、エラーコード、条文番号、人名のように、1文字違えば別物になる語は、意味が近い別の文書を上位に出してしまいます。こうした語を含む業務文書を検索するなら、BM25などのキーワード検索と並べて走らせ、順位をRRF(Reciprocal Rank Fusion)とは?計算式とk=60の意味・製品別設定・RAG-Fusionでの使い方で統合する構成にします。
もう1つの失敗は、モデルを後から変えることです。text-embedding-3-smallのベクトルとruri-v3のベクトルは次元も空間も異なり、混ぜて比べられません。モデルを替えるときは全文書の埋め込み直しが必要です。日本語中心でデータ量が多い業務なら、APIで試作した段階で、本番もAPIを使い続けるのかローカルモデルへ移すのかを決めておきます。検索精度をさらに上げたいときは、上位候補だけをクロスエンコーダとは?バイエンコーダとの違いと精度・速度の判断基準【2026年版】で並べ替える方法があります。
社内文書を対象にした検索やRAGを、モデル選定から評価まで含めて設計する場合は、一創のRAG構築支援でも相談を受け付けています。
よくある質問
OpenAIのembedding APIは無料で使えますか?
無料枠はなく、入力トークン数に応じた従量課金です。text-embedding-3-smallは100万トークンあたり0.02ドルです。本記事のサンプルで測った1文字あたり約1.1トークンで換算すると、日本語約90万字分に当たります(文の内容によって比率は変わります)。費用をかけずに試すなら、sentence-transformersでruri-v3やe5をローカル実行します。
日本語のembeddingに向いたモデルはどれですか?
APIならtext-embedding-3-smallかlarge、ローカルならruri-v3(日本語特化・最大8,192トークン)かmultilingual-e5が候補です。CPUだけで大量に処理するなら、本記事の計測で毎秒196文だったruri-v3-30mが扱いやすい選択です。
8,192トークンを超える長い文書はどう埋め込みますか?
OpenAI APIは1入力8,192トークンを超えるとエラーになるため、事前に段落や見出し単位で分割し、分割した単位ごとに埋め込みます。tiktokenのcl100k_baseで数えてから送ると、上限超過を送信前に検知できます。
embeddingの次元数はどう決めればよいですか?
まずはモデルの既定値(smallなら1,536)で始め、保存容量や検索速度が問題になってからdimensionsで縮めます。ベクトルDBの次元上限がある場合は、その上限に合わせてlargeを縮める方法が使えます。
OpenAI APIで作ったembeddingをローカルモデルに切り替えられますか?
ベクトルをそのまま流用することはできません。モデルごとに次元と空間が異なるため、切り替えるときは全文書をローカルモデルで埋め込み直し、質問側も同じモデルで埋め込みます。