T-CREATOR

LangChain で RAG 構築:Retriever・VectorStore の設計ベストプラクティス

LangChain で RAG 構築:Retriever・VectorStore の設計ベストプラクティス

近年、大規模言語モデル(LLM)の活用が急速に進む中で、Retrieval-Augmented Generation(RAG)システムが注目を集めています。RAG は、外部の知識ベースから関連情報を取得し、それを基に回答を生成する技術で、LLM の知識不足や幻覚問題を解決する有効な手段です。

この記事では、LangChain を使用した RAG システムの構築において、特に Retriever と VectorStore の設計に焦点を当てたベストプラクティスをご紹介します。実際のコード例とともに、効率的で実用的な RAG システムを構築するための具体的な手法を学んでいただけます。

背景

RAG が必要になる理由

大規模言語モデルは非常に優秀ですが、以下のような課題があります。

mermaidflowchart TD
    llm[大規模言語モデル] -->|課題1| knowledge[知識の限界]
    llm -->|課題2| hallucination[幻覚・不正確な情報]
    llm -->|課題3| update[情報の更新困難]

    knowledge --> solution[RAGシステム]
    hallucination --> solution
    update --> solution

    solution --> retrieval[外部知識の検索]
    solution --> generation[正確な回答生成]

RAG システムが解決する主な問題は次の通りです:

  • 知識の範囲制限: 学習データの範囲内でしか回答できない
  • 情報の古さ: 学習データのカットオフ時点以降の情報が反映されない
  • 幻覚問題: 存在しない情報を事実として回答してしまう可能性

これらの課題により、企業での実用的な AI システム構築では、最新かつ正確な情報を参照できる RAG アプローチが不可欠となっています。

従来の検索システムとの違い

従来のキーワードベース検索と RAG の違いを以下の表で整理します:

項目従来の検索システムRAG システム
検索方式キーワードマッチングセマンティック検索
結果の形式文書リスト自然言語での回答
意図理解限定的高度な文脈理解
カスタマイズ性低い高い
実装複雑度低い中〜高い

RAG システムでは、ベクトル化された文書から意味的に関連する情報を検索し、それを基に自然な回答を生成できます。これにより、ユーザーの質問意図をより深く理解した回答が可能になります。

LangChain を選ぶメリット

LangChain が RAG 構築で選ばれる理由は以下の通りです:

  1. 豊富な VectorStore 対応: Chroma、FAISS、Pinecone 等、主要なベクトルデータベースをサポート
  2. 柔軟な Retriever 設計: 様々な検索戦略を簡単に実装・切り替え可能
  3. エコシステムの充実: LLM、Embedding、Chain 等の統合が容易
  4. 活発なコミュニティ: 継続的なアップデートと豊富な情報

これらの特徴により、プロトタイプから本番運用まで一貫して LangChain を使用できるメリットがあります。

課題

VectorStore 選択の複雑さ

RAG システム構築において、適切な VectorStore の選択は重要な課題です。選択を困難にする主な要因を図で示します:

mermaidflowchart LR
    choice[VectorStore選択] -->|考慮要素1| performance[パフォーマンス要件]
    choice -->|考慮要素2| scale[スケール要件]
    choice -->|考慮要素3| cost[コスト制約]
    choice -->|考慮要素4| maintenance[運用・保守性]

    performance --> local[ローカル型<br/>FAISS, Chroma]
    performance --> cloud[クラウド型<br/>Pinecone, Weaviate]

    scale --> distributed[分散対応]
    scale --> single[単体運用]

各 VectorStore には以下のような特性があります:

  • FAISS: 高速だが単体サーバー向け、運用が複雑
  • Chroma: 開発が容易だが大規模運用に制限
  • Pinecone: 高機能だが月額コストが発生
  • Weaviate: 多機能だが学習コストが高い

この多様性により、要件に最適な選択を行うのが困難になっています。

Retriever 設計の難しさ

効果的な Retriever を設計する際に直面する主な課題は以下です:

  1. 検索戦略の選択: Dense、Sparse、Hybrid のどれを選ぶか
  2. チャンクサイズの最適化: 文書分割の粒度設定
  3. 類似度計算方式: コサイン類似度、ユークリッド距離等の選択
  4. フィルタリング条件: メタデータを使った条件絞り込み

これらの要素が相互に影響するため、最適な組み合わせを見つけるのは試行錯誤が必要です。

検索精度とパフォーマンスのバランス

