T-CREATOR

Dify RAG でヒットしないとき:埋め込み品質・分割・検索パラメータの診断術

Dify RAG でヒットしないとき:埋め込み品質・分割・検索パラメータの診断術

Dify で RAG(Retrieval-Augmented Generation)を構築したものの、「期待した文書がヒットしない」「検索精度が低い」という経験はありませんか。せっかくナレッジベースを整備しても、適切な情報が取得できなければ AI の回答品質は大きく低下してしまいます。

RAG の検索精度を左右する要因は多岐にわたりますが、特に重要なのが 埋め込み品質文書分割(チャンク分割)検索パラメータ の 3 つです。本記事では、これらの要素を体系的に診断し、ヒット率を改善する実践的な手法を解説いたします。初めて RAG のチューニングに取り組む方でも、段階的に問題を特定できるよう、図解と具体例を交えてご紹介しますね。

背景

RAG の基本的な仕組み

RAG は、大規模言語モデル(LLM)に外部知識を組み合わせることで、より正確で最新の情報に基づいた回答を生成する技術です。Dify では、ユーザーがアップロードした文書をナレッジベースとして管理し、質問に応じて関連する文書を検索して LLM に渡します。

以下の図は、Dify における RAG の基本的なデータフローを示しています。

mermaidflowchart LR
  user["ユーザー"] -->|質問| dify["Dify アプリ"]
  dify -->|埋め込みベクトル化| embed["Embedding モデル"]
  embed -->|検索クエリベクトル| vdb[("ベクトルDB")]
  vdb -->|類似文書取得| dify
  dify -->|プロンプト生成| llm["LLM"]
  llm -->|回答| user

図で理解できる要点:

  • ユーザーの質問は埋め込みベクトルに変換される
  • ベクトル DB から類似度の高い文書が取得される
  • 取得した文書を元に LLM が回答を生成する

埋め込みベクトルと類似度検索

埋め込み(Embedding)とは、テキストを数百〜数千次元のベクトル(数値の配列)に変換する技術です。意味的に近いテキストは、ベクトル空間上でも近い位置に配置されます。

Dify では、文書を事前に埋め込みベクトルに変換してベクトル DB に保存し、質問が来ると同様にベクトル化して類似度検索を行います。この仕組みにより、キーワード検索では見つけられない意味的に関連する文書も取得できるのです。

しかし、この仕組みが適切に機能するには、いくつかの条件が揃っている必要があります。

課題

RAG でヒットしない主な原因は、以下の 3 つの領域に分類できます。

1. 埋め込み品質の問題

埋め込みモデルが質問と文書の意味的関連性を正しく捉えられていない場合、類似度スコアが低くなり、関連文書が取得できません。

主な原因:

  • 埋め込みモデルの言語や分野が合っていない
  • 質問と文書の表現が大きく異なる
  • モデルの次元数や性能が不足している

2. 文書分割(チャンク分割)の問題

文書を適切なサイズに分割できていないと、必要な情報が複数のチャンクに分散したり、逆に大きすぎて検索精度が低下したりします。

主な原因:

  • チャンクサイズが大きすぎる(情報密度が低い)
  • チャンクサイズが小さすぎる(文脈が失われる)
  • オーバーラップが不適切(重複が少なすぎる)

3. 検索パラメータの問題

適切な文書が埋め込まれていても、検索時のパラメータ設定が不適切だと、必要な文書が取得されません。

主な原因:

  • Top K の値が小さすぎる(候補が少ない)
  • スコア閾値が高すぎる(フィルタリングされすぎ)
  • リランキングの設定が不適切

以下の図は、これらの課題がどのように検索失敗につながるかを示しています。

