なぜコストが膨らむか
OpenAI の text-embedding-3-small は 1M トークンで $0.02。安いように見えるが、検索インデックスを毎晩 全件再計算 する設計だと 無駄な再計算が累積 して気づくと月 $50〜200 になる。
私は手元のサイドプロジェクトでこれをやって痛い目を見た。3 つの工夫 で 1/10 にした実話を書く。
工夫 1: text の hash で再計算をスキップ
embedding する元 text に変化が無ければ、embedding も変化しない。当たり前だが、これを hash で機械的に検出 する仕組みが要る。
class Post < ApplicationRecord
before_save :update_embedding_hash
def update_embedding_hash
text = embedding_source_text # title + excerpt + body の連結
self.embedding_source_hash = Digest::SHA256.hexdigest(text)
end
end
class EmbedJob
def perform(post_id)
post = Post.find(post_id)
return if post.embedding_source_hash == post.last_embedded_hash
embedding = OpenAI::Client.new.embeddings.create(input: post.embedding_source_text).data.first.embedding
post.update!(embedding: embedding, last_embedded_hash: post.embedding_source_hash)
end
end
これだけで 更新があった記事だけ embedding が走る。50 記事のサイトで月 1 回更新が 5 件なら、コストは 1/10 になる。
工夫 2: dim を 1536 → 768 / 256 に絞る
text-embedding-3-small は dim を任意に指定 できる(OpenAI の Matryoshka Embeddings 機能)。
client.embeddings.create(
model: 'text-embedding-3-small',
input: text,
dimensions: 768 # ← 1536 → 768 に絞る
)
dim を半分にすると、ストレージも検索コストも半分になる。精度の劣化は 5% 未満(OpenAI の論文値)で、検索体感はほぼ変わらない。
私は最初 1536 で運用していたが、768 に下げて検索品質が変わらなかったので 256 まで下げた。これで 1536 → 256 で 6 分の 1。
工夫 3: バッチング
OpenAI の embeddings API は 入力配列で複数同時 に投げられる。
texts = posts.map(&:embedding_source_text)
response = client.embeddings.create(
model: 'text-embedding-3-small',
input: texts, # ← 配列
dimensions: 256
)
response.data.each_with_index do |item, i|
posts[i].update!(embedding: item.embedding)
end
50 件を 1 リクエストで処理できる。リクエスト数で課金されるサービス(embeddings は基本トークン課金だが、レート制限はリクエスト数)では、バッチで rate limit を回避 できる。
実測コスト
私の手元のプロジェクト(200 記事 / 月次更新 30 件)で、この 3 つを順次入れた結果:
| 状態 | 月コスト |
|---|---|
| 全件再計算 + 1536 dim | $42 |
| hash チェック追加 | $8 |
| dim 768 に変更 | $4 |
| dim 256 に変更 + バッチング | $0.60 |
70 倍の差。dim を絞るだけで効果は劇的。
落とし穴
新規記事を全件 reindex すると、hash check が効かない。これは想定通りで、新規記事 200 本の初回 embedding に $5〜10 程度かかる。これは諦めて払う。
逆に注意すべきは、body のスペースだけ変えた更新で hash がズレること。text.strip.gsub(/\s+/, ' ') のような正規化を hash 計算前に挟むと、空白の差分で再計算が走らない。
まとめ
embedding コストを下げる 3 つの工夫:
- hash で更新検出 → 変更あった記事だけ recompute
- dim を絞る (1536 → 256) → ストレージ + 計算 6 分の 1、品質 5% 劣化
- バッチング → rate limit 回避、API 呼び出し数を圧縮
3 つ全部入れると、コストは 約 1/70 になる。月 $40 が $0.6 になる感覚。
安い API でも、回数を雑に増やせば富裕税になる。最初の設計で hash と dim を考える。