RAG システムでは検索精度の向上とレスポンス速度の確保を両立する必要があります:

精度向上手法パフォーマンスへの影響実装コスト
大きな Embedding モデルレスポンス時間増加低い
複数 Retriever の組み合わせ計算量増加中程度
Re-ranking 機能大幅な時間増加高い
インデックス最適化メモリ使用量増加中程度

この調整は、システムの用途や要求仕様によって最適解が変わるため、慎重な設計が求められます。

解決策

LangChain における VectorStore の種類と特徴

LangChain で利用可能な主要な VectorStore の特徴を整理します:

mermaidclassDiagram
    class VectorStore {
        +add_documents()
        +similarity_search()
        +as_retriever()
    }

    class FAISS {
        +高速検索
        +ローカル運用
        +大規模対応
    }

    class Chroma {
        +開発容易
        +永続化対応
        +メタデータ豊富
    }

    class Pinecone {
        +クラウド型
        +スケーラブル
        +高機能
    }

    VectorStore <|-- FAISS
    VectorStore <|-- Chroma
    VectorStore <|-- Pinecone

各 VectorStore の具体的な選択指針:

開発・プロトタイプ段階

推奨: Chroma

  • 簡単なセットアップ
  • 永続化機能付き
  • メタデータフィルタリング対応

中規模本番運用

推奨: FAISS + 独自永続化

  • 高速な検索性能
  • メモリ効率が良い
  • カスタマイズ性が高い

大規模・エンタープライズ

推奨: Pinecone または Weaviate

  • 分散処理対応
  • 運用負荷が低い
  • 高度な機能セット

Retriever の設計指針

効果的な Retriever を設計するための基本方針をご紹介します:

1. チャンクサイズの最適化

文書分割の粒度は検索精度に大きく影響します:

javascript// 推奨されるチャンクサイズ設定例
const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000, // 基本は1000文字
  chunkOverlap: 200, // 20%のオーバーラップ
  separators: ['\n\n', '\n', '。', '.', ' ', ''],
});

上記設定の根拠:

  • 1000 文字: ほとんどの Embedding モデルの最適範囲
  • 200 文字オーバーラップ: 文脈の連続性を保持
  • 階層的区切り: 自然な文章境界で分割

2. 検索戦略の選択

用途に応じた検索戦略の選び方:

検索タイプ適用場面特徴
Dense Retrieval意味的類似性重視セマンティック検索に優秀
Sparse Retrievalキーワード重視専門用語・固有名詞に強い
Hybrid Retrievalバランス重視両方の長所を活用

3. Multi-Query Retriever の活用

検索精度向上のための高度な手法:

typescript// Multi-Query Retriever の基本実装
import { MultiQueryRetriever } from 'langchain/retrievers/multi_query';

const retriever = MultiQueryRetriever.fromLLM({
  llm: chatModel,
  retriever: vectorStore.asRetriever(),
  queryCount: 3, // 3つの異なる質問を生成
});

この手法により、元の質問から複数の検索クエリを生成し、より幅広い関連文書を取得できます。

最適化のための設定パラメータ

パフォーマンスと精度を調整するための重要パラメータ:

VectorStore 共通パラメータ

typescriptconst retrieverConfig = {
  k: 5, // 取得文書数(基本は3-5)
  scoreThreshold: 0.7, // 類似度閾値(0.6-0.8推奨)
  fetchK: 20, // 事前フィルタリング数
  lambda: 0.5, // Diversity調整(MMR使用時)
};

検索品質向上パラメータ

javascript// Re-ranking用の設定
const rerankConfig = {
  model: 'cross-encoder/ms-marco-MiniLM-L-6-v2',
  topK: 10, // Re-rank対象数
  returnTopK: 3, // 最終返却数
};

これらのパラメータを調整することで、用途に応じた最適な検索結果を得ることができます。

具体例

Chroma + OpenAI Embeddings での基本構築

最も一般的な RAG システムの構築例から始めます。

環境準備

まず必要なパッケージをインストールします:

bashyarn add langchain @langchain/openai @langchain/chroma
yarn add -D @types/node

基本的な VectorStore の構築

typescriptimport { Chroma } from '@langchain/chroma';
import { OpenAIEmbeddings } from '@langchain/openai';
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';

環境変数と Embedding の初期化:

typescript// 環境設定
const embeddings = new OpenAIEmbeddings({
  openAIApiKey: process.env.OPENAI_API_KEY,
  modelName: 'text-embedding-3-small', // コスト効率重視
});

