Claude Media
Voyage embeddingのinput_typeでquery/documentを分ける理由

Voyage embeddingのinput_typeでquery/documentを分ける理由

Voyage AIのinput_typeはAnthropicのdocsでは省略禁止、Voyage本家のリファレンスでは推奨と書き方が分かれます。前置プロンプトの中身と、省略時の影響を解説します。

input_typeを省略すると何が起きるか

Voyage AIの埋め込みAPIにはinput_typeという引数があります。値は"query"か"document"のどちらかで、埋め込むテキストが検索クエリなのか検索対象のドキュメントなのかを伝えます。input_typeは省略可能で、既定値はNoneです。

省略した場合やinput_type=Noneを渡した場合、Voyageは入力をそのままベクトルに変換します。指定すると役割に応じた短いプロンプトが前置され、検索向けに調整されたベクトルが返ります。

内部で何が起きているか — プロンプトの自動前置

input_typeを指定すると、Voyageは埋め込みを計算する前に、入力テキストの先頭へ役割に応じた短いプロンプトを自動で挿入します。Voyage本家のリファレンスは、この前置文を「透明性のため」として明示しています。

くらべる

input_typeごとに前置される文字列

検索する側

query

前置される文字列はRepresent the query for retrieving supporting documents: です。公式の例では、When is Apple's conference call scheduled?がこの文字列の後ろに続く形でモデルへ渡ります。

検索される側

document

前置される文字列はRepresent the document for retrieval: です。公式の例では、Apple's conference call to discuss fourth fiscal quarter results...がこの後ろに続きます。

つまりinput_typeは単なるメタデータのフラグではなく、モデルに渡す実際の入力テキストを書き換える引数です。同じ文でも、queryとして埋め込んだベクトルとdocumentとして埋め込んだベクトルは、この前置文の分だけ異なる値になると考えられます。

なぜクエリと文書で違うプロンプトを使うのか

検索クエリと検索対象の文書は、同じ話題を扱っていても文の形がまったく違います。クエリは短い疑問文や断片的なキーワードになりがちで、文書は完結した説明文であることが多くなります。この非対称さをモデルに教えないまま同じ埋め込み方をすると、文の形の違いがノイズになり、意味が近いクエリと文書でもベクトル空間上で離れてしまうことがあります。

前置プロンプトで役割を明示するのは、この非対称さをモデル側に伝えて吸収させるための工夫と読めます。Voyageは、前置によって検索向けに調整したベクトルになると説明しています。

この非対称性は、運用の流れにも表れます。

手順

query と document を呼び分けるタイミング

  1. 1

    インデックス構築時: document

    文書やチャンクを一括でinput_type="document"で埋め込み、ベクトルDBに保存します。

  2. 2

    検索のたび: query

    ユーザーの入力はその場でinput_type="query"で埋め込み、保存済みのベクトルと突き合わせます。

実装コード — queryとdocumentを分けて呼ぶ

まずライブラリを入れます。

pip install -U voyageai

VOYAGE_API_KEY環境変数にAPIキーを設定しておけば、voyageai.Client()が自動で読みます。

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]

呼び出すメソッドは同じで、input_typeの値だけが違います。

HTTP APIを直接使う場合は、リクエストボディのJSONにinput_typeを入れます。値の候補はnull・query・documentで、既定はnullです。

curl https://api.voyageai.com/v1/embeddings \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $VOYAGE_API_KEY" \
  -d '{
    "input": ["When is Apple'"'"'s conference call scheduled?"],
    "model": "voyage-4",
    "input_type": "query"
  }'

input_typeが要らない場面もある

input_typeが効くのは、クエリで文書を検索するretrieval・RAGの構造を持つタスクに限られます。Anthropicのdocsがinput_typeの使用を求めているのも「retrievalタスクとRAGのユースケース」であって、埋め込みの用途すべてではありません。

たとえば、テキストを事前に決めたカテゴリへ分類するタスクや、似た文書同士をグルーピングするクラスタリングでは、「検索する側/される側」という非対称な関係そのものがありません。

Anthropicのdocsには、分類やクラスタリングの話題でvoyage-large-2-instructが出てきます。このモデルは入力テキストの前に任意の指示文を足すよう訓練されており、input_typeの代わりにVoyageのGitHubで公開されている指示文集を使う形です。Voyage本家のモデル一覧では、このモデルは「Older models」(いまもAPIから使える旧世代)に置かれ、16,000トークン・1024次元と書かれています。同じ一覧に、既存ユーザーにはvoyage-3への移行を勧める注記もあります。