mermaidflowchart TD
  query["質問文"] --> embed_issue{"埋め込み品質<br/>は適切か?"}
  embed_issue -->|No| fail1["類似度スコア低下<br/>→ヒットしない"]
  embed_issue -->|Yes| chunk_issue{"チャンク分割<br/>は適切か?"}
  chunk_issue -->|No| fail2["情報の分散・欠落<br/>→ヒットしない"]
  chunk_issue -->|Yes| param_issue{"検索パラメータ<br/>は適切か?"}
  param_issue -->|No| fail3["フィルタリング過多<br/>→ヒットしない"]
  param_issue -->|Yes| success["適切な文書取得<br/>→回答生成"]

図で理解できる要点:

  • 問題は段階的に発生する
  • 各段階で適切な対処が必要
  • 最終的な成功には 3 つすべての要素が重要

解決策

ここでは、各課題に対する診断方法と具体的な対処法を解説します。

1. 埋め込み品質の診断と改善

診断方法

まず、埋め込みモデルが質問と文書を適切にベクトル化できているかを確認します。Dify のナレッジベース設定で、現在使用している埋め込みモデルを確認しましょう。

診断チェックリスト:

#項目確認内容推奨設定
1モデルの言語対応日本語文書に対応しているかmultilingual または ja モデル
2モデルの次元数ベクトルの次元数は十分か768 次元以上
3モデルの分野適合性文書の専門分野に適しているか汎用モデルまたは分野特化モデル
4類似度スコア実際の検索でスコアはどの程度か0.7 以上が理想

改善方法

埋め込みモデルの変更は、Dify のナレッジベース設定から行えます。以下は推奨されるモデルの選択基準です。

日本語文書の場合:

  • text-embedding-ada-002(OpenAI): 多言語対応で高品質
  • text-multilingual-embedding-002(Google): 多言語に強い
  • intfloat​/​multilingual-e5-large(オープンソース): コスト重視

モデルを変更した後は、必ずナレッジベースの 再埋め込み を実行してください。既存の埋め込みベクトルは自動的には更新されません。

typescript// Dify API を使った埋め込みベクトルの確認例
interface EmbeddingConfig {
  provider: string;
  model: string;
  dimensions: number;
}

// 現在の埋め込み設定を確認
const checkEmbeddingConfig = async (
  datasetId: string
): Promise<EmbeddingConfig> => {
  const response = await fetch(
    `https://api.dify.ai/v1/datasets/${datasetId}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.DIFY_API_KEY}`,
      },
    }
  );

  const data = await response.json();
  return data.embedding_model;
};

上記のコードは、Dify API を使って現在のナレッジベースの埋め込み設定を確認する例です。providermodel の情報を取得することで、どのモデルが使われているかを確認できます。

2. 文書分割(チャンク分割)の診断と改善

診断方法

チャンク分割の適切性を判断するには、実際に分割された文書を確認するのが最も確実です。

診断チェックリスト:

#項目確認内容推奨設定
1チャンクサイズ1 チャンクの文字数は適切か500-1000 文字
2オーバーラップ隣接チャンク間の重複は適切かチャンクサイズの 10-20%
3文脈の保持意味の切れ目で分割されているか段落・セクション単位
4情報の完結性1 チャンク内で意味が完結するか質問に答えられる単位

チャンクサイズの最適化

チャンクサイズは、文書の性質によって調整が必要です。以下は、文書タイプ別の推奨設定です。

文書タイプ別の推奨設定:

#文書タイプチャンクサイズオーバーラップ理由
1技術ドキュメント800-1000 文字100-200 文字詳細な説明が多い
2FAQ・Q&A300-500 文字50-100 文字1 問 1 答で完結
3長文記事・論文1000-1500 文字200-300 文字文脈の連続性が重要
4マニュアル500-800 文字100-150 文字手順の一貫性が必要

Dify では、ナレッジベースの「分割設定」からこれらのパラメータを調整できます。設定変更後は、文書を再度アップロードして分割を反映させましょう。

typescript// チャンク分割設定の例
interface ChunkConfig {
  mode: 'automatic' | 'custom';
  max_tokens: number;
  chunk_overlap: number;
  separator: string;
}