// テキスト分割器の設定
const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 200,
});

VectorStore の作成と文書の追加:

typescript// Chromaインスタンスの作成
const vectorStore = new Chroma(embeddings, {
  collectionName: 'rag-collection',
  url: process.env.CHROMA_URL || 'http://localhost:8000',
});

// 文書データの準備と追加
async function addDocuments(texts: string[]) {
  // テキストを適切なサイズに分割
  const docs = await textSplitter.createDocuments(texts);

  // メタデータの追加
  const docsWithMetadata = docs.map((doc, index) => ({
    ...doc,
    metadata: {
      ...doc.metadata,
      source: `document_${index}`,
      timestamp: new Date().toISOString(),
    },
  }));

  // VectorStoreに追加
  await vectorStore.addDocuments(docsWithMetadata);
  console.log(
    `${docsWithMetadata.length} documents added successfully`
  );
}

基本的な検索と Retriever 作成

typescript// Retrieverの作成
const retriever = vectorStore.asRetriever({
  k: 5, // 上位5件を取得
  searchType: 'similarity_score_threshold',
  searchKwargs: {
    scoreThreshold: 0.7, // 類似度0.7以上
  },
});

// 検索実行の例
async function searchExample(query: string) {
  const relevantDocs = await retriever.getRelevantDocuments(
    query
  );

  console.log(
    `Found ${relevantDocs.length} relevant documents:`
  );
  relevantDocs.forEach((doc, index) => {
    console.log(`\nDocument ${index + 1}:`);
    console.log(
      `Content: ${doc.pageContent.slice(0, 200)}...`
    );
    console.log(`Source: ${doc.metadata.source}`);
    console.log(`Score: ${doc.metadata.score}`);
  });

  return relevantDocs;
}

この基本構成により、シンプルで効果的な RAG システムを構築できます。

FAISS を使った高速検索システム

大量文書での高速検索が必要な場合の FAISS 実装例です。

FAISS の初期設定

typescriptimport { FaissStore } from '@langchain/community/vectorstores/faiss';
import { OpenAIEmbeddings } from '@langchain/openai';

高性能設定での FAISS ストア作成:

typescript// 高性能Embedding設定
const embeddings = new OpenAIEmbeddings({
  modelName: 'text-embedding-3-large', // 高精度モデル
  dimensions: 1536, // 次元数明示
});

// FAISSインデックス作成
async function createFaissIndex(documents: Document[]) {
  // バッチ処理での効率的なインデックス作成
  const vectorStore = await FaissStore.fromDocuments(
    documents,
    embeddings
  );

  // インデックスの永続化
  await vectorStore.save('./faiss_index');
  console.log('FAISS index saved successfully');

  return vectorStore;
}

高速検索のための最適化

typescript// 保存されたインデックスの読み込み
async function loadFaissIndex() {
  const vectorStore = await FaissStore.load(
    './faiss_index',
    embeddings
  );

  return vectorStore;
}

// 高速Retrieverの設定
async function createOptimizedRetriever() {
  const vectorStore = await loadFaissIndex();

  const retriever = vectorStore.asRetriever({
    k: 10, // より多くの候補を取得
    searchType: 'mmr', // Maximum Marginal Relevance使用
    searchKwargs: {
      fetchK: 50, // 事前に50件取得
      lambda: 0.7, // 多様性重視
    },
  });

  return retriever;
}

バッチ処理での効率的な文書追加

typescript// 大量文書の効率的な処理
async function addDocumentsBatch(
  vectorStore: FaissStore,
  documents: Document[],
  batchSize: number = 100
) {
  console.log(
    `Processing ${documents.length} documents in batches of ${batchSize}`
  );

  for (let i = 0; i < documents.length; i += batchSize) {
    const batch = documents.slice(i, i + batchSize);
    await vectorStore.addDocuments(batch);

    console.log(
      `Processed batch ${
        Math.floor(i / batchSize) + 1
      }/${Math.ceil(documents.length / batchSize)}`
    );

    // メモリ管理のための小休止
    if (i % (batchSize * 10) === 0) {
      await new Promise((resolve) =>
        setTimeout(resolve, 1000)
      );
    }
  }

  // 更新されたインデックスを保存
  await vectorStore.save('./faiss_index');
}

この実装により、100 万文書規模でも高速な検索が可能になります。

Hybrid Retriever の実装

