Voyage embeddingのinput_typeでquery/documentを分ける理由
Voyage AIのinput_typeパラメーターは、retrievalとRAGでは省略しないよう公式ドキュメントが指示しています。内部で何が起きているかと実装例を解説します。
input_typeを省略すると何が起きるか
Voyage AIの埋め込みAPIにはinput_typeという引数があります。値は"query"か"document"のどちらかで、埋め込むテキストが検索クエリなのか検索対象のドキュメントなのかを伝えます。この引数は省略できますが、retrievalタスクとRAGでは省略しないようAnthropicのplatform docsは指示しています。
input_typeとは、埋め込み対象のテキストの役割(検索する側か、検索される側か)をAPIに伝えるパラメーターです。省略した場合やinput_type=Noneを明示的に渡した場合、Voyageは役割の区別なしにテキストをそのまま埋め込みます。指定すると検索用のベクトル表現が改善し、検索品質の向上につながるとされています。
内部で何が起きているか — プロンプトの自動前置
input_typeを指定すると、Voyageは埋め込みを計算する前に、入力テキストの先頭へ役割に応じた短いプロンプトを自動で挿入します。input_type="query"なら「Represent the query for retrieving supporting documents: 」、input_type="document"なら「Represent the document for retrieval: 」という文字列が前置されます。
公式ドキュメントが挙げている例で確認すると、実際に埋め込まれるテキストは次のように変わります。
| 元のテキスト | input_type | 実際に埋め込まれるテキスト |
|---|---|---|
| When is Apple's conference call scheduled? | input_typequery | 実際に埋め込まれるテキストRepresent the query for retrieving supporting documents: When is Apple's conference call scheduled? |
| Apple's conference call to discuss fourth fiscal quarter results... | input_typedocument | 実際に埋め込まれるテキストRepresent the document for retrieval: Apple's conference call to discuss fourth fiscal quarter results... |
つまりinput_typeは単なるメタデータのフラグではなく、モデルに渡す実際の入力テキストを書き換える引数です。同じ文でも、queryとして埋め込んだベクトルとdocumentとして埋め込んだベクトルは、この前置文の分だけ異なる値になります。
なぜクエリと文書で違うプロンプトを使うのか
検索クエリと検索対象の文書は、同じ話題を扱っていても文の形がまったく違います。クエリは短い疑問文や断片的なキーワードになりがちで、文書は完結した説明文であることが多くなります。この非対称さをモデルに教えないまま同じ埋め込み方をすると、文の形の違いがノイズになり、意味が近いクエリと文書でもベクトル空間上で離れてしまうことがあります。
前置プロンプトで役割を明示するのは、この非対称さをモデル側に伝えて吸収させるための工夫と読めます。クエリ側には「これは検索に使う短い問いです」、文書側には「これは検索される側の内容です」という前提を与えることで、形の違いを飛び越えて意味の近さを拾いやすくする狙いです。
この非対称性は、実際の運用フローにも反映されます。文書側の埋め込みは、インデックスを構築する段階で一括してinput_type="document"で計算し、ベクトルDBに保存しておくのが基本です。クエリ側の埋め込みは、ユーザーが検索するたびにinput_type="query"でその場で計算します。同じembed()メソッドでも、呼ばれるタイミングと頻度がまったく違う2つの処理だと捉えておくと、実装を分けて考えやすくなります。
実装コード — queryとdocumentを分けて呼ぶ
import voyageai
vo = voyageai.Client()
query = "When is Apple's conference call scheduled?"
document = (
"Apple's conference call to discuss fourth fiscal quarter results "
"and business updates is scheduled for Thursday, November 2, 2023 "
"at 2:00 p.m. PT / 5:00 p.m. ET."
)
# 検索クエリ側
query_embd = vo.embed([query], model="voyage-4", input_type="query").embeddings[0]
# 検索対象の文書側
doc_embd = vo.embed([document], model="voyage-4", input_type="document").embeddings[0]同じembed()メソッドを呼ぶだけで、input_typeの値を変えるだけで済みます。実装コストは値を1つ渡すだけでほぼゼロなので、検索品質の改善が見込めるなら指定しない理由がない引数です。
input_typeが要らない場面もある
input_typeが効くのは、クエリで文書を検索するretrieval・RAGの構造を持つタスクに限られます。公式ドキュメントがinput_typeの使用を求めているのも「retrievalタスクとRAGのユースケース」であって、埋め込みの用途すべてではありません。
たとえば、テキストを事前に決めたカテゴリへ分類するタスクや、似た文書同士をグルーピングするクラスタリングでは、「検索する側/される側」という非対称な関係そのものが存在しません。すべてのテキストが同じ役割で扱われるため、input_typeを指定する意味がなくなります。なお公式ドキュメントのFAQには、任意の指示文を入力テキストの前に追加できるよう訓練されたvoyage-large-2-instructというvoyage-2世代のモデルへの言及がありますが、モデル一覧のページには載っておらずFAQの本文にしか出てこないため、現行のラインアップに含まれているか、現在も利用できるものなのかは、この記事の調査範囲では確認できていません。
つまり判断軸は「クエリと文書という非対称な関係があるか」の1点です。関係があるならinput_typeを指定し、無いなら指定不要というのが基本の切り分けになります。分類やクラスタリングのタスクにモデルを使う場合は、input_typeの指定要否だけでなく、そのモデルが現行のラインアップに含まれているかどうかも、実装に着手する前に、公式ドキュメントの最新版でひと手間かけて確認しておくと安全です。記事やブログの名前だけを頼りに古い世代のモデルを組み込んでしまうと、後から差し替えが必要になり、思わぬところで手戻りが発生します。
検索の全体像で見るとどこに効くか
input_typeの効果は、埋め込み単体ではなく検索の一連の流れの中で確認するとわかりやすくなります。公式ドキュメントのクイックスタート例では、6件の候補文書をinput_type="document"で埋め込み、ユーザーの質問をinput_type="query"で埋め込んだうえで、両者のドット積(内積)を計算して最も近い候補を探します。
import numpy as np
# 候補文書側(input_type="document")
doc_embds = vo.embed(documents, model="voyage-4", input_type="document").embeddings
# ユーザーの質問側(input_type="query")
query_embd = vo.embed([query], model="voyage-4", input_type="query").embeddings[0]
# Voyageの埋め込みは正規化済みなので、内積 = コサイン類似度
similarities = np.dot(doc_embds, query_embd)
retrieved_id = np.argmax(similarities)ここで両方に同じinput_type(たとえば両方とも"document"、あるいは両方とも省略)を使うと、前置されるプロンプトが揃ってしまい、クエリと文書という役割の違いをモデルに伝えられません。documents側とquery側でinput_typeを出し分けて初めて、このコードが想定通りに機能します。
内積で類似度を計算できているのも、Voyageの埋め込みが長さ1に正規化されているからです。正規化済みのベクトル同士では、内積とコサイン類似度が同じ値になります。もし別のモデルやカスタムの後処理でベクトルの長さが1からずれていると、同じnp.dotのコードでも類似度の大小関係が崩れることがあります。次元を切り詰める場合(先頭部分だけを取り出す運用)は正規化をやり直す必要がある点も、あわせて押さえておくと安全です。
省略した影響をどう見積もるか
input_typeを省略しても埋め込みAPI自体はエラーを返さないため、影響に気づくのは「検索精度がなんとなく悪い」という主観的な違和感がきっかけになりがちです。数値で確認するには、想定している検索クエリと、それに対応する正解文書のペアを数十件用意し、input_typeを正しく指定した場合と省略した場合とで、上位N件に正解文書が入るかどうかを比較します。
この比較はinput_typeを指定するかどうか以外の条件(モデル・次元数・チャンクの切り方)を揃えて行うのが前提です。条件を揃えないまま比較すると、精度差がinput_typeによるものなのか他の要因によるものなのか切り分けられません。既存のRAGパイプラインにinput_typeを後から入れる場合は、導入前後でこの簡易評価を1回ずつ回しておくと、効果を数値で説明できます。
評価用のペアは、実際のユーザークエリのログから抽出するのが理想です。開発時に自分で作った質問文は、想定通りの言い回しに寄りがちで、本番のクエリが持つ言葉の揺れ(表記ゆれ・省略・口語的な言い回し)を反映しません。ログが無い初期段階では、想定ユーザーが実際に打ちそうな短い疑問文を複数パターン用意し、丁寧な完全文とは意図的に文体を変えておくと、クエリと文書の非対称さをより実態に近い形で再現できます。
よくあるつまずき
input_typeを省略したまま本番に出す: ローカルの動作確認では埋め込みが返ってくるため、省略していても一見エラーにはなりません。しかも検索結果を並べても見た目では判別できないため、気づかないまま運用されがちです。RAGパイプラインの実装時に、query側とdocument側それぞれのembed()呼び出しへinput_typeを明示するチェックをコードレビューの項目に加えておくと防ぎやすくなります。既存コードに後からinput_typeを追加する場合は、grepで.embed(の呼び出し箇所を洗い出しておくと漏れを防げます。
queryとdocumentを取り違える: ユーザー入力を埋め込む箇所には"query"、インデックス対象のチャンクを埋め込む箇所には"document"を割り当てます。バッチでチャンクを埋め込む処理と、リアルタイムでユーザー入力を埋め込む処理が別のコードパスに分かれているプロジェクトでは、どちらのパスがどちらの値を使うべきか、実装前に一度整理しておくと取り違えを避けやすくなります。取り違えても埋め込みベクトル自体は正常な値として返るため、コードレビューでの目視確認が実質的な最後の防波堤になります。目視以外の手当てとしては、同じ文字列を"query"と"document"の両方で埋め込み、返ってきた2本のベクトルを比べる方法があります。前置きが効いていれば別々のベクトルになるので、ほぼ一致するようなら値がAPIまで届いていないと分かります。
まとめ
input_typeは"query"か"document"かをVoyageに伝えるだけの引数に見えますが、実際には入力テキストの先頭に役割別のプロンプトを前置する仕組みです。省略すると、この前置が働かないまま埋め込みが計算されます。実装コストはほぼゼロなので、retrieval・RAG用途でembed()を呼ぶときは、queryとdocumentの両方の呼び出しに必ず明示します。エラーにならない引数だからこそ、明示しないまま放置されやすい点には注意します。
埋め込みをRAGパイプライン全体に組み込む手順はClaude RAGの構築ガイド、チャンク分割の精度を上げる前処理はContextual Retrievalで扱っています。