// 技術ドキュメント向けの設定例
const technicalDocConfig: ChunkConfig = {
  mode: 'custom',
  max_tokens: 800, // 約 800-1000 文字相当
  chunk_overlap: 150, // 約 150-200 文字の重複
  separator: '\n\n', // 段落区切り
};

このコードは、Dify の API または設定ファイルでチャンク分割を定義する際の構造例です。max_tokens はトークン数(日本語では約 1 文字 = 1-2 トークン)、chunk_overlap は重複部分のトークン数を指定します。

オーバーラップの重要性

オーバーラップ(重複)を設定すると、文書の境界をまたぐ情報も確実に取得できます。以下の図で、オーバーラップの効果を視覚的に理解しましょう。

mermaidflowchart TD
  doc["元文書: ABCDEFGHIJKLMNOP"]

  subgraph no_overlap["オーバーラップなし"]
    chunk1["チャンク1: ABCDE"]
    chunk2["チャンク2: FGHIJ"]
    chunk3["チャンク3: KLMNO"]
  end

  subgraph with_overlap["オーバーラップあり"]
    chunk4["チャンク1: ABCDE"]
    chunk5["チャンク2: DEFGH"]
    chunk6["チャンク3: GHIJK"]
  end

  doc --> no_overlap
  doc --> with_overlap

  no_overlap --> issue["境界情報DEが<br/>取得できない"]
  with_overlap --> good["境界情報DEも<br/>確実に取得"]

図で理解できる要点:

  • オーバーラップなしでは境界の情報が失われる可能性がある
  • オーバーラップありでは境界情報も確実にカバーできる
  • 適切な重複率でバランスを取ることが重要

3. 検索パラメータの診断と改善

診断方法

検索パラメータは、取得する文書の量と品質を制御します。Dify のアプリ設定で確認できますので、まずは現在の設定を把握しましょう。

診断チェックリスト:

#パラメータ確認内容推奨設定影響
1Top K取得する文書数は適切か3-5 件少ないと取りこぼし
2Score Thresholdスコア閾値は厳しすぎないか0.7 以下高いとフィルタ過多
3Rerankリランキングを有効化しているか有効推奨精度向上
4Rerank Top Nリランキング後の件数は適切か2-3 件LLM への入力を最適化

Top K の最適化

Top K は、ベクトル検索で取得する候補文書の数です。この値が小さすぎると、必要な情報が取得されない可能性があります。

typescript// 検索パラメータの設定例
interface RetrievalConfig {
  topK: number;
  scoreThreshold: number;
  rerankEnabled: boolean;
  rerankTopN: number;
}

// 標準的な設定例
const standardConfig: RetrievalConfig = {
  topK: 5, // 5 件の候補を取得
  scoreThreshold: 0.7, // 類似度 0.7 以上のみ
  rerankEnabled: true, // リランキング有効化
  rerankTopN: 3, // 最終的に 3 件を LLM に渡す
};

このコードは、Dify のアプリ設定における検索パラメータの構造です。topK で候補数を、scoreThreshold で品質基準を制御します。

スコア閾値の調整

スコア閾値(Score Threshold)は、類似度がこの値以上の文書のみを取得するフィルタです。厳しすぎると有用な文書まで除外されてしまいます。

推奨調整ステップ:

  1. 初期設定: スコア閾値を 0.0(無効)に設定して、すべての候補のスコアを確認
  2. スコア分布の把握: 実際の検索で、関連文書のスコアがどの範囲にあるかを確認
  3. 閾値の設定: 関連文書の最低スコアより少し低めに設定(例: 最低スコアが 0.75 なら、閾値は 0.70)
  4. 検証と調整: 実際の検索結果を見ながら微調整
