Beautiful HD Wallpapers(iOS / Android、累計5,000万DL超)のバックエンドに、Gemini API の Embedding を使ったカテゴリ自動分類機能を追加しようとしたときのことです。公式ドキュメントのサンプルコードは数行で動いたのですが、実際に壁紙画像の説明文を数百件処理し始めた途端に、3つの問題が重なって出てきました。
個人開発でモバイルアプリを2014年から運営していると、こうした「ドキュメントには書かれていない落とし穴」に繰り返しぶつかります。エラーメッセージだけでは原因を特定しにくいものが多いので、実際に踏んだ手順と修正の流れをそのまま記録しておきます。同じ状況で悩んでいる方の参考になれば幸いです。
INVALID_ARGUMENT — 途中で次元数を変えると既存インデックスが壊れる
Gemini の text-embedding-004 モデルはデフォルトで768次元のベクトルを返します。output_dimensionality パラメータを使えば任意の次元数に変更できるのですが、一度インデックスを構築した後に次元数を変えると、INVALID_ARGUMENT エラーが発生して処理が止まります。
私が実際にぶつかったのは、最初に output_dimensionality=3072 でインデックスを作成し、後でコスト削減のために768次元に変更しようとしたケースでした。エラーメッセージは「引数が間違っている」としか出ないため、最初はAPIの呼び出し方が悪いのかと思い、30分ほど公式ドキュメントを読み直しました。次元数の不一致が原因だと気づいたのは、ChromaDBのコレクション設定を改めて確認したときでした。
原因が分かれば対処は単純で、インデックスと output_dimensionality を一致させるか、新しい次元数でインデックスを作り直すかのどちらかです。コスト削減を優先するなら768次元でインデックスを再構築する必要があります。壁紙アプリでは件数が多かったため、週末の深夜に再生成バッチを走らせて対応しました。
import google.generativeai as genai
genai.configure(api_key="YOUR_GEMINI_API_KEY")
# ❌ 既存の3072次元インデックスに768次元ベクトルを投げると INVALID_ARGUMENT
result = genai.embed_content(
model="models/text-embedding-004",
content="美しい山の景色",
output_dimensionality=768, # インデックスと不一致
)
# ✅ インデックスと次元数を揃える
result = genai.embed_content(
model="models/text-embedding-004",
content="美しい山の景色",
output_dimensionality=3072, # 既存インデックスに合わせる
)次元数の選び方について補足すると、768次元と3072次元の検索精度の差は、個人開発規模のデータ量では体感しにくいほど小さいことがあります。精度よりもストレージコストを優先するなら768次元で十分です。いずれにせよ、最初に決めた次元数は変えないようにしましょう。output_dimensionality は「後から調整できるパラメータ」ではなく「インデックスと一緒に固定する設定」と理解しておくと、このエラーを防げます。
RESOURCE_EXHAUSTED(429)— バッチ処理でレートリミットに詰まる
カテゴリ自動分類のために壁紙の説明文を500件一気に処理しようとして、RESOURCE_EXHAUSTED エラーが出続けました。Gemini API の Embedding エンドポイントには1分あたりの呼び出し制限があります。
最初は50msのインターバルを入れれば大丈夫だろうと思っていたのですが、それでも429が続きました。特にフリープランでは制限が厳しく、連続呼び出しではあっという間に上限に達してしまいます。
指数バックオフを実装することで安定しました。エラーが発生するたびに待機時間を倍にしていく方法で、最初は1秒、次が2秒、その次が4秒と増えていきます。5回試みてもダメなら例外を投げる設計にしています。
import time
import google.generativeai as genai
def embed_with_backoff(content: str, max_retries: int = 5) -> list[float]:
"""指数バックオフ付きの Embedding 呼び出し"""
for attempt in range(max_retries):
try:
result = genai.embed_content(
model="models/text-embedding-004",
content=content,
task_type="RETRIEVAL_DOCUMENT",
)
return result["embedding"]
except Exception as e:
if "429" in str(e) or "RESOURCE_EXHAUSTED" in str(e):
wait_sec = 2 ** attempt # 1, 2, 4, 8, 16 秒
print(f"Rate limit: {wait_sec}s 待機({attempt + 1}回目)")
time.sleep(wait_sec)
else:
raise
raise RuntimeError("最大リトライ回数を超過しました")呼び出しの間には最低でも1秒のインターバルを入れることをおすすめします。100msでは不十分でした。また、数百件を超えるバッチ処理は、昼間の処理よりも夜間に分散して実行する方がレートリミットに引っかかりにくいです。
もう一つ、バッチ全体でリトライ設計をする場合は、途中で失敗しても最初からやり直さないよう、処理済みのインデックスをファイルに保存しておく必要があります。私は500件の処理が400件目で失敗したとき、最初からやり直す羽目になりました。それ以来、チェックポイントのファイル保存を必ず実装するようにしています。
task_type を指定しないと RAG の精度が低いままになる
エラーが出なくなった後も、カテゴリ分類の検索精度が思ったより出ないという問題が残りました。「自然な山の風景」で検索しても、関係のない壁紙が上位に来てしまう状況が続きました。
調べてみると、原因は task_type の指定漏れでした。Gemini API の Embedding モデルは用途に応じて最適なベクトル空間を使い分けます。検索クエリとインデックスに格納するドキュメントで、それぞれ異なる task_type を指定しないと、モデルが汎用のベクトル空間を使ってしまい、検索精度が落ちます。
# ❌ task_type 未指定 — 汎用ベクトル空間で精度が落ちる
result = genai.embed_content(
model="models/text-embedding-004",
content="ナチュラルな壁紙を探しています",
)
# ✅ 検索クエリには RETRIEVAL_QUERY
query_result = genai.embed_content(
model="models/text-embedding-004",
content="ナチュラルな壁紙を探しています",
task_type="RETRIEVAL_QUERY",
)
# ✅ ドキュメント格納時は RETRIEVAL_DOCUMENT
doc_result = genai.embed_content(
model="models/text-embedding-004",
content="緑の葉と木漏れ日が差し込む自然の景色",
task_type="RETRIEVAL_DOCUMENT",
)task_type を正しく設定した後、コサイン類似度ベースの検索精度が実測で15〜20%改善されました。この変更の良いところは、クエリ側だけ変えれば既存インデックスを作り直さずに効果を確認できる点です。まずクエリに RETRIEVAL_QUERY を設定して精度を計測し、改善を確認してからドキュメント側も RETRIEVAL_DOCUMENT で再生成するという順番が現実的です。
3点を最初に決めておくと後が楽になる
これらのエラーを一通り経験した後は、Embedding を組み込む前に次の3点を必ず最初に決めるようにしています。次元数・task_type の用途分け・バックオフの設計は、後から変えようとすると既存データの再生成が必要になることが多く、最初に設計しておいた方が明確に楽です。
# 壁紙アプリで採用した Embedding 設定(参考)
EMBEDDING_CONFIG = {
"model": "models/text-embedding-004",
"output_dimensionality": 768, # 最初に決めて変更しない
"query_task_type": "RETRIEVAL_QUERY",
"doc_task_type": "RETRIEVAL_DOCUMENT",
"batch_interval_sec": 1.0, # 本番は 1 秒インターバル
"max_retries": 5, # 指数バックオフ 5 回まで
}廣川政樹(Dolice)は個人開発でこうした実運用上の問題を一つひとつ解決しながら、2014年からiOS/Androidアプリを運営してきました。公式ドキュメントには書かれていない落とし穴が多い分野だからこそ、実際に踏んだエラーと対処法を残しておくことに意味があると感じています。
まず試すなら task_type から
クエリ側に RETRIEVAL_QUERY を設定するだけなら数分で終わります。インデックスの再構築も不要なため、変更コストが最も低い改善策です。Gemini API Embedding を使った RAG 実装で精度に悩んでいる場合は、ここから試してみてください。
Embedding モデルの選択について補足
text-embedding-004 以外に gemini-embedding-exp-03-07 など実験的なモデルも存在しますが、個人開発の本番環境では安定稼働を優先して text-embedding-004 を使い続けています。実験モデルは精度が高い反面、予告なく挙動が変わることがあるため、特に数百件以上のデータを一度に処理するバッチ処理では、本番モデルの方が扱いやすいと感じています。
モデルの選定は最初の設計段階で決めてしまうのが賢明です。後からモデルを切り替えると、新しいモデルのベクトル空間が既存モデルと異なるため、インデックスの全データを再生成する必要が出てきます。次元数の変更と同じく、「後から変えると影響が大きい」判断の一つです。
RAG の精度をさらに上げるために
task_type の設定で精度が改善された後、さらに精度を上げるために試したことがあります。一つは、カテゴリ名だけでなく説明文を一緒に Embedding すること。「自然」というカテゴリ名だけより、「山・森・湖・空など自然の景色を収録した壁紙」という説明文を含めた方が、クエリとの類似度が高くなりました。
もう一つは、検索クエリを前処理することです。ユーザーが入力する検索語はバラエティがあるため、クエリをそのまま Embedding するより、短い説明文に変換してから Embedding した方が精度が安定しました。これはGemini API を使ったプロンプトで行っており、壁紙アプリの検索体験の改善につながっています。
Gemini Embedding API は、設定を正しく整えれば個人開発のスケールでも十分に実用できます。最初の3つのエラーさえ乗り越えれば、その後の開発体験はかなりスムーズです。