導入部
「ベクトル検索を導入したけれど、思ったようなドキュメントがヒットしない」
AI開発の現場で、このような課題に直面したことはないでしょうか。
例えば、「2024年のAIトレンドに関するレポートを見せて」と質問したのに、2021年の古いドキュメントが上位に来てしまったり、「技術部門の議事録だけ探したい」のに、営業資料が混ざり込んだりする。これは、ベクトル検索が「意味の近さ(Semantic Similarity)」には強い反面、「条件による厳密な絞り込み」を苦手としているために起こる現象です。
多くのRAG(Retrieval-Augmented Generation)システムが直面するこの「検索精度の壁」を突破する鍵となるのが、LangChainのSelf-Query Retrieverです。これは、ユーザーの自然言語クエリを解析し、ベクトル検索用のクエリと、メタデータフィルタリング用の構造化クエリに自動分解してくれる強力なコンポーネントです。
しかし、公式ドキュメント通りに実装しても、日本語環境ではなかなか期待通りに動いてくれないことがあります。「フィルタが効かない」「意図しない条件が生成される」といったトラブルは、開発現場でよく見られる課題です。
本記事では、Self-Query Retrieverを日本語環境で実用的に使いこなすための技術仕様と実装パターンを解説します。単なる使い方の紹介ではなく、内部プロンプトのオーバーライドや、LLMに誤解させないスキーマ定義のポイントなど、現場の課題解決に直結する現実的なアプローチを提示します。
ベクトル検索の潜在能力を引き出し、ユーザーが本当に求めている情報を届けられるシステムを構築していきましょう。
1. Self-Query Retriever アーキテクチャ仕様
RAGの検索精度を高めるうえで、Self-Query Retrieverが内部で何を行っているのか、そのメカニズムを正確に把握することは極めて重要です。ここを論理的に理解しておくことで、意図したフィルタリングが機能しない際のトラブルシューティングがスムーズになります。
コンポーネント構成図
Self-Query Retrieverは、単一の機能ではなく、複数のコンポーネントが連動するパイプライン処理です。LangChainの現行アーキテクチャにおける大まかなデータフローは以下のようになります。
- User Query: ユーザーが自然言語で質問を入力(例:「東京の30代男性向けの保険商品は?」)。
- Query Construction Chain: LLMがクエリを解析し、二つの要素に分解します。
- Search Query: ベクトル検索に使うキーワードや文章(例:「保険商品」)。
- Filter: メタデータに基づく絞り込み条件(例:
AND(eq("location", "Tokyo"), eq("gender", "male"), gt("age", 30)))。 - 注意点: OpenAI APIを利用する場合、2026年2月にGPT-4o等のレガシーモデルが廃止され、GPT-5.2が新たな標準モデルへと移行しています。そのため、プロンプトやチェーンの動作検証は、GPT-5.2などの最新モデルを明示的に指定して再テストすることを推奨します。
- Structured Query Translator: 生成された中間形式の構造化クエリを、使用しているVector Store(Chroma、Pinecone、Weaviateなど)固有のフィルタ構文に変換します。
- Vector Store: 変換されたフィルタ条件を適用しながら、ベクトル検索を実行します。
Query Construction Chainの役割
このアーキテクチャの核となるのが、LLMを用いたQuery Construction Chainです。ここでLLMは、「翻訳者」としての役割を果たします。
重要なのは、LLMが直接データベースを操作するわけではないという点です。LLMはあくまで、LangChainが定義した「抽象構文木(Abstract Syntax Tree)」を生成するだけです。この抽象的なクエリを、具体的なDBクエリ(SQLのWHERE句や、ElasticsearchのQuery DSLのようなもの)に変換するのは、後続のTranslatorの役目です。ChatGPTのような高度な推論能力を持つ最新のAPIモデルを選択することで、複雑な自然言語の条件指定であっても、この抽象構文木を正確に生成できるようになります。
Structured Query Translatorの仕組み
各ベクトルデータベースは、フィルタリングの記法が全く異なります。例えば、ChromaDBは$eqや$andといったMongoDBライクな構文を使いますが、Pineconeは異なるJSON構造を要求します。
LangChainのSelfQueryRetrieverは、初期化時に指定されたVector Storeに応じて適切なTranslatorを自動的に(あるいは明示的に)選択します。これにより、開発者はDBごとの細かな構文差異を意識することなく、統一的なインターフェースでフィルタリングを実装できます。
ただし、昨今ではPineconeのServerlessアーキテクチャの普及や、Qdrantなどへの移行によるコスト最適化など、ベクトルデータベースを取り巻く環境は絶えず変化しています。各Vector Storeの最新のフィルタ構文やサポート状況については、必ず公式ドキュメントで最新情報を確認することをお勧めします。Translatorが差異を吸収してくれるとはいえ、根底にあるデータベースの仕様変更には常に注意を払う必要があります。
2. クラス初期化とパラメータ詳細リファレンス
実装の第一歩は、SelfQueryRetrieverクラスの正しい初期化です。主要メソッドであるfrom_llmの引数仕様を網羅的に解説します。ここの設定が、後の挙動を大きく左右するため、丁寧な設定が求められます。
from_llm() メソッドの引数仕様
from langchain.retrievers.self_query.base import SelfQueryRetriever
retriever = SelfQueryRetriever.from_llm(
llm=llm, # 構造化クエリ生成に使用するLLM
vectorstore=vectorstore, # 検索対象のVector Store
document_contents=document_content_description, # ドキュメントの概要説明
metadata_field_info=metadata_field_info, # メタデータスキーマ(後述)
structured_query_translator=translator, # 任意のTranslator(通常は自動推論)
chain_kwargs={"verbose": True}, # 内部Chainへの引数
enable_limit=True, # 検索件数制限(k)の動的変更を許可するか
use_original_query=False, # フィルタ生成に失敗した場合のフォールバック設定
verbose=True # デバッグ情報の出力
)
llm_chain_kwargs の設定オプション
chain_kwargsは、内部で動くLLMChainに渡されるパラメータです。ここに{"verbose": True}を設定しておくと、LLMがどのようなプロンプトを受け取り、どのような生のレスポンスを返したかがコンソールに出力されます。開発中はTrueにしておくことで、問題の切り分けが容易になります。
search_type と search_kwargs の挙動
Self-Query Retriever自体はリトリーバーですが、内部的にはVector Storeの検索メソッドを呼び出しています。デフォルトでは類似度検索(similarity search)が行われますが、MMR(Maximal Marginal Relevance)などを使いたい場合は、search_type="mmr"やsearch_kwargs={"k": 5}などを設定可能です。ただし、Self-Queryによって生成されたフィルタが優先されるため、kの値などは動的に上書きされる可能性があります。
3. メタデータスキーマ定義 (AttributeInfo) 仕様
検索精度を最も大きく左右するのが、このメタデータスキーマの定義です。LLMはこの定義書だけを頼りにフィルタを作ります。人間には通じても、LLMには通じない定義が多々あるため、論理的かつ明確な記述が必要です。
AttributeInfo クラスの構造
from langchain.chains.query_constructor.base import AttributeInfo
metadata_field_info = [
AttributeInfo(
name="genre",
description="The genre of the movie. One of ['science fiction', 'comedy', 'drama', 'thriller', 'romance', 'action', 'animated']",
type="string",
),
AttributeInfo(
name="year",
description="The year the movie was released",
type="integer",
),
# ...
]
description フィールドの記述ルール
ここが最大のポイントです。descriptionは単なるメモではありません。LLMへのプロンプトの一部として機能します。
- 列挙型は全リストを書く: カテゴリカルなデータ(ジャンル、部署名など)の場合、取りうる値をリスト形式で明記してください。
One of ['A', 'B', 'C']という形式が最もLLMに理解されやすいです。 - 日本語か英語か: フィールド名(
name)はデータベースのカラム名に合わせる必要がありますが(通常は英数字)、descriptionはLLMが理解できる言語であれば日本語でも構いません。しかし、実務の現場での一般的な傾向として、英語で記述した方がLLMの解釈精度が高いと考えられます。特に複雑な条件の場合、英語の方が論理的な曖昧さが少ないためです。 - 同義語を含める: ユーザーが異なる言葉で検索する可能性がある場合、descriptionに同義語を含めるとヒット率が上がります。例:
description="Department name. Also known as 'Division' or 'Unit'."
type フィールドの許容データ型
typeには、string, integer, float, booleanなどが指定できます。ここで誤った型を定義すると(例:実際は文字列なのにintegerと定義する)、生成されたクエリがDB側で型エラーを引き起こします。Vector Store側のスキーマと厳密に一致させてください。
4. クエリ変換プロンプトのカスタマイズ仕様
デフォルトのプロンプトは英語ベースであり、一般的なケースには対応していますが、日本語特有の表現や、複雑なビジネスロジックには対応しきれないことがあります。ここで、現場の課題に即したプロンプトエンジニアリングが重要になります。
デフォルトプロンプトの構造解析
LangChainのデフォルトプロンプトは、Few-Shot(少数の例示)形式を採用しています。「ユーザーの入力」→「構造化クエリ」という例をいくつか見せることで、LLMにタスクを学習させています。
しかし、デフォルトの例はすべて英語です。日本語のクエリ(例:「〜を除く」「〜かつ〜」)が入力された際、LLMが混乱して誤った演算子を選択することがあります。
日本語クエリ向けプロンプトテンプレートの修正
精度を高めるには、日本語のFew-Shot事例を追加したカスタムプロンプトを作成し、それをfrom_llmに渡すのが実用的です。
from langchain.chains.query_constructor.base import (
get_query_constructor_prompt,
StructuredQueryOutputParser
)
# 日本語のFew-Shot例を定義
examples = [
(
"2020年以降に公開されたSF映画で、コメディ以外のものを探して",
{
"query": "SF映画",
"filter": "and(gt(\"year\", 2020), eq(\"genre\", \"science fiction\"), ne(\"genre\", \"comedy\"))"
}
),
(
"東京か大阪にある予算1000万以下のプロジェクト",
{
"query": "プロジェクト",
"filter": "and(or(eq(\"location\", \"Tokyo\"), eq(\"location\", \"Osaka\")), lt(\"budget\", 10000000))"
}
)
]
# プロンプトの生成
prompt = get_query_constructor_prompt(
document_contents,
metadata_field_info,
examples=examples # カスタム事例を注入
)
# Retrieverの初期化時に渡す
retriever = SelfQueryRetriever.from_llm(
# ...他の引数
chain_kwargs={"prompt": prompt}
)
このように、日本語特有の「〜以外(ne)」「〜か〜(or)」といった論理構造を明示的に教えることで、意図通りのフィルタが生成される確率が格段に向上します。
5. 実装コード例とレスポンスオブジェクト
ここでは、ChromaDBを使用した具体的な実装コードを示します。LangChainのバージョンはlangchain>=0.1.0を想定しています。
また、Self-Query Retrieverを構築する際のLLMには、クエリの意図を正確に解釈する高い推論能力が求められます。OpenAI APIを利用して新たにシステムを構築・移行する場合、GPT-4o等のレガシーモデルから、100万トークン級のコンテキストウィンドウと高度な推論(Thinking機能)を備えた「GPT-5.2」を標準モデルとして指定することが推奨されます。
基本実装パターン (ChromaDB)
import os
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.chains.query_constructor.base import AttributeInfo
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain_core.documents import Document
# 1. Vector Storeの準備(ダミーデータ)
docs = [
Document(
page_content="A bunch of scientists bring back dinosaurs and mayhem breaks out",
metadata={"year": 1993, "rating": 7.7, "genre": "science fiction"},
),
Document(
page_content="Leo DiCaprio gets lost in a dream within a dream within a dream ...",
metadata={"year": 2010, "director": "Christopher Nolan", "rating": 8.2},
),
# ... 他のドキュメント
]
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(docs, embeddings)
# 2. メタデータスキーマの定義
metadata_field_info = [
AttributeInfo(
name="genre",
description="The genre of the movie",
type="string",
),
AttributeInfo(
name="year",
description="The year the movie was released",
type="integer",
),
AttributeInfo(
name="rating",
description="A 1-10 rating for the movie",
type="float",
),
]
document_content_description = "Brief summary of a movie"
# 3. LLMの準備(最新の標準モデル GPT-5.2 を指定し、温度は0にして決定論的にする)
llm = ChatOpenAI(model="gpt-5.2", temperature=0)
# 4. Self-Query Retrieverの構築
retriever = SelfQueryRetriever.from_llm(
llm,
vectorstore,
document_content_description,
metadata_field_info,
verbose=True
)
# 5. 実行
# 「8.5点より高い評価がついた映画」 -> rating > 8.5 のフィルタが生成される
results = retriever.invoke("I want to watch a movie rated higher than 8.5")
for doc in results:
print(f"Content: {doc.page_content}")
print(f"Metadata: {doc.metadata}")
この実装では、映画のあらすじとメタデータ(ジャンル、公開年、評価)を持つダミーデータを用意し、ChromaDBに格納しています。
重要なのは、AttributeInfoを用いて各メタデータの型と説明を明確に定義している点です。これにより、指定したLLMがユーザーの自然言語クエリから適切なフィルタ条件を推論し、ベクトルデータベース向けの構造化されたクエリを生成できます。LLMのtemperatureを0に設定することで、フィルタ生成の挙動を決定論的(一貫性のある結果)に保つ工夫も施しています。
構造化クエリの確認方法
SelfQueryRetriever.from_llmの引数でverbose=Trueに設定しているため、実行時にコンソールへ以下のようなログが出力されます。
query='movie' filter=Comparison(comparator=<Comparator.GT: 'gt'>, attribute='rating', value=8.5)
これを確認することで、LLMが「8.5より高い(GT: Greater Than)」と正しく解釈したか、それとも「8.5以上(GTE: Greater Than or Equal)」と解釈したかなどを詳細に検証できます。日本語の曖昧なクエリを入力した場合でも、このログを通じて意図通りの比較演算子(Comparator)が適用されているかを確認するプロセスは、検索精度のチューニングにおいて非常に重要です。
さらに、最終的なレスポンスとして返却されるのは、メタデータフィルタリングとベクトル検索の両方を通過したDocumentオブジェクトのリストです。各Documentオブジェクトには、テキスト本体を格納するpage_contentと、抽出条件として使われたmetadataがそのまま保持されています。この仕様により、検索結果をユーザーインターフェースに表示する際、該当ドキュメントのテキストだけでなく、なぜそのドキュメントが選ばれたのかという根拠(評価スコアや公開年など)も併せて提示することが容易になります。
6. エラーハンドリングとトラブルシューティング
本番環境で運用する際、最も注意すべきなのがLLMの出力揺れによるエラーです。Self-Query Retrieverは堅牢なツールですが、万能ではありません。現実的な運用を見据えた対策が必要です。
パースエラー (OutputParserException) の対処
LLMが期待されたJSON形式以外のテキスト(例:「はい、わかりました。クエリは以下の通りです...」といった余計な前置き)を出力してしまい、パースエラーが発生することがあります。
これを防ぐには、OutputFixingParserの利用を検討するか、より強力なモデル(ChatGPTなど)を使用することが推奨されます。また、LangChain v0.1系ではfix_parserオプションは標準のfrom_llmには含まれていない場合があるため、カスタムチェーンを組む必要があるかもしれません。
フィルタ条件の空振り対策
よくあるのが、LLMが張り切ってフィルタを作りすぎ、結果として「ヒット件数ゼロ」になるケースです。例えば、「面白い映画」というクエリに対し、メタデータに存在しないgenre="omoshiroi"というフィルタを勝手に作ってしまうような場合です。
対策としては、AttributeInfoのdescriptionに「このフィールドに含まれる値以外は推測しないでください」と強い制約を加えることが有効です。また、use_original_query=Trueを設定しておくと、構造化クエリの生成に失敗した際に、元の自然言語クエリを使った通常のベクトル検索にフォールバックしてくれるため、UXの低下を防ぐことができます。
まとめ
Self-Query Retrieverは、RAGシステムの検索精度を飛躍的に高める強力な武器です。しかし、それを使いこなすには、単にライブラリをインポートするだけでなく、以下の3つのポイントを押さえた「実装の工夫」が必要です。
- 正確なスキーマ定義: LLMが理解しやすい言葉で、データの型と内容を詳細に記述する。
- プロンプトの日本語化: 日本語特有の論理条件をFew-Shotで教え込み、解釈ミスを減らす。
- 防御的なエラーハンドリング: パースエラーや空振りを想定し、フォールバック戦略を用意する。
これらの技術を適用することで、ユーザーの曖昧な質問に対しても、的確なフィルタリングと高精度な回答を提供できるRAGシステムが構築できるはずです。費用対効果を意識しながら、現場の課題解決に直結するシステム開発を進めていきましょう。
あなたの開発するAIサービスが、ユーザーにとって真に役立つものになることを応援しています。
コメント