typescript// スコア閾値の段階的調整を支援する関数
const analyzeScoreDistribution = (
  results: Array<{ content: string; score: number }>
) => {
  // スコアの統計情報を計算
  const scores = results.map((r) => r.score);
  const max = Math.max(...scores);
  const min = Math.min(...scores);
  const avg =
    scores.reduce((a, b) => a + b, 0) / scores.length;

  console.log(`スコア分布:`);
  console.log(`  最大: ${max.toFixed(3)}`);
  console.log(`  最小: ${min.toFixed(3)}`);
  console.log(`  平均: ${avg.toFixed(3)}`);

  // 推奨閾値を提案
  const recommendedThreshold = min - 0.05;
  console.log(
    `推奨閾値: ${recommendedThreshold.toFixed(3)}`
  );
};

上記の関数は、検索結果のスコア分布を分析し、適切な閾値を提案します。最小スコアより少し低めの値を推奨することで、取りこぼしを防ぎます。

リランキングの活用

リランキング(Rerank)は、ベクトル検索で取得した候補を、より高度なモデルで再評価する機能です。これにより、検索精度を大幅に向上できます。

typescript// リランキングの動作イメージ
interface RerankResult {
  originalRank: number;
  rerankScore: number;
  content: string;
}

// ベクトル検索 → リランキングのフロー
const searchWithRerank = async (
  query: string,
  config: RetrievalConfig
): Promise<RerankResult[]> => {
  // 1. ベクトル検索で Top K 件を取得
  const vectorResults = await vectorSearch(
    query,
    config.topK
  );

  // 2. リランキングモデルで再評価
  const rerankResults = await rerankModel(
    query,
    vectorResults
  );

  // 3. Top N 件を最終結果として返す
  return rerankResults.slice(0, config.rerankTopN);
};

このコードは、ベクトル検索とリランキングを組み合わせた検索フローを示しています。最初に多めの候補を取得し、高精度なモデルで絞り込むことで、精度とコストのバランスを取ります。

以下の図は、検索パラメータの最適化プロセスを示しています。

mermaidflowchart TD
  start["検索開始"] --> vector["ベクトル検索<br/>Top K=5"]
  vector --> threshold{"スコア閾値<br/>≥0.7?"}
  threshold -->|Yes| pass["候補に残す"]
  threshold -->|No| filter["除外"]
  pass --> rerank["リランキング<br/>精度向上"]
  rerank --> final["Top N=3を<br/>LLMに渡す"]
  final --> generate["回答生成"]

  style filter fill:#ffcccc
  style final fill:#ccffcc

図で理解できる要点:

  • Top K で広く候補を取得
  • スコア閾値で品質をフィルタリング
  • リランキングで精度を向上
  • Top N で LLM への入力を最適化

具体例

ここでは、実際に Dify RAG でヒットしない問題を診断し、解決する具体的な手順を示します。

シナリオ: 技術ドキュメントの検索失敗

状況:

  • 社内の技術ドキュメントをナレッジベースに登録
  • 「Next.js のデプロイ手順」という質問に対して関連文書がヒットしない
  • ドキュメントには確実に該当内容が含まれている

ステップ 1: 埋め込み品質の診断

まず、現在の埋め込みモデルと検索結果のスコアを確認します。

typescript// Dify API で検索結果とスコアを取得
const diagnosisStep1 = async (query: string) => {
  const response = await fetch(
    'https://api.dify.ai/v1/datasets/{dataset_id}/retrieve',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.DIFY_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query: query,
        retrieval_model: {
          top_k: 10,
          score_threshold: 0.0, // 診断のため閾値を無効化
        },
      }),
    }
  );

  const data = await response.json();
  return data.records;
};

このコードは、診断のためにスコア閾値を無効化し、Top K を多めに設定して検索を実行します。すべての候補とそのスコアを確認できます。

実行結果の分析:

