【LangChain】ベクトルストア検索で関連性の低い結果が返る問題の解決法

導入

LangChainを用いてRAG(Retrieval-Augmented Generation)アプリケーションを構築する際、ベクトルストアからの検索結果が期待したほど関連性の高くないドキュメントばかりを返し、最終的な回答の質が低下する問題は、開発者が頻繁に直面する課題です。ユーザーのクエリに対して、最も関連性の高い情報を正確に取得することは、RAGシステムの性能を決定づける核心的な要素です。本記事では、この「関連性の低い結果が返る」という問題に焦点を当て、その根本原因を探り、実践的な解決策をコード例と共に詳細に解説します。

原因説明

ベクトル検索で関連性の低い結果が返される主な原因は、以下の3点に集約されます。

  • 埋め込みモデルの不適切さ: 使用している埋め込みモデルが、特定のドメイン(例: 法律文書、医療論文)や言語に対して最適化されていない場合、文書とクエリの意味的な関係性を適切にベクトル空間に反映できません。
  • チャンキング戦略の非最適化: 文書を意味のまとまりなく単純に固定長で分割すると、重要な文脈が分断され、検索時に不完全な情報しか提供されなくなります。
  • 検索方法とパラメータの設定ミス: 類似度計算方法(コサイン類似度等)や、取得する結果数(k)、スコアのしきい値など、検索プロセス自体の設定がユースケースに合っていない可能性があります。

これらの原因は複合的に関わっていることが多く、包括的なアプローチでの改善が必要です。

解決方法

関連性の高い検索結果を得るためには、データの前処理から検索戦略まで、パイプライン全体を最適化する必要があります。以下に、実践的な解決ステップを説明します。

1. 適切な埋め込みモデルの選択とファインチューニング

汎用モデル(例: `all-MiniLM-L6-v2`)ではドメイン特有のニュアンスを捉えきれない場合があります。より性能の高いモデル(例: `text-embedding-3-small`)への切り替え、または独自データでのファインチューニングを検討します。

from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Chroma

# より高性能な埋め込みモデルを試す例
embeddings = HuggingFaceEmbeddings(
    model_name="intfloat/multilingual-e5-large", # 多言語・高精度モデル
    model_kwargs={'device': 'cpu'},
    encode_kwargs={'normalize_embeddings': True} # 類似度計算を最適化
)

# ベクトルストアの作成(既存のデータを再インデックスする必要あり)
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"
)

2. 文脈を考慮した高度なチャンキング

固定長分割ではなく、セマンティックな境界(例: 段落、見出し)で分割するRecursiveCharacterTextSplitterの使用、またはオーバーラップを設けて文脈を保持する方法が有効です。

from langchain.text_splitter import RecursiveCharacterTextSplitter, MarkdownHeaderTextSplitter

# 方法1: 再帰的分割とオーバーラップ
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,          # 目標チャンクサイズ
    chunk_overlap=100,       # 重複する文字数で文脈を保持
    separators=["\n\n", "\n", "。", "、", " ", ""], # 分割優先順位
    length_function=len,
)
chunks = text_splitter.split_documents(documents)

# 方法2: マークダウンの見出し構造を考慮した分割(構造化文書向け)
headers_to_split_on = [("#", "Header 1"), ("##", "Header 2")]
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
md_chunks = markdown_splitter.split_text(markdown_text)

3. 検索戦略の高度化

単純な類似度検索(similarity_search)だけでなく、関連性スコアによるフィルタリングや、複数検索方法の組み合わせを導入します。

from langchain.vectorstores import Chroma
from langchain.schema import Document
import numpy as np

# 既存のベクトルストアを読み込み
vectorstore = Chroma(
    persist_directory="./chroma_db",
    embedding_function=embeddings
)

# 解決策 3-1: スコア付き検索としきい値フィルタリング
docs_with_score = vectorstore.similarity_search_with_relevance_scores(
    query="あなたのクエリ",
    k=10 # まず多めに取得
)
# 関連性スコア(例: コサイン類似度)でフィルタリング
filtered_docs = [doc for doc, score in docs_with_score if score > 0.7] # しきい値は要調整

