🎯 「なぜかバスケットボール用品がゴルフに分類される」
ある EC カテゴリの自動マッピング(モールのカテゴリ辞書に、自社カテゴリを embedding の類似度で突き合わせる仕組み)で、こんな症状が出ていました。
バスケットボール関連の商品が、なぜか「ゴルフ」カテゴリに高い類似度でマッピングされる。
モデルもおかしくない。次元数も合っている。コサイン類似度の計算も正しい。閾値もいじっていない。にもかかわらず、検索のヒット精度がじわじわ悪い。「要レビュー」「低信頼度」の件数が想定より多い。
原因は、ベクター検索の設計でとても踏みやすく、かつ エラーも例外も出さずに静かに精度だけを下げる 落とし穴でした。それが本記事のテーマ、「参照される側(document)」と「参照する側(query)」で embedding の task_type を使い分けていなかった という問題です。
この記事は、Google 公式ドキュメントで挙動を裏取りしながら、
- ベクター検索の document 側 / query 側 という非対称性とは何か
task_type未指定が なぜ静かに精度を下げるのか- 正しい使い分けと、コードを直すだけでは直らない 運用上の落とし穴
を、AI 駆動開発の実例(一般化)とともに整理します。
🧭 前提:ベクター検索には「参照される側」と「参照する側」がある
セマンティック検索 / RAG / カテゴリマッピングのようなベクター検索は、必ず 2 種類のテキストを扱います。
| 役割 | 呼び名 | 具体例 | 性質 |
|---|---|---|---|
| 参照される側 | document | あらかじめベクトル化して DB に貯めておく文書・カテゴリ辞書・ナレッジ | 長め・網羅的・蓄積される |
| 参照する側 | query | 検索のたびに投げる質問・キーワード・突き合わせ対象 | 短め・断片的・その場で生成 |
検索とは「query のベクトルに近い document のベクトルを引く」操作です。ここで見落とされがちなのが、query と document は文章としての性質がまるで違う という点です。
- query:「バスケ シューズ」のように短く断片的
- document:「スポーツ>バスケットボール>シューズ・アクセサリ」のように構造的で長い
この 非対称性 を埋めるために、embedding モデルには「同じテキストでも、document として埋め込むか query として埋め込むかで最適化を変える」機能があります。それが task_type です。
📌 キーワード:非対称検索(asymmetric retrieval) document は「検索される文書」として、query は「検索クエリ」として、別々の最適化 でベクトル化し、両者を突き合わせる。これが retrieval(情報検索)の正攻法です。同じ最適化で揃える「対称検索」は、別の用途(文同士の類似度比較など)向けです。
⚠️ 落とし穴:task_type を指定しないと、両方が RETRIEVAL_QUERY になる
ここが本丸です。Gemini の embedding(gemini-embedding-001)には、用途別の task_type が用意されています1。
| task_type | 公式の説明(要約) | retrieval での役割 |
|---|---|---|
RETRIEVAL_DOCUMENT | ”Embeddings optimized for document search” | 保存側(document) =検索される文書 |
RETRIEVAL_QUERY | ”Embeddings optimized for general search queries” | 検索側(query) =検索クエリ |
SEMANTIC_SIMILARITY | テキスト同士の類似度評価用 | retrieval には使わない(後述) |
CLASSIFICATION / CLUSTERING | 分類 / クラスタリング用 | retrieval 対象外 |
QUESTION_ANSWERING / FACT_VERIFICATION | QA / 事実検証用 | 専用用途 |
CODE_RETRIEVAL_QUERY | コード検索クエリ用 | コード retrieval |
そして決定的な事実がこれです。
task_typeを指定しない(TASK_TYPE_UNSPECIFIED)と、RETRIEVAL_QUERYにデフォルトされる。2
公式の enum 定義では TASK_TYPE_UNSPECIFIED は “Unset value, which will default to one of the other enum values” とされ、実験的にも 未指定と RETRIEVAL_QUERY は dot product が完全一致する(=同じ最適化になる)ことが確認されています2。retrieval 用途に当てはまらない場合のデフォルト推奨も RETRIEVAL_QUERY です3。
何が起きるか
task_type をどこにも指定していないコードは、保存側も検索側も、両方とも RETRIEVAL_QUERY でベクトル化 されています。つまり:
- ✅ 本来:document は
RETRIEVAL_DOCUMENT、query はRETRIEVAL_QUERY→ 非対称検索が成立 - ❌ 実際:document も query も
RETRIEVAL_QUERY→ document が「検索される文書」として最適化されていない
ここで重要なのは、これはクラッシュしない ということです。両方 RETRIEVAL_QUERY で揃った「対称」な空間でも、コサイン類似度は計算でき、それっぽい結果も返ってきます。壊れてはいない。ただ最適ではない。 だからこそテストは通り、デモは動き、本番でだけ「なぜかヒット率が悪い」という形で表面化します。冒頭の「バスケ→ゴルフ」は、この非対称検索の不成立が有力な一因でした。
🚫 SEMANTIC_SIMILARITY を「似てるものを探す用途」と勘違いしない
名前から「類似検索なら SEMANTIC_SIMILARITY だろう」と選びたくなりますが、これは retrieval 用途には使ってはいけません。公式が明言しています。
“Do not use this for search or retrieval. It is intended for semantic textual similarity.”3
SEMANTIC_SIMILARITY は「2 つの文がどれくらい意味的に近いか」を対称的に測る用途(言い換え判定、重複検出など)向けです。document を探す retrieval では、RETRIEVAL_DOCUMENT × RETRIEVAL_QUERY の非対称ペア一択 です。
🛠️ 直し方:保存と検索を「両方セットで」直す
1. コード:保存側=DOCUMENT / 検索側=QUERY を明示する
保存(ベクトルを DB に upsert する経路)と検索(クエリをベクトル化して類似検索する経路)の両方に、明示的に task_type を渡します。
# 保存側:カテゴリ辞書をベクトル化して DB に貯める
embedding = embedding_service.generate(
category.path_name,
task_type="RETRIEVAL_DOCUMENT", # ← 検索される文書として最適化
)
# 検索側:自社カテゴリを query としてベクトル化し、類似 document を引く
query_vec = embedding_service.generate(
search_text,
task_type="RETRIEVAL_QUERY", # ← 検索クエリとして最適化
)
⚠️ 片方だけ直しても無意味 保存側だけ
RETRIEVAL_DOCUMENTにして検索側を未指定(=QUERY)のままにする、あるいはその逆は、非対称検索が成立せず効果が出ません。document=DOCUMENT と query=QUERY は 必ずペアで 揃えること。
2. 伝播漏れに注意:呼び出し経路をすべて洗う
実装で最も漏れやすいのが 「引数を足しただけで、途中の経路に渡し忘れる」 ことです。embedding 生成は往々にして複数経路を持ちます。
- 逐次生成(1 件ずつの
generate) - 大量生成のバッチ経路(数千件を Batch API でまとめて生成)
- 非同期ジョブ(Pub/Sub やワーカー経由の chunk 処理)
このうち 1 経路でも task_type の伝播を忘れると、そこだけ未指定(=QUERY)のまま動き続け、再ベクター化しても一部が古い最適化で混ざります。UI のボタン経路だけ直して非同期ジョブ経路を直し忘れる、というのが典型的な事故です。保存側の全パスが DOCUMENT になっていることを確認しましょう。
3. 運用:コードを直しても、貯めたベクトルは直らない
ここが 2 つ目の大きな落とし穴です。
デプロイしただけでは精度は改善しない。 既に DB に貯まっている document ベクトルは、間違った
task_type(QUERY 相当)で生成された値のまま だからです。
保存側を RETRIEVAL_DOCUMENT に直したら、既存の document を全件 force で再ベクター化(上書き) し、その後で再検索/再マッピングする必要があります。順序を守らないと、古いベクトルのまま検索してしまい「直したはずなのに変わらない」となります。
4. 「マスター辞書」か「学習キャッシュ」かで再生成要否が変わる
再ベクター化が必須かどうかは、その embedding の 性格 で決まります。ここを混同すると、不要な再生成をしたり、必要な再生成を飛ばしたりします。
| 性格 | 例 | task_type 修正後の対応 |
|---|---|---|
| マスター辞書 | モールのカテゴリ辞書など、検索対象として常に存在し続ける document | force 全件再ベクター化が必須(消して貯め直すと辞書自体が消える) |
| 学習キャッシュ | 処理のたびに蓄積されるキャッシュ。マスタではない | 全削除して貯め直すだけでよい(次回以降、正しい task_type で再蓄積される。再ベクター化処理は不要) |
💡 いずれの場合も 順序厳守。「① task_type 対応をデプロイ → ② 旧ベクトルの再生成 or 全削除 → ③ 再検索」。逆順だと、削除・再生成の後でも古い QUERY 相当のまま貯まってしまいます。
🆕 新しい embedding モデルでは task_type パラメータが使えない
最後に、これから設計する人向けの重要な変化です。task_type パラメータは gemini-embedding-001 専用 で、より新しい世代の embedding モデルでは扱いが変わります3。
| モデル世代 | 用途の指定方法 |
|---|---|
gemini-embedding-001 | task_type パラメータ(RETRIEVAL_DOCUMENT など enum 指定) |
より新しい世代(gemini-embedding-2 等) | プロンプト内に task を指示として記述(パラメータではなく自然言語の instruction で task:search_result のように指定) |
つまり「document / query の非対称性を意識して使い分ける」という 設計思想は不変 ですが、指定する API 表面は世代で変わる。モデルを乗り換えるときは、task_type パラメータの移植ではなく プロンプト指示への置き換え が必要になります。
💰 補足:大量の document は Batch API で半額にできる
カテゴリ辞書のようにマスター document が数千〜万件あると、再ベクター化のコストが気になります。Gemini の Batch API は embeddings に対応 しており、ターンアラウンド目標 24 時間(実際は数分〜数十分のことが多い)の非同期処理と引き換えに、料金が半額($0.075 / 1M input tokens) になります4。
- 大量・非同期で良い保存側(document の再ベクター化) → Batch API(半額・ポーリング待ち)が適合
- 1 件ずつ即時性が要る検索側(query) → 同期 API(1 件のためにポーリングするのは無駄)
カテゴリパスのような短いテキストは入力トークンが少なくコストも小さいので、全件再ベクター化でも Batch API なら十分現実的です。
✅ チェックリスト
ベクター検索を実装・レビューするときに、この記事の落とし穴を踏んでいないか確認してください。
- 保存側(document) に
RETRIEVAL_DOCUMENTを明示しているか - 検索側(query) に
RETRIEVAL_QUERYを明示しているか - 「両方セット」で揃っているか(片方だけになっていないか)
- 逐次・バッチ・非同期ジョブの 全経路 に task_type が伝播しているか
- retrieval なのに
SEMANTIC_SIMILARITYを使っていないか - コード修正後、既存 document ベクトルを再生成 したか(デプロイだけで終わっていないか)
- その embedding は マスター辞書か学習キャッシュか(再生成 or 全削除のどちらが正しいか)
- 「① デプロイ → ② 再生成 → ③ 再検索 → ④ 検証」の 順序 を守ったか
- 新世代モデルへ移行するなら、
task_typeパラメータを プロンプト指示 に置き換えたか
🧩 まとめ
ベクター検索の精度劣化は、モデルや次元数や閾値といった「目立つパラメータ」より、document と query の非対称性 という地味な設計判断に潜んでいます。
- ベクター検索には 参照される側(document) と 参照する側(query) がある
- Gemini では
task_type未指定がRETRIEVAL_QUERYにデフォルト され、document が最適化されない - この不具合は 例外を出さず、静かにヒット率だけを下げる ため見つけにくい
- 直すには 保存=DOCUMENT / 検索=QUERY を両方 揃え、既存ベクトルを再生成 し、順序を守る
AI 駆動開発では「動いているから正しい」と錯覚しやすい領域ほど、公式ドキュメントでの裏取り が効きます。本記事の挙動はすべて Google 公式の embedding ドキュメントで確認したものです。設計時に一度、task_type の使い分けを声に出して確認してください。
🔗 関連記事・用語
- embedding(埋め込み / ベクトル表現) — ベクター検索の入力となるベクトル表現
- task_type(embedding のタスクタイプ) — 非対称検索を成立させる用途別最適化
- ベクター検索 / セマンティック検索 — 意味の近さで引く検索方式
- RAG(検索拡張生成) — 検索精度が回答品質を直接左右する応用
- Batch API(Gemini) — 大量 document を半額で再ベクター化
- Gemini / Vertex AI — embedding を提供するモデル / 基盤
- AI 駆動開発が積み上げる技術的負債 — 「動くから正しい」と錯覚する構造的バイアス
- Google AI Studio 入門 — Gemini を業務投入する前の足場
Footnotes
-
Google. “Embeddings | Gemini API | Google AI for Developers”. ↩
-
technicalwriting.dev. “Understanding task types in the Gemini Embedding API”.
TASK_TYPE_UNSPECIFIEDがRETRIEVAL_QUERYと同一の dot product を返す実験的検証を含む。 ↩ ↩2 -
Google Cloud. “Choose an embeddings task type | Generative AI on Vertex AI”. ↩ ↩2 ↩3
-
Google for Developers. “Gemini Batch API now supports Embeddings and OpenAI Compatibility”. ↩