typescript// 取得した結果を分析
const analyzeResults = (
  records: any[],
  expectedKeyword: string
) => {
  console.log('=== 検索結果分析 ===');

  records.forEach((record, index) => {
    const hasKeyword =
      record.content.includes(expectedKeyword);
    console.log(
      `${index + 1}. スコア: ${record.score.toFixed(3)}`
    );
    console.log(
      `   キーワード "${expectedKeyword}" 含む: ${
        hasKeyword ? 'Yes' : 'No'
      }`
    );
    console.log(
      `   内容抜粋: ${record.content.substring(0, 50)}...`
    );
    console.log('');
  });
};

// 使用例
const records = await diagnosisStep1(
  'Next.jsのデプロイ手順'
);
analyzeResults(records, 'デプロイ');

この分析により、「デプロイ」というキーワードを含む文書のスコアを確認できます。期待する文書が低スコアの場合、埋め込みモデルが適切でない可能性があります。

ステップ 2: チャンク分割の確認

次に、ナレッジベースの文書がどのように分割されているかを確認します。

typescript// ナレッジベースの文書セグメント(チャンク)を取得
const diagnosisStep2 = async (documentId: string) => {
  const response = await fetch(
    `https://api.dify.ai/v1/datasets/{dataset_id}/documents/${documentId}/segments`,
    {
      headers: {
        Authorization: `Bearer ${process.env.DIFY_API_KEY}`,
      },
    }
  );

  const data = await response.json();
  return data.data;
};

このコードは、特定の文書がどのようにチャンクに分割されているかを取得します。各チャンクの内容と文字数を確認しましょう。

チャンク分析:

typescript// チャンクの品質を分析
const analyzeChunks = (segments: any[]) => {
  console.log('=== チャンク分析 ===');

  segments.forEach((segment, index) => {
    const length = segment.content.length;
    const hasContext =
      segment.content.split('\n').length > 1;

    console.log(`チャンク ${index + 1}:`);
    console.log(`  文字数: ${length}`);
    console.log(`  複数段落: ${hasContext ? 'Yes' : 'No'}`);
    console.log(
      `  内容: ${segment.content.substring(0, 100)}...`
    );
    console.log('');
  });

  // 統計情報
  const avgLength =
    segments.reduce((sum, s) => sum + s.content.length, 0) /
    segments.length;
  console.log(`平均文字数: ${avgLength.toFixed(0)} 文字`);
};

この分析により、チャンクサイズが適切か、情報が分散していないかを確認できます。例えば、「デプロイ手順」が複数のチャンクに分かれている場合、チャンクサイズを大きくする必要があります。

ステップ 3: 検索パラメータの最適化

診断結果に基づいて、検索パラメータを調整します。

typescript// 最適化された検索設定
interface OptimizedConfig {
  embedding: {
    model: string;
    provider: string;
  };
  chunking: {
    max_tokens: number;
    chunk_overlap: number;
  };
  retrieval: {
    topK: number;
    scoreThreshold: number;
    rerankEnabled: boolean;
    rerankTopN: number;
  };
}

// 技術ドキュメント向けの最適化設定例
const optimizedConfig: OptimizedConfig = {
  embedding: {
    model: 'text-embedding-ada-002',
    provider: 'openai',
  },
  chunking: {
    max_tokens: 1000, // 技術文書は大きめ
    chunk_overlap: 150, // 15% のオーバーラップ
  },
  retrieval: {
    topK: 5,
    scoreThreshold: 0.65, // 診断結果から調整
    rerankEnabled: true,
    rerankTopN: 3,
  },
};

このコードは、診断結果に基づいて最適化された設定を定義しています。埋め込みモデル、チャンク分割、検索パラメータをすべて考慮した総合的な設定です。

ステップ 4: 改善の検証

設定変更後、実際に検索精度が改善したかを検証します。

