社内ドキュメント検索やRAG(検索拡張生成)の構築において、「ベクトル検索を導入したが、製品型番で検索しても正しいドキュメントが出ない」という課題が開発現場でしばしば挙げられます。
意味理解に優れたベクトル検索は画期的ですが、一文字の違いが重要な「型番」や「社内固有の略語」には、従来のキーワード検索が適しているケースがあります。
そこで注目されているのが、両者の利点を組み合わせた「ハイブリッド検索」です。
本記事では、単一手法の限界といった理論的背景から、PythonとLangChainを用いたハイブリッド検索システムの構築手順までをコードを交えて解説します。システムの挙動を段階的に分解し、実務で応用できるレベルの体系的な理解を目指しましょう。
1. 検索アルゴリズムの特性とユーザーの検索意図のミスマッチ
実装の前に問題を構造的に理解しましょう。検索精度が上がらない主な原因は、ツールの選定ミスではなく、検索アルゴリズムの特性とユーザーの検索意図のミスマッチにあります。
「意味はわかるが型番に弱い」ベクトル検索の弱点
ベクトル検索(Dense Retrieval)はテキストを多次元ベクトルに変換し、意味の近さを計算します。「PCの調子が悪い」というクエリで「トラブルシューティング」をヒットさせるなど、意味の理解に優れています。
しかし、この「意味の抽象化」が弱点にもなります。例えば「製品A-123」と「製品A-124」はベクトル空間上で近くに配置されやすいため、「A-123」の検索結果に「A-124」の仕様書が上位表示される現象が起こります。
「完全一致しかできない」キーワード検索の限界
一方、キーワード検索(Sparse Retrieval / BM25など)は単語の出現頻度に基づきます。「A-123」という文字列の有無で判定するため型番検索には有効ですが、「休暇を取りたい」というクエリに対し、ドキュメントに「有給取得」としか記載がなければヒットしません。用語の定義が厳密に一致している必要があります。
両者を組み合わせる「ハイブリッド検索」の数学的メリット
ハイブリッド検索は、これら2つの異なるロジックの検索結果を統合(アンサンブル)する手法です。
ここで重要なのがRRF (Reciprocal Rank Fusion)という考え方です。具体的には以下の処理を行います。
- キーワード検索での順位を出す
- ベクトル検索での順位を出す
- それぞれの順位の逆数(1/順位)をスコアとして足し合わせる
これにより、「両方で上位のドキュメント」や「片方で圧倒的に上位のドキュメント」が総合的に高く評価され、RAGの回答精度(Context Precision)の底上げに繋がります。
2. 開発環境のセットアップとデータ準備
再現性を重視し、Google ColabなどのNotebook環境でも動作する構成で環境を構築します。
必要なライブラリのインストール
LangChainの最新エコシステムに合わせ、基本ライブラリ、OpenAI専用パッケージ、ベクトル検索用のfaiss-cpu、キーワード検索用のrank_bm25をインストールします。
# 必要なライブラリのインストール
# langchain-openai: OpenAIモデル専用の統合パッケージ
# langchain-community: コミュニティ主導の統合機能(BM25など)を含む
!pip install langchain langchain-community langchain-openai faiss-cpu rank_bm25 tiktoken
続いてモジュールのインポートとAPIキーの設定を行います。APIキーは環境変数やシークレット管理機能を利用して安全に管理してください。
import os
from langchain_openai import OpenAIEmbeddings
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import FAISS
from langchain.retrievers import EnsembleRetriever
from langchain_core.documents import Document
# APIキーの設定
# Google ColabのSecrets機能や環境変数から読み込むのがベストプラクティスです
# os.environ["OPENAI_API_KEY"] = "sk-..."
検証用ダミーデータの作成:社内規定と技術仕様書
ハイブリッド検索の真価を確認するため、「文脈理解が必要な文章(社内規定)」と「完全一致が重要な識別子(製品コード)」が混在するデータセットを定義します。
# 検証用ドキュメントリスト
docs_list = [
# --- 社内規定系(ベクトル検索が得意な意味的領域) ---
"社員は、年次有給休暇を年間最低5日間取得する義務があります。申請は2週間前までに行ってください。",
"交通費の精算は、月末締め翌月10日払いです。領収書の原本提出が必須となります。",
"リモートワーク規定:週3回までの在宅勤務を認めます。始業・終業時のチャット報告が必要です。",
# --- 製品仕様書・型番系(キーワード検索が得意な記号的領域) ---
"製品コード: PROD-X100。仕様: 電圧100V、重量5kg。次世代モデル。",
"製品コード: PROD-X200。仕様: 電圧200V、重量7kg。産業用モデル。",
"エラーコード: ERR-999。原因: 通信タイムアウト。対処法: 再起動してください。",
]
# Documentオブジェクトに変換
documents = [Document(page_content=text) for text in docs_list]
チャンク化戦略:検索精度を左右する分割のコツ
実務で長文ドキュメントを扱う場合、チャンク化(分割)戦略が検索精度を大きく左右します。ハイブリッド検索では以下のトレードオフを考慮します。
- チャンクが大きすぎる場合: キーワード密度が下がり、BM25のスコアリング精度が低下する可能性があります。
- チャンクが小さすぎる場合: 文脈が分断され、ベクトル検索の意味理解が不正確になる恐れがあります。
テクニカルライティングの観点からは、ドキュメントの構造(見出しや段落)を意識した分割が推奨されます。一般的な日本語ドキュメントでは、300〜500文字程度を目安に、文脈の連続性を保つためのオーバーラップ(重複部分)を持たせて分割するのがバランスの良いアプローチです。意味のまとまりを維持することで、検索後のLLMによる回答生成も安定します。
3. 実践Part 1:2つの検索手法を個別に実装・比較する
統合の前に各Retriever(検索器)を単体で作成し、挙動の違いを確認します。開発の基本である「要素への分解と単体テスト」を行うことで、各手法の特性を正確に把握でき、後のチューニングがスムーズになります。
BM25によるキーワード検索の実装と挙動確認
まずはキーワード検索(BM25)の実装です。BM25Retrieverはlangchain_communityパッケージに含まれています。
from langchain_community.retrievers import BM25Retriever
# BM25 Retrieverの初期化
# 文書リスト(documents)は前のステップで作成したものを使用
bm25_retriever = BM25Retriever.from_documents(documents)
bm25_retriever.k = 2 # 上位2件を取得
# テスト検索:型番で検索
print("--- BM25: 'PROD-X100' で検索 ---")
results = bm25_retriever.invoke("PROD-X100")
for doc in results:
print(f"- {doc.page_content}")
予想される結果: PROD-X100を含むドキュメントがヒットします。BM25は出現頻度の低い単語を重要視するため、型番などのユニークな文字列に強く反応します。
OpenAI Embeddingsを用いたベクトル検索の実装と挙動確認
次に、FAISSとOpenAIの埋め込みモデルを使用したベクトル検索です。
※セキュリティ確保のため、各ライブラリは最新の安定版を使用してください。
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
# Embeddingsモデルの準備(コストパフォーマンスに優れたモデルを指定)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# VectorStoreの作成
faiss_vectorstore = FAISS.from_documents(documents, embeddings)
faiss_retriever = faiss_vectorstore.as_retriever(search_kwargs={"k": 2})
# テスト検索:概念で検索
print("\n--- Vector: '休みを取りたい' で検索 ---")
results = faiss_retriever.invoke("休みを取りたい")
for doc in results:
print(f"- {doc.page_content}")
予想される結果: 「休み」という単語が直接含まれていなくても、「年次有給休暇」を含むドキュメントがヒットします。単語の一致ではなく意味の近さを計算するのがベクトル検索の強みです。
実験:同じクエリで全く違う結果が出ることを確認する
ここで「PROD-X100の不具合」という複合的なクエリを試してみます。
- BM25の場合: 「PROD-X100」に強く反応しますが、ドキュメント内に「不具合」という単語がなければスコアが伸び悩みます。
- Vector検索の場合: 「不具合」の意味に引きずられ、文脈が似ている別の製品の「エラーコード: ERR-999」のドキュメントを上位に出す可能性があります。
このように単一の手法では複合的な意図を取りこぼすリスクがあり、これがハイブリッド検索の必要性を示しています。
4. 実践Part 2:EnsembleRetrieverによるハイブリッド検索の構築
LangChainのEnsembleRetrieverを使用し、キーワード検索とベクトル検索を効率的に統合するハイブリッド検索を実装します。
LangChainのEnsembleRetrieverで2つを統合する
EnsembleRetrieverは、複数のRetrieverとその重み付け(weights)を引数に取り、異なる検索結果をブレンドします。
※実装の際は公式ドキュメントで最新の仕様をご確認ください。
# ハイブリッド検索(Ensemble Retriever)の初期化
# weightsの合計は通常1.0になるように設定します
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, faiss_retriever],
weights=[0.5, 0.5] # BM25とVectorを50:50で評価
)
# ハイブリッド検索の実行(invokeメソッドを使用)
query = "PROD-X100の電源について知りたい"
print(f"\n--- Hybrid Search: '{query}' ---")
results = ensemble_retriever.invoke(query)
for i, doc in enumerate(results):
print(f"{i+1}. {doc.page_content}")
重み付け(Weights)の調整による検索結果の変化
上記コードの weights=[0.5, 0.5] は両者を対等に扱う設定ですが、実務ではこの比率調整が検索精度を左右します。ユーザーの検索行動(ユースケース)を分析し、最適なバランスを設定します。
- 製造業・ECサイト(型番検索が多い場合):
weights=[0.7, 0.3]程度とし、BM25を強めに設定します。 - ヘルプデスク・FAQ(自然言語での質問が多い場合):
weights=[0.3, 0.7]程度とし、ベクトル検索を強めに設定します。
まずは 0.4 (BM25) : 0.6 (Vector) 程度から開始し、実際のクエリと検索結果を分析しながら最適なバランスを見つけることを推奨します。
RRF(順位融合)アルゴリズムが働く仕組み
EnsembleRetrieverの内部では、各検索器の順位に基づくRRF(Reciprocal Rank Fusion)アルゴリズムが動作しています。
例えば、BM25で1位、Vectorで5位だったドキュメントのスコアは以下のようになります(定数k=60の場合)。
$ Score = \frac{1}{60 + 1} + \frac{1}{60 + 5} $
一方、両方で3位だった場合は以下のようになります。
$ Score = \frac{1}{60 + 3} + \frac{1}{60 + 3} $
このように、複数の手法で「安定して上位」のドキュメントの方が最終スコアが高くなりやすい傾向があります。これによりノイズが排除され、信頼性の高い結果が提示されます。
5. 実務運用のためのチューニングとトラブルシューティング
実運用環境へ移行すると様々な壁に直面します。ここでは現場で遭遇しやすい課題と実践的な対策を解説します。
ドメイン特有の用語辞書の必要性
キーワード検索の精度は、テキストの単語分割(分かち書き)に大きく依存します。
例えば「スマートウォッチ」を「スマート」と「ウォッチ」に分割すると、意図しない文書がヒットする可能性があります。社内用語や製品名は、形態素解析器(MeCabやSudachiなど)のユーザー辞書に登録し、「一語」として認識させることが重要です。
また、システム側の対応だけでなく、元のドキュメントにおける用語の定義と表記揺れの統一も不可欠です。テクニカルライティングの基本である「一貫した用語の使用」を徹底することで、ドキュメントの保守性が高まり、結果として検索精度も向上します。
# 簡易的な日本語トークナイザーの構成例(実運用ではMeCabやSudachiを推奨)
def japanese_tokenizer(text):
import MeCab
# 分かち書きモードで初期化
tagger = MeCab.Tagger("-Owakati")
return tagger.parse(text).strip().split()
# LangChainのBM25Retrieverを使用する際は、preprocess_func引数などで
# このようなトークナイズ関数を適用することで精度が向上します
検索速度と精度のトレードオフ
ハイブリッド検索は両方の検索を実行するため、数百万件規模のデータを扱う場合はレスポンス遅延が課題となります。
対策として、すべてのクエリでハイブリッド検索を行うのではなく、「クエリの性質」に応じて検索手法を切り替える(ルーティングする)ロジックが有効です。
- パターンマッチング: 正規表現で型番(例:
[A-Z]{4}-\d{4})を検出した場合はBM25のみを実行する。 - クエリの長さ: 非常に短いクエリの場合はベクトル検索を省略する。
また、WeaviateやPineconeなどの最新ベクトルデータベースはハイブリッド検索をネイティブサポートしています。データ量が膨大な場合はデータベース側の機能を利用することでパフォーマンス改善が期待できます。
よくあるエラーとデバッグ方法
「エラーは出ないが期待した結果が返らない」場合、EnsembleRetriever全体ではなく、BM25RetrieverとVectorStoreRetrieverを個別に実行して問題を切り分けます。
BM25でヒットしない場合:
トークナイズの設定を見直します。クエリやインデックス作成時の単語分割が不適切な可能性があり、辞書登録やドキュメントの表記統一で改善するケースが大半です。ベクトル検索でヒットしない場合:
チャンクサイズが不適切か、質問と回答の意味的距離が遠い可能性があります。「仮説的な質問生成(HyDE)」を導入し、クエリを「回答が含まれていそうな文章」に変換することでヒット率が向上することがあります。
開発時はLangSmithなどの可観測性ツールを活用し、各リトリーバーの取得ドキュメントをトレースできるようにするとチューニング効率が上がります。
6. まとめ:高精度なRAG構築への次のステップ
本記事で解説したハイブリッド検索の重要ポイントを振り返ります。
- ベクトル検索の弱点: 製品型番など「完全一致」が求められるクエリに弱い傾向があります。
- ハイブリッド検索の強み: 両者の順位を統合(RRFなど)し、互いの弱点を補完して再現率を高めます。
- 実装のポイント: LangChainの
EnsembleRetrieverで効率的に統合でき、ドメインに合わせた重み付け調整が鍵となります。
ハイブリッド検索の導入はRAGシステムの信頼性を高める大きな一歩ですが、さらなる高みを目指すためのステップも紹介します。
さらに精度を高めるためのリランク(Re-ranking)技術
取得した上位ドキュメントに対し、Cross-Encoderなどのモデルを用いて再評価と並び替えを行うアプローチです。計算コストは高くなりますが、LLMに渡すコンテキストの質が劇的に向上するため、業務システムでは標準的な構成になりつつあります。
自律的な検索エージェントへの進化
最近は、AIが自律的に判断して行動する「Agentic RAG(エージェント型RAG)」への移行が進んでいます。
- クエリの分解: 複雑な質問を複数のサブクエリに分解して検索する。
- ツールの使い分け: データベース検索、Web検索、API呼び出しを動的に選択する。
- 自己修正: 検索結果が不十分な場合、検索ワードを変えて再検索を行う。
LangChainやLangGraphを活用し、より柔軟な「考える検索システム」へと発展させることが可能です。
継続的な精度改善のサイクル
検索システムは構築後も以下のサイクルを回し続けることが不可欠です。
- ログ分析: ユーザーの質問と提示されたドキュメントの適切さを分析する。
- フィードバックループ: ユーザーからの評価(Good/Bad)を収集し、精度の指標とする。
- ドキュメントとモデルの保守: ドメイン知識の変化に合わせ、用語辞書の更新、埋め込みモデルの再選定、そして元ドキュメントの継続的なリライトを行う。
また、バックエンドのLLMの進化に合わせてプロンプトやコンテキストの渡し方も見直す必要があります。継続的な改善を通じて、ユーザーの課題解決に直結する信頼性の高い検索システムを構築していきましょう。
コメント