# 解決策 3-2: MMR (Maximal Marginal Relevance) を用いた多様性確保
# 類似性は保ちつつ、重複の少ない多様な結果を返す
mmr_docs = vectorstore.max_marginal_relevance_search(
    query="あなたのクエリ",
    k=4,
    fetch_k=10, # MMRアルゴリズムが選択肢を選ぶためのプールサイズ
    lambda_mult=0.7 # 多様性と関連性のバランス (1:関連性優先, 0:多様性優先)
)

# 解決策 3-3: ハイブリッド検索(ベクトル検索 + キーワード検索)のシミュレーション
# LangChainのFAISSでは可能ですが、Chromaで実装する例:
def hybrid_search(query, vectorstore, text_weight=0.5, vector_weight=0.5):
    # 1. ベクトル類似度検索
    vector_results = vectorstore.similarity_search_with_score(query, k=10)
    # 2. 簡易キーワードマッチング(本番ではBM25等の利用を推奨)
    all_docs = vectorstore.get()['documents']
    keyword_matches = []
    for i, doc in enumerate(all_docs):
        if any(keyword in doc for keyword in query.split()):
            # 簡易スコア(マッチしたキーワード数)
            score = sum([1 for kw in query.split() if kw in doc])
            keyword_matches.append((Document(page_content=doc), score))
    # 3. スコアの正規化と融合(簡易例)
    # ... スコア融合ロジックをここに実装 ...
    return combined_results

4. クエリの拡張と書き換え

ユーザーの短いクエリを、より情報豊富で検索に適した形に変換します。LLMを活用したクエリ拡張が有効です。

from langchain.llms import OpenAI
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate

llm = OpenAI(temperature=0)

# クエリを拡張するプロンプトテンプレート
expansion_prompt = PromptTemplate(
    input_variables=["original_query"],
    template="""以下のユーザークエリを、関連する文書をベクトルデータベースから検索するために最適化された3つの異なる検索クエリに拡張してください。
    各クエリは簡潔で、重要なキーワードを含んでいてください。
    元のクエリ: {original_query}
    生成された検索クエリ(番号付きリスト):
    """
)

expansion_chain = LLMChain(llm=llm, prompt=expansion_prompt)
original_query = "LangChainのエラー"
expanded_queries_text = expansion_chain.run(original_query)

# 生成されたテキストからクエリをパース(簡易例)
generated_queries = [q.strip()[3:] for q in expanded_queries_text.split('\n') if q.strip() and q.strip()[0].isdigit()]

# 拡張された各クエリで検索し、結果を統合
all_retrieved_docs = []
for eq in generated_queries[:3]: # 最初の3つを使用
    docs = vectorstore.similarity_search(eq, k=3)
    all_retrieved_docs.extend(docs)
# 重複を除去(page_contentに基づく簡易除去)
unique_docs = []
seen_content = set()
for doc in all_retrieved_docs:
    if doc.page_content not in seen_content:
        seen_content.add(doc.page_content)
        unique_docs.append(doc)

これらの手法を単独または組み合わせて使用することで、ベクトルストアからの検索結果の関連性を大幅に向上させることができます。自らのデータとユースケースに合わせて、チャンクサイズ、オーバーラップ、スコアしきい値などのパラメータを継続的に調整・評価することが成功の鍵です。

まとめ

LangChainを用いたアプリケーションにおいて、ベクトル検索の結果の関連性を高めるには、単一の「銀の弾丸」は存在せず、データ準備(チャンキング)、意味表現(埋め込みモデル)、検索アルゴリズム、そしてクエリそのものに対する総合的な最適化が求められます。本記事で紹介した、高性能な埋め込みモデルの採用、文脈を保持するチャンキング、MMRやスコアフィルタリングを活用した検索戦略の高度化、そしてLLMによるクエリ拡張といった手法を、段階的に実装・評価することで、RAGシステムの核心である「検索」の精度を確実に向上させることができます。これらの改善は、最終的に生成される回答の質と信頼性の飛躍的な向上へと直結します。

💡 この問題を根本的に解決するには

ローカル環境でGPUトラブルが頻発する場合、クラウドGPUサービスの利用も検討してみてください。環境構築の手間なく、すぐにAI開発を始められます。

  • RunPod — RTX 4090が$0.44/h〜、ワンクリックでJupyter環境が起動
  • Vast.ai — コミュニティGPUマーケットプレイス、最安値でGPUレンタル
この記事は役に立ちましたか?