判断軸は「クエリと文書という非対称な関係があるか」の1点です。関係があればinput_typeを指定し、なければ指定の要否を気にする必要はありません。

検索の全体像で見るとどこに効くか

input_typeの効果は、埋め込み単体ではなく検索の一連の流れの中で見るとわかりやすくなります。Anthropicのdocsのクイックスタート例では、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に正規化されているからです。正規化済みのベクトル同士では、内積とコサイン類似度が同じ値になり、コサイン類似度とユークリッド距離は同じ順位を返します。

次元を切り詰める場合(先頭部分だけを取り出す運用)は、正規化をやり直す必要があります。次元削減の実装はVoyage embeddingを次元削減する方法で扱っています。

文脈付きチャンク埋め込みでは取り扱いが変わる

voyage-context-4などの文脈付きチャンク埋め込みは、embed()ではなくcontextualized_embed()で呼びます。この関数にもinput_typeがあり、既定はNoneです。検索用途ではqueryかdocumentの指定を推奨する点はembed()と同じです。

違いは、使える組み合わせが入力の形で決まることです。Voyage本家の入力検証表では、次のようになっています。

用途inputsの形input_type
分割済みの文書を埋め込むinputsの形List[List[str]]input_typedocument
クライアント側で分割して埋め込むinputsの形List[List[str]]input_typedocument
バックエンドの自動チャンク分割(enable_auto_chunking=True)inputsの形List[str]input_typedocument
クエリを埋め込むinputsの形List[str]またはList[List[str]]input_typequery

自動チャンク分割を有効にするときは、input_typeがdocumentであることが条件です。クエリを入れ子のリストで渡す場合、内側のリストには1件のクエリだけを入れます。

省略した影響をどう見積もるか

input_typeは省略可能で既定値がNoneなので、省略した呼び出しもそのまま通ります。影響に気づくのは「検索精度がなんとなく悪い」という違和感がきっかけになりがちです。

ここで手がかりになるのが、Voyage本家のリファレンスにある一文です。input_typeを付けて作った埋め込みと付けずに作った埋め込みは「compatible(互換)」と書かれています。両者は同じ種類のベクトルとして混ぜて扱えるので、既存のインデックスが省略のまま作られていても、クエリ側だけqueryにして効果を比べる選択肢があります。ただし、互換であることは精度が同じという意味ではありません。

数値で確認するには、想定している検索クエリと正解文書のペアを数十件用意し、input_typeを正しく指定した場合と省略した場合とで、上位N件に正解文書が入るかどうかを比べます。モデル・次元数・チャンクの切り方は揃えます。条件を揃えないと、精度差がinput_typeによるものか他の要因によるものか切り分けられません。

評価用のペアは、実際のユーザークエリのログから抽出するのが理想です。開発時に自分で作った質問文は想定通りの言い回しに寄りがちで、本番のクエリが持つ表記ゆれや口語的な言い回しを反映しません。ログがない段階では、想定ユーザーが打ちそうな短い疑問文を複数用意し、丁寧な完全文とは文体を変えておきます。

よくあるつまずき

input_typeを省略したまま本番に出す: 省略可能な引数なので、ローカルの動作確認では気づきにくくなります。RAGパイプラインの実装時に、query側とdocument側それぞれのembed()呼び出しへinput_typeを明示するチェックをコードレビューの項目に加えておくと防げます。既存コードに後から追加するときは、grepで.embed(の呼び出し箇所を洗い出すと漏れを減らせます。

queryとdocumentを取り違える: ユーザー入力を埋め込む箇所には"query"、インデックス対象のチャンクを埋め込む箇所には"document"を割り当てます。バッチ処理とリアルタイム処理が別のコードパスに分かれていると、取り違えが起きやすくなります。取り違えても埋め込みベクトル自体は正常な値で返るため、コードレビューでの目視が実質的な最後の防波堤です。補助的な確認として、同じ文字列を"query"と"document"の両方で埋め込み、返ってきた2本のベクトルを比べる方法があります。前置文が異なるため、2本は別の値になると考えられ、完全に一致するならinput_typeが反映されていない可能性があります。

まとめ

input_typeは、入力テキストの先頭に役割別のプロンプトを前置する引数です。retrieval・RAG用途では、文書の埋め込みに"document"、クエリの埋め込みに"query"を付けます。Anthropicのdocsは省略を禁じ、Voyage本家は推奨にとどめていますが、検索用途で指定する側に倒せば、どちらのページにも反しません。

RAG全体への組み込みはClaude RAGの構築ガイド、チャンクに文脈を足す前処理はContextual Retrievalで扱っています。

この記事を共有:XはてブLinkedIn