CTS-KB
AI駆動開発

ベクター検索の落とし穴:保存(document)と検索(query)で task_type を分けないとヒット率が落ちる

⏱ 約 10 分で読めます
#AI駆動開発#embedding#ベクター検索#セマンティック検索#RAG#Gemini#Vertex AI#task_type#非対称検索

🎯 「なぜかバスケットボール用品がゴルフに分類される」

ある 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_VERIFICATIONQA / 事実検証用専用用途
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_QUERYdocument が「検索される文書」として最適化されていない

ここで重要なのは、これはクラッシュしない ということです。両方 RETRIEVAL_QUERY で揃った「対称」な空間でも、コサイン類似度は計算でき、それっぽい結果も返ってきます。壊れてはいない。ただ最適ではない。 だからこそテストは通り、デモは動き、本番でだけ「なぜかヒット率が悪い」という形で表面化します。冒頭の「バスケ→ゴルフ」は、この非対称検索の不成立が有力な一因でした。

何が起きるか

✅ 正しい使い分け(非対称)

RETRIEVAL_DOCUMENT

RETRIEVAL_QUERY

document(カテゴリ辞書)

非対称retrieval 空間

query(自社カテゴリ)

query↔document の差を埋めた検索

❌ task_type 未指定(両方 QUERY)

RETRIEVAL_QUERY

RETRIEVAL_QUERY

document(カテゴリ辞書)

対称空間

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 で再ベクター化(上書き) し、その後で再検索/再マッピングする必要があります。順序を守らないと、古いベクトルのまま検索してしまい「直したはずなのに変わらない」となります。

3. 運用:コードを直しても、貯めたベクトルは直らない

①デプロイtask_type 対応版

②force 全件再ベクター化(DOCUMENT)

③全件再検索・再マッピング(QUERY)

④検証誤分類の解消確認

4. 「マスター辞書」か「学習キャッシュ」かで再生成要否が変わる

再ベクター化が必須かどうかは、その embedding の 性格 で決まります。ここを混同すると、不要な再生成をしたり、必要な再生成を飛ばしたりします。

性格task_type 修正後の対応
マスター辞書モールのカテゴリ辞書など、検索対象として常に存在し続ける documentforce 全件再ベクター化が必須(消して貯め直すと辞書自体が消える)
学習キャッシュ処理のたびに蓄積されるキャッシュ。マスタではない全削除して貯め直すだけでよい(次回以降、正しい task_type で再蓄積される。再ベクター化処理は不要)

💡 いずれの場合も 順序厳守。「① task_type 対応をデプロイ → ② 旧ベクトルの再生成 or 全削除 → ③ 再検索」。逆順だと、削除・再生成の後でも古い QUERY 相当のまま貯まってしまいます。


🆕 新しい embedding モデルでは task_type パラメータが使えない

最後に、これから設計する人向けの重要な変化です。task_type パラメータは gemini-embedding-001 専用 で、より新しい世代の embedding モデルでは扱いが変わります3

モデル世代用途の指定方法
gemini-embedding-001task_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 の使い分けを声に出して確認してください。

🔗 関連記事・用語

Footnotes

  1. Google. “Embeddings | Gemini API | Google AI for Developers”.

  2. technicalwriting.dev. “Understanding task types in the Gemini Embedding API”. TASK_TYPE_UNSPECIFIEDRETRIEVAL_QUERY と同一の dot product を返す実験的検証を含む。 2

  3. Google Cloud. “Choose an embeddings task type | Generative AI on Vertex AI”. 2 3

  4. Google for Developers. “Gemini Batch API now supports Embeddings and OpenAI Compatibility”.