typescript// 改善前後の比較
const compareResults = async (
  query: string,
  before: any[],
  after: any[]
) => {
  console.log('=== 改善前後の比較 ===');
  console.log(`質問: ${query}`);
  console.log('');

  console.log('【改善前】');
  before.slice(0, 3).forEach((record, index) => {
    console.log(
      `  ${index + 1}. スコア: ${record.score.toFixed(3)}`
    );
    console.log(
      `     ${record.content.substring(0, 60)}...`
    );
  });

  console.log('');
  console.log('【改善後】');
  after.slice(0, 3).forEach((record, index) => {
    console.log(
      `  ${index + 1}. スコア: ${record.score.toFixed(3)}`
    );
    console.log(
      `     ${record.content.substring(0, 60)}...`
    );
  });

  // スコア向上率を計算
  const beforeAvg =
    before
      .slice(0, 3)
      .reduce((sum, r) => sum + r.score, 0) / 3;
  const afterAvg =
    after.slice(0, 3).reduce((sum, r) => sum + r.score, 0) /
    3;
  const improvement =
    ((afterAvg - beforeAvg) / beforeAvg) * 100;

  console.log('');
  console.log(`スコア向上率: ${improvement.toFixed(1)}%`);
};

この比較により、改善の効果を定量的に測定できます。スコアの向上だけでなく、実際に期待する文書が上位にランクインしているかも確認しましょう。

完全な診断フロー

以下は、上記のステップを統合した完全な診断フローです。

typescript// 包括的な RAG 診断ツール
class RAGDiagnostics {
  private apiKey: string;
  private datasetId: string;

  constructor(apiKey: string, datasetId: string) {
    this.apiKey = apiKey;
    this.datasetId = datasetId;
  }

  // 完全な診断を実行
  async runFullDiagnosis(
    query: string,
    expectedDocId?: string
  ) {
    console.log('=== Dify RAG 診断開始 ===\n');

    // 1. 埋め込み品質診断
    console.log('ステップ 1: 埋め込み品質診断');
    const searchResults = await this.searchWithFullDetails(
      query
    );
    this.analyzeEmbeddingQuality(searchResults);

    // 2. チャンク分割診断
    if (expectedDocId) {
      console.log('\nステップ 2: チャンク分割診断');
      const chunks = await this.getDocumentChunks(
        expectedDocId
      );
      this.analyzeChunkQuality(chunks);
    }

    // 3. 検索パラメータ診断
    console.log('\nステップ 3: 検索パラメータ診断');
    this.analyzeSearchParameters(searchResults);

    // 4. 改善提案
    console.log('\n=== 改善提案 ===');
    this.generateRecommendations(searchResults);
  }

  // その他のメソッドは省略...
}

このクラスは、すべての診断ステップを統合し、包括的な分析と改善提案を提供します。実際のプロジェクトでは、このようなツールを作成しておくことで、効率的に問題を特定できますね。

まとめ

Dify RAG でヒットしない問題は、埋め込み品質チャンク分割検索パラメータ の 3 つの要素を体系的に診断することで解決できます。

本記事でご紹介した診断手法をまとめると、以下のようになります。

診断の基本ステップ:

  1. 埋め込み品質の確認: スコア閾値を無効化して全候補を取得し、期待する文書のスコアを確認する
  2. チャンク分割の検証: 文書がどのように分割されているかを確認し、情報の分散や欠落がないかチェックする
  3. 検索パラメータの最適化: Top K、スコア閾値、リランキングを段階的に調整し、最適なバランスを見つける
  4. 改善の検証: 設定変更前後で検索結果を比較し、定量的に効果を測定する

重要なポイント:

  • 埋め込みモデルは文書の言語と分野に合ったものを選択しましょう
  • チャンクサイズは文書タイプに応じて調整し、オーバーラップを適切に設定することで境界情報の取りこぼしを防げます
  • 検索パラメータは一度に変更せず、段階的に調整して効果を確認しながら最適化していきましょう
  • リランキングを活用することで、検索精度を大幅に向上できます

RAG の検索精度向上は試行錯誤が必要ですが、体系的なアプローチを取ることで、確実に改善できるはずです。本記事の診断手法を活用して、ぜひ高品質な RAG システムを構築してくださいね。

関連リンク