セマンティック検索とキーワード検索を組み合わせた高度な検索システムです。

Hybrid Retriever のアーキテクチャ

mermaidsequenceDiagram
    participant User
    participant HybridRetriever
    participant DenseRetriever
    participant SparseRetriever
    participant Ranker

    User->>HybridRetriever: クエリ送信
    HybridRetriever->>DenseRetriever: セマンティック検索
    HybridRetriever->>SparseRetriever: キーワード検索
    DenseRetriever-->>HybridRetriever: 検索結果A
    SparseRetriever-->>HybridRetriever: 検索結果B
    HybridRetriever->>Ranker: 結果の統合・ランキング
    Ranker-->>HybridRetriever: 統合結果
    HybridRetriever-->>User: 最終結果

Dense Retriever の実装

typescript// セマンティック検索用のRetriever
class DenseRetriever {
  private vectorStore: VectorStore;

  constructor(vectorStore: VectorStore) {
    this.vectorStore = vectorStore;
  }

  async retrieve(
    query: string,
    k: number = 10
  ): Promise<Document[]> {
    const retriever = this.vectorStore.asRetriever({
      k,
      searchType: 'similarity',
    });

    return await retriever.getRelevantDocuments(query);
  }
}

Sparse Retriever の実装

typescriptimport { BM25Retriever } from '@langchain/community/retrievers/bm25';

// キーワード検索用のRetriever
class SparseRetriever {
  private bm25Retriever: BM25Retriever;

  constructor(documents: Document[]) {
    this.bm25Retriever =
      BM25Retriever.fromDocuments(documents);
  }

  async retrieve(
    query: string,
    k: number = 10
  ): Promise<Document[]> {
    this.bm25Retriever.k = k;
    return await this.bm25Retriever.getRelevantDocuments(
      query
    );
  }
}

Hybrid Retriever のメイン実装

typescript// Hybrid Retrieverの統合クラス
class HybridRetriever {
  private denseRetriever: DenseRetriever;
  private sparseRetriever: SparseRetriever;
  private alpha: number; // Dense/Sparse重み調整

  constructor(
    denseRetriever: DenseRetriever,
    sparseRetriever: SparseRetriever,
    alpha: number = 0.7 // Denseを重視
  ) {
    this.denseRetriever = denseRetriever;
    this.sparseRetriever = sparseRetriever;
    this.alpha = alpha;
  }

  async retrieve(
    query: string,
    k: number = 5
  ): Promise<Document[]> {
    // 並列で両方の検索を実行
    const [denseResults, sparseResults] = await Promise.all(
      [
        this.denseRetriever.retrieve(query, k * 2),
        this.sparseRetriever.retrieve(query, k * 2),
      ]
    );

    // スコアの正規化と統合
    const combinedResults = this.combineResults(
      denseResults,
      sparseResults
    );

    // 上位k件を返却
    return combinedResults.slice(0, k);
  }

  private combineResults(
    denseResults: Document[],
    sparseResults: Document[]
  ): Document[] {
    const scoreMap = new Map<string, number>();
    const docMap = new Map<string, Document>();

    // Dense結果のスコア処理
    denseResults.forEach((doc, index) => {
      const key = this.getDocKey(doc);
      const normalizedScore =
        (denseResults.length - index) / denseResults.length;
      scoreMap.set(key, this.alpha * normalizedScore);
      docMap.set(key, doc);
    });

    // Sparse結果のスコア処理と統合
    sparseResults.forEach((doc, index) => {
      const key = this.getDocKey(doc);
      const normalizedScore =
        (sparseResults.length - index) /
        sparseResults.length;
      const existingScore = scoreMap.get(key) || 0;
      scoreMap.set(
        key,
        existingScore + (1 - this.alpha) * normalizedScore
      );
      docMap.set(key, doc);
    });

    // スコア順でソート
    const sortedEntries = Array.from(
      scoreMap.entries()
    ).sort(([, scoreA], [, scoreB]) => scoreB - scoreA);

    return sortedEntries.map(([key]) => docMap.get(key)!);
  }

  private getDocKey(doc: Document): string {
    // 文書の一意キーを生成(内容のハッシュまたはメタデータのID)
    return doc.metadata.id || doc.pageContent.slice(0, 100);
  }
}

使用例

typescript// Hybrid Retrieverの使用例
async function setupHybridRAG() {
  // VectorStore(Dense用)
  const vectorStore = new Chroma(embeddings, {
    collectionName: 'hybrid-collection',
  });

  // 文書データの準備
  const documents = await loadDocuments();
  await vectorStore.addDocuments(documents);

  // Retrieverインスタンス作成
  const denseRetriever = new DenseRetriever(vectorStore);
  const sparseRetriever = new SparseRetriever(documents);

  // Hybrid Retriever作成
  const hybridRetriever = new HybridRetriever(
    denseRetriever,
    sparseRetriever,
    0.6 // バランス重視の設定
  );

  return hybridRetriever;
}

// 実際の検索実行
async function hybridSearch(query: string) {
  const retriever = await setupHybridRAG();
  const results = await retriever.retrieve(query, 5);

  console.log(`Hybrid search results for: "${query}"`);
  results.forEach((doc, index) => {
    console.log(
      `${index + 1}. ${doc.pageContent.slice(0, 150)}...`
    );
  });

  return results;
}

この実装により、キーワード検索の精密性とセマンティック検索の柔軟性を両立した高精度な検索システムを構築できます。

まとめ

設計時の重要ポイント

効果的な RAG システムを構築するための重要な設計指針をまとめます:

1. VectorStore 選択の判断基準

選択要因推奨 VectorStore理由
プロトタイプ・小規模Chroma簡単セットアップ、永続化対応
中規模・性能重視FAISS高速検索、メモリ効率
大規模・エンタープライズPinecone/Weaviateスケーラビリティ、運用性

2. チャンクサイズの最適化方針

  • 基本設定: 1000 文字、200 文字オーバーラップ
  • 技術文書: 1500 文字(コード例含む場合)
  • FAQ・短文: 500 文字(簡潔な回答用)
  • 学術論文: 2000 文字(詳細な文脈が必要)

3. 検索戦略の選択指針

用途に応じた適切な検索方式の選択:

mermaidflowchart TD
    start[検索要件分析] --> question{主な検索対象は?}

    question -->|概念・意味| semantic[セマンティック検索]
    question -->|キーワード・固有名詞| keyword[キーワード検索]
    question -->|両方重要| hybrid[Hybrid検索]

    semantic --> dense[Dense Retriever]
    keyword --> sparse[Sparse Retriever]
    hybrid --> combined[Hybrid Retriever]

    dense --> optimize1[Embedding最適化]
    sparse --> optimize2[BM25パラメータ調整]
    combined --> optimize3[重み配分調整]

4. パフォーマンス最適化のポイント

  • インデックス作成: バッチ処理で効率化
  • 検索実行: キャッシュ機構の活用
  • メモリ管理: 適切なクリーンアップ処理
  • 並列処理: 複数 Retriever の同時実行

運用時の注意点

1. データ更新戦略

RAG システムの運用では、継続的なデータ更新が重要です:

  • 増分更新: 新規文書のみをインデックスに追加
  • 定期的な再構築: インデックス全体の品質維持
  • バージョン管理: インデックスのバックアップと復旧体制

2. 検索品質の監視

システムの品質維持のための監視項目:

監視指標目標値改善アクション
検索精度80%以上チャンクサイズ調整、Retriever 改善
レスポンス時間1 秒以内インデックス最適化、キャッシュ活用
関連度スコア0.7 以上閾値調整、フィルタリング改善

3. エラーハンドリング

堅牢な運用のための例外処理:

typescript// エラーハンドリングの例
async function safeRetrieve(
  query: string,
  maxRetries: number = 3
) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const results = await retriever.getRelevantDocuments(
        query
      );
      return results;
    } catch (error) {
      console.error(`Attempt ${attempt} failed:`, error);

      if (attempt === maxRetries) {
        // フォールバック処理
        return await fallbackSearch(query);
      }

      // 指数バックオフで再試行
      await new Promise((resolve) =>
        setTimeout(resolve, Math.pow(2, attempt) * 1000)
      );
    }
  }
}

4. セキュリティ考慮事項

エンタープライズ環境での重要な配慮:

  • アクセス制御: ユーザー権限に基づく文書フィルタリング
  • データ暗号化: 保存時・転送時の暗号化
  • 監査ログ: 検索履歴と結果の記録
  • PII 保護: 個人情報の適切な取り扱い

これらのポイントを押さえることで、実用的で安定した RAG システムを構築・運用できます。適切な設計と継続的な改善により、ユーザーにとって価値のある AI アプリケーションを提供していきましょう。

関連リンク