T-CREATOR

RAG(Retrieval-Augmented Generation)構成を Dify で実装する方法

RAG(Retrieval-Augmented Generation)構成を Dify で実装する方法

近年、AI 技術の進歩により、大規模言語モデル(LLM)を活用したアプリケーション開発が急速に普及しています。しかし、従来の LLM には学習データの時点での知識しか持たないという制約がありました。

この課題を解決する革新的な技術が「RAG(Retrieval-Augmented Generation)」です。RAG は、外部の知識ベースから関連情報を検索し、その情報を基に回答を生成する仕組みで、より正確で最新の情報に基づいた応答を可能にします。

本記事では、Dify プラットフォームで RAG 構成を実装する具体的な方法について、基礎概念から実践的な実装まで詳しく解説いたします。初心者の方にもわかりやすく、段階的に RAG システムを構築できるよう、豊富なコード例とともにご紹介していきます。

RAG の基本概念と仕組み

RAG とは何か:検索拡張生成の原理

RAG(Retrieval-Augmented Generation)は、「検索」と「生成」を組み合わせた AI 技術です。従来の生成 AI が学習済みの知識のみに依存するのに対し、RAG は外部データベースから関連情報を動的に取得し、その情報を基に回答を生成します。

この仕組みにより、以下のような利点が生まれます:

#利点説明効果
1最新情報の活用リアルタイムでデータベースを参照時事性の高い回答が可能
2専門知識の拡張特定分野の知識ベースを活用専門性の高い回答を提供
3幻覚の削減根拠となる情報源を明示回答の信頼性が向上
4継続的な学習モデル再学習なしで知識更新運用コストの削減

従来の LLM との違いと優位性

従来の LLM と RAG システムの違いを具体的に比較してみましょう。

typescript// 従来のLLM:固定された学習データのみを使用
interface TraditionalLLM {
  input: string;
  output: string;
  knowledgeCutoff: Date; // 学習データの期限
  canUpdateKnowledge: false;
}

// RAGシステム:動的な知識検索を組み合わせ
interface RAGSystem {
  input: string;
  retrievedContext: string[]; // 検索された関連文書
  output: string;
  knowledgeSource: 'dynamic'; // 動的な知識ソース
  canUpdateKnowledge: true;
  sourceDocuments: Document[]; // 根拠となる文書
}

この違いにより、RAG システムは以下の優位性を持ちます:

情報の鮮度:データベースを更新するだけで最新情報を反映できます。従来の LLM では、新しい情報を取り込むためにモデル全体の再学習が必要でした。

専門性の向上:特定分野の文書を集めた知識ベースを構築することで、その分野に特化した高品質な回答を生成できます。

透明性の確保:回答の根拠となった文書を明示できるため、情報の出典を確認でき、信頼性が向上します。

RAG アーキテクチャの構成要素

RAG システムは、主に以下の 4 つのコンポーネントで構成されます:

typescriptinterface RAGArchitecture {
  // 1. 文書処理コンポーネント
  documentProcessor: {
    chunking: (document: string) => string[]; // 文書の分割
    preprocessing: (chunks: string[]) => string[]; // 前処理
    embedding: (chunks: string[]) => number[][]; // ベクトル化
  };

  // 2. 検索コンポーネント
  retriever: {
    vectorStore: VectorDatabase; // ベクトルデータベース
    similaritySearch: (
      query: string,
      k: number
    ) => Document[];
    hybridSearch?: (query: string) => Document[]; // ハイブリッド検索
  };

  // 3. 生成コンポーネント
  generator: {
    llm: LanguageModel; // 言語モデル
    promptTemplate: string; // プロンプトテンプレート
    generate: (context: string[], query: string) => string;
  };

  // 4. 統合コンポーネント
  orchestrator: {
    pipeline: (query: string) => Promise<RAGResponse>;
    contextManager: ContextManager; // コンテキスト管理
    errorHandler: ErrorHandler; // エラーハンドリング
  };
}

各コンポーネントが連携することで、効果的な RAG システムが構築されます。

ベクトル検索と意味検索の仕組み

RAG の核心となるのが、ベクトル検索による意味検索です。この仕組みを詳しく見てみましょう。

typescript// 埋め込みベクトルの生成
class EmbeddingGenerator {
  private model: EmbeddingModel;

  constructor(
    modelName: string = 'text-embedding-ada-002'
  ) {
    this.model = new EmbeddingModel(modelName);
  }

  // テキストをベクトルに変換
  async generateEmbedding(text: string): Promise<number[]> {
    // テキストの前処理
    const cleanedText = this.preprocessText(text);

    // 埋め込みベクトルの生成
    const embedding = await this.model.embed(cleanedText);

    return embedding;
  }

  private preprocessText(text: string): string {
    return text
      .replace(/\s+/g, ' ') // 空白文字の正規化
      .replace(/[^\w\s]/g, '') // 特殊文字の除去
      .toLowerCase() // 小文字変換
      .trim();
  }
}

// 類似度計算
class SimilarityCalculator {
  // コサイン類似度の計算
  static cosineSimilarity(
    vecA: number[],
    vecB: number[]
  ): number {
    const dotProduct = vecA.reduce(
      (sum, a, i) => sum + a * vecB[i],
      0
    );

    const magnitudeA = Math.sqrt(
      vecA.reduce((sum, a) => sum + a * a, 0)
    );

    const magnitudeB = Math.sqrt(
      vecB.reduce((sum, b) => sum + b * b, 0)
    );

    return dotProduct / (magnitudeA * magnitudeB);
  }

  // ユークリッド距離の計算
  static euclideanDistance(
    vecA: number[],
    vecB: number[]
  ): number {
    const squaredDifferences = vecA.map((a, i) =>
      Math.pow(a - vecB[i], 2)
    );

    return Math.sqrt(
      squaredDifferences.reduce(
        (sum, diff) => sum + diff,
        0
      )
    );
  }
}

意味検索では、単純なキーワードマッチングではなく、文章の「意味」を理解して関連文書を検索します。これにより、同じ意味を持つ異なる表現でも適切に検索できるようになります。

Dify での RAG 実装の準備

必要なコンポーネントの理解

Dify で RAG システムを構築するには、以下のコンポーネントの理解が必要です:

typescriptinterface DifyRAGComponents {
  // Dify 固有のコンポーネント
  difyApp: {
    appType: 'chatbot' | 'completion';
    workflowConfig: WorkflowConfig;
    knowledgeBase: KnowledgeBase;
  };

  // 知識ベース設定
  knowledgeBase: {
    name: string;
    description: string;
    embeddingModel:
      | 'text-embedding-ada-002'
      | 'text-embedding-3-small';
    retrievalConfig: RetrievalConfig;
  };

  // 検索設定
  retrievalConfig: {
    searchMethod: 'vector' | 'full_text' | 'hybrid';
    topK: number; // 取得する文書数
    scoreThreshold: number; // 類似度の閾値
    rerankingModel?: string; // 再ランキングモデル
  };

  // データセット管理
  dataset: {
    documents: Document[];
    segments: Segment[]; // 分割されたテキスト
    embeddings: number[][]; // 埋め込みベクトル
  };
}

これらのコンポーネントを適切に設定することで、効果的な RAG システムを構築できます。

データソースの準備と前処理

RAG システムの品質は、データソースの質に大きく依存します。適切なデータ準備が成功の鍵となります。

typescript// データソースの種類と処理方法
interface DataSource {
  type:
    | 'pdf'
    | 'docx'
    | 'txt'
    | 'html'
    | 'markdown'
    | 'csv';
  content: string | Buffer;
  metadata: {
    title: string;
    author?: string;
    createdAt: Date;
    tags: string[];
    category: string;
  };
}

class DataPreprocessor {
  // PDFファイルの処理
  async processPDF(
    file: Buffer
  ): Promise<ProcessedDocument> {
    const pdfParser = new PDFParser();
    const text = await pdfParser.extract(file);

    return {
      content: this.cleanText(text),
      metadata: await this.extractMetadata(file),
      chunks: this.chunkText(text, 1000, 200), // 1000文字、200文字重複
    };
  }

  // テキストのクリーニング
  private cleanText(text: string): string {
    return text
      .replace(/\r\n/g, '\n') // 改行の統一
      .replace(/\n{3,}/g, '\n\n') // 過度な改行の削除
      .replace(/\s{2,}/g, ' ') // 連続空白の削除
      .replace(
        /[^\w\s\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/g,
        ''
      ) // 不要な文字の除去
      .trim();
  }

  // テキストの分割(チャンキング)
  private chunkText(
    text: string,
    chunkSize: number,
    overlap: number
  ): TextChunk[] {
    const chunks: TextChunk[] = [];
    const sentences = this.splitIntoSentences(text);

    let currentChunk = '';
    let currentLength = 0;

    for (const sentence of sentences) {
      if (
        currentLength + sentence.length > chunkSize &&
        currentChunk
      ) {
        chunks.push({
          content: currentChunk.trim(),
          startIndex: chunks.length * (chunkSize - overlap),
          endIndex:
            chunks.length * (chunkSize - overlap) +
            currentChunk.length,
        });

        // オーバーラップの処理
        const overlapText = this.getOverlapText(
          currentChunk,
          overlap
        );
        currentChunk = overlapText + sentence;
        currentLength = currentChunk.length;
      } else {
        currentChunk += sentence;
        currentLength += sentence.length;
      }
    }

    // 最後のチャンクを追加
    if (currentChunk.trim()) {
      chunks.push({
        content: currentChunk.trim(),
        startIndex: chunks.length * (chunkSize - overlap),
        endIndex:
          chunks.length * (chunkSize - overlap) +
          currentChunk.length,
      });
    }

    return chunks;
  }

  // 文章の分割
  private splitIntoSentences(text: string): string[] {
    // 日本語と英語の文章区切りに対応
    const sentencePattern = /[。!?\.\!\?]+\s*/g;
    return text
      .split(sentencePattern)
      .filter((s) => s.trim().length > 0);
  }

  // オーバーラップテキストの取得
  private getOverlapText(
    text: string,
    overlapSize: number
  ): string {
    const words = text.split(/\s+/);
    return (
      words.slice(-Math.floor(overlapSize / 10)).join(' ') +
      ' '
    );
  }
}

データの前処理では、以下の点に注意が必要です:

文字エンコーディング:異なるファイル形式からのテキスト抽出時に、文字化けを防ぐための適切なエンコーディング処理が重要です。

チャンクサイズの最適化:長すぎると検索精度が下がり、短すぎると文脈が失われます。一般的には 500-1500 文字程度が適切です。

オーバーラップの設定:隣接するチャンク間で情報の連続性を保つため、10-20%程度のオーバーラップを設けることを推奨します。

埋め込みモデルの選択基準

埋め込みモデルの選択は、RAG システムの性能に直接影響します。以下の基準で選択しましょう:

#評価項目text-embedding-ada-002text-embedding-3-smalltext-embedding-3-large
1次元数1,5361,5363,072
2処理速度高速高速中速
3精度標準高精度最高精度
4コスト
5多言語対応良好優秀優秀
typescript// 埋め込みモデルの設定と比較
class EmbeddingModelSelector {
  private models = {
    'ada-002': {
      dimensions: 1536,
      costPerToken: 0.0001,
      maxTokens: 8191,
      languages: ['en', 'ja', 'zh', 'ko'],
    },
    'embedding-3-small': {
      dimensions: 1536,
      costPerToken: 0.00002,
      maxTokens: 8191,
      languages: ['en', 'ja', 'zh', 'ko', 'es', 'fr', 'de'],
    },
    'embedding-3-large': {
      dimensions: 3072,
      costPerToken: 0.00013,
      maxTokens: 8191,
      languages: ['en', 'ja', 'zh', 'ko', 'es', 'fr', 'de'],
    },
  };

  // 用途に応じたモデル選択
  selectModel(requirements: ModelRequirements): string {
    const {
      budget,
      accuracy,
      speed,
      languages,
      documentCount,
    } = requirements;

    // 予算重視の場合
    if (budget === 'low') {
      return 'embedding-3-small';
    }

    // 精度重視の場合
    if (accuracy === 'high' && documentCount > 10000) {
      return 'embedding-3-large';
    }

    // 速度重視の場合
    if (speed === 'high') {
      return 'ada-002';
    }

    // バランス重視の場合
    return 'embedding-3-small';
  }

  // コスト見積もり
  estimateCost(
    modelName: string,
    totalTokens: number
  ): number {
    const model = this.models[modelName];
    return totalTokens * model.costPerToken;
  }
}

ベクトルデータベースの設定

Dify では Pinecone、Weaviate、Qdrant など複数のベクトルデータベースをサポートしています。それぞれの特徴を理解して選択しましょう。

typescript// ベクトルデータベースの設定
interface VectorDBConfig {
  provider: 'pinecone' | 'weaviate' | 'qdrant' | 'chroma';
  connectionConfig: {
    apiKey: string;
    environment: string;
    indexName: string;
  };
  indexConfig: {
    dimensions: number;
    metric: 'cosine' | 'euclidean' | 'dotproduct';
    replicas: number;
    shards?: number;
  };
}

class VectorDatabaseManager {
  private config: VectorDBConfig;
  private client: VectorDBClient;

  constructor(config: VectorDBConfig) {
    this.config = config;
    this.client = this.initializeClient();
  }

  // データベースクライアントの初期化
  private initializeClient(): VectorDBClient {
    switch (this.config.provider) {
      case 'pinecone':
        return new PineconeClient({
          apiKey: this.config.connectionConfig.apiKey,
          environment:
            this.config.connectionConfig.environment,
        });

      case 'weaviate':
        return new WeaviateClient({
          scheme: 'https',
          host: this.config.connectionConfig.environment,
          apiKey: this.config.connectionConfig.apiKey,
        });

      case 'qdrant':
        return new QdrantClient({
          url: this.config.connectionConfig.environment,
          apiKey: this.config.connectionConfig.apiKey,
        });

      default:
        throw new Error(
          `Unsupported provider: ${this.config.provider}`
        );
    }
  }

  // インデックスの作成
  async createIndex(): Promise<void> {
    const indexSpec = {
      name: this.config.connectionConfig.indexName,
      dimension: this.config.indexConfig.dimensions,
      metric: this.config.indexConfig.metric,
      replicas: this.config.indexConfig.replicas,
    };

    try {
      await this.client.createIndex(indexSpec);
      console.log(
        `インデックス ${indexSpec.name} を作成しました`
      );
    } catch (error) {
      console.error('インデックス作成エラー:', error);
      throw error;
    }
  }

  // ベクトルの挿入
  async upsertVectors(
    vectors: VectorData[]
  ): Promise<void> {
    const batchSize = 100; // バッチサイズ

    for (let i = 0; i < vectors.length; i += batchSize) {
      const batch = vectors.slice(i, i + batchSize);

      try {
        await this.client.upsert({
          indexName: this.config.connectionConfig.indexName,
          vectors: batch.map((v) => ({
            id: v.id,
            values: v.embedding,
            metadata: v.metadata,
          })),
        });

        console.log(
          `バッチ ${
            Math.floor(i / batchSize) + 1
          } を挿入完了`
        );
      } catch (error) {
        console.error(
          `バッチ挿入エラー (${i}-${i + batchSize}):`,
          error
        );
        throw error;
      }
    }
  }

  // 類似ベクトルの検索
  async searchSimilar(
    queryVector: number[],
    topK: number = 5,
    filter?: Record<string, any>
  ): Promise<SearchResult[]> {
    try {
      const response = await this.client.query({
        indexName: this.config.connectionConfig.indexName,
        vector: queryVector,
        topK,
        includeMetadata: true,
        filter,
      });

      return response.matches.map((match) => ({
        id: match.id,
        score: match.score,
        metadata: match.metadata,
      }));
    } catch (error) {
      console.error('検索エラー:', error);
      throw error;
    }
  }
}

ベクトルデータベースの選択では、以下の要素を考慮する必要があります:

スケーラビリティ:予想されるデータ量と検索頻度に対応できる性能を持つか確認しましょう。

レイテンシ:リアルタイム検索が必要な場合は、応答速度を重視したデータベースを選択します。

コスト効率:ストレージコストと検索コストのバランスを考慮して選択することが重要です。

段階的な実装手順

基本的な RAG 構成の作成

Dify で RAG システムを構築する最初のステップは、基本的な構成を作成することです。以下の手順で進めていきます。

typescript// Dify RAG アプリケーションの基本設定
interface DifyRAGApp {
  appId: string;
  name: string;
  description: string;
  type: 'chatbot';
  model_config: {
    provider: 'openai';
    model: 'gpt-4';
    parameters: {
      temperature: 0.1;
      max_tokens: 1000;
      top_p: 0.9;
    };
  };
  dataset_configs: DatasetConfig[];
  retrieval_model: RetrievalModelConfig;
}

class DifyRAGBuilder {
  private config: DifyRAGApp;
  private apiClient: DifyAPIClient;

  constructor(
    apiKey: string,
    baseUrl: string = 'https://api.dify.ai/v1'
  ) {
    this.apiClient = new DifyAPIClient(apiKey, baseUrl);
  }

  // RAGアプリケーションの初期化
  async initializeRAGApp(
    config: Partial<DifyRAGApp>
  ): Promise<string> {
    const defaultConfig: DifyRAGApp = {
      appId: '',
      name: config.name || 'RAG Application',
      description:
        config.description || 'RAG-powered chatbot',
      type: 'chatbot',
      model_config: {
        provider: 'openai',
        model: 'gpt-4',
        parameters: {
          temperature: 0.1,
          max_tokens: 1000,
          top_p: 0.9,
        },
      },
      dataset_configs: [],
      retrieval_model: {
        search_method: 'hybrid',
        reranking_enable: true,
        reranking_model: {
          provider: 'cohere',
          model: 'rerank-multilingual-v2.0',
        },
        top_k: 5,
        score_threshold: 0.5,
      },
    };

    this.config = { ...defaultConfig, ...config };

    try {
      const response = await this.apiClient.createApp(
        this.config
      );
      this.config.appId = response.app_id;

      console.log(
        `RAGアプリケーション作成完了: ${response.app_id}`
      );
      return response.app_id;
    } catch (error) {
      console.error('アプリケーション作成エラー:', error);
      throw error;
    }
  }

  // システムプロンプトの設定
  async configureSystemPrompt(): Promise<void> {
    const systemPrompt = `
あなたは専門的な知識を持つAIアシスタントです。
提供された文書の内容に基づいて、正確で有用な回答を提供してください。

## 回答のガイドライン:
1. 提供された文書の内容のみを根拠として回答する
2. 文書に記載されていない情報は推測しない
3. 不明な点がある場合は素直に「わからない」と答える
4. 回答の根拠となる文書の部分を明示する
5. 簡潔で理解しやすい表現を心がける

## 文書が提供されていない場合:
「申し訳ございませんが、この質問にお答えするための関連文書が見つかりませんでした。
より具体的な質問や、別の表現で質問していただけますでしょうか?」

提供された文書: {{#context#}}
ユーザーの質問: {{#query#}}
`;

    try {
      await this.apiClient.updateAppConfig(
        this.config.appId,
        {
          system_prompt: systemPrompt,
        }
      );

      console.log('システムプロンプト設定完了');
    } catch (error) {
      console.error('システムプロンプト設定エラー:', error);
      throw error;
    }
  }
}

基本構成では、以下の要素を適切に設定することが重要です:

モデル選択:回答生成に使用する LLM を選択します。複雑な質問には GPT-4、コスト重視なら GPT-3.5-turbo を選択しましょう。

温度設定:創造性と一貫性のバランスを調整します。RAG では事実に基づく回答が重要なため、0.1-0.3 程度の低い値を推奨します。

検索設定:取得する文書数(top_k)と類似度閾値(score_threshold)を適切に設定します。

文書の前処理とチャンク分割

効果的な RAG システムには、適切な文書の前処理とチャンク分割が不可欠です。

typescript// 高度なチャンク分割戦略
class AdvancedChunker {
  private chunkSize: number;
  private overlap: number;
  private preserveStructure: boolean;

  constructor(
    chunkSize: number = 1000,
    overlap: number = 200,
    preserveStructure: boolean = true
  ) {
    this.chunkSize = chunkSize;
    this.overlap = overlap;
    this.preserveStructure = preserveStructure;
  }

  // 構造を保持したチャンク分割
  async chunkDocument(
    document: ProcessedDocument
  ): Promise<Chunk[]> {
    if (this.preserveStructure) {
      return this.structureAwareChunking(document);
    } else {
      return this.simpleChunking(document.content);
    }
  }

  // 構造認識チャンク分割
  private structureAwareChunking(
    document: ProcessedDocument
  ): Chunk[] {
    const chunks: Chunk[] = [];
    const sections = this.identifyDocumentSections(
      document.content
    );

    for (const section of sections) {
      if (section.content.length <= this.chunkSize) {
        // セクション全体が1チャンクに収まる場合
        chunks.push({
          id: this.generateChunkId(),
          content: section.content,
          metadata: {
            ...document.metadata,
            section_title: section.title,
            section_type: section.type,
            chunk_index: chunks.length,
          },
          embedding: null, // 後で生成
        });
      } else {
        // セクションを複数チャンクに分割
        const sectionChunks = this.splitSection(section);
        chunks.push(...sectionChunks);
      }
    }

    return chunks;
  }

  // 文書セクションの識別
  private identifyDocumentSections(
    content: string
  ): DocumentSection[] {
    const sections: DocumentSection[] = [];
    const lines = content.split('\n');

    let currentSection: DocumentSection = {
      title: 'Introduction',
      type: 'content',
      content: '',
      level: 0,
    };

    for (const line of lines) {
      // 見出しの検出
      const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
      if (headingMatch) {
        // 前のセクションを保存
        if (currentSection.content.trim()) {
          sections.push({ ...currentSection });
        }

        // 新しいセクションを開始
        currentSection = {
          title: headingMatch[2],
          type: 'heading',
          content: '',
          level: headingMatch[1].length,
        };
      } else if (line.trim()) {
        currentSection.content += line + '\n';
      }
    }

    // 最後のセクションを追加
    if (currentSection.content.trim()) {
      sections.push(currentSection);
    }

    return sections;
  }

  // セクションの分割
  private splitSection(section: DocumentSection): Chunk[] {
    const chunks: Chunk[] = [];
    const paragraphs = section.content
      .split('\n\n')
      .filter((p) => p.trim());

    let currentChunk = '';
    let currentLength = 0;

    for (const paragraph of paragraphs) {
      const paragraphLength = paragraph.length;

      if (
        currentLength + paragraphLength > this.chunkSize &&
        currentChunk
      ) {
        // チャンクサイズを超える場合、現在のチャンクを保存
        chunks.push({
          id: this.generateChunkId(),
          content: this.addSectionContext(
            currentChunk,
            section.title
          ),
          metadata: {
            section_title: section.title,
            section_type: section.type,
            chunk_index: chunks.length,
            total_chunks: 0, // 後で更新
          },
          embedding: null,
        });

        // オーバーラップを考慮して次のチャンクを開始
        const overlapText = this.getOverlapText(
          currentChunk,
          this.overlap
        );
        currentChunk = overlapText + paragraph;
        currentLength = currentChunk.length;
      } else {
        currentChunk +=
          (currentChunk ? '\n\n' : '') + paragraph;
        currentLength +=
          paragraphLength + (currentChunk ? 2 : 0);
      }
    }

    // 最後のチャンクを追加
    if (currentChunk.trim()) {
      chunks.push({
        id: this.generateChunkId(),
        content: this.addSectionContext(
          currentChunk,
          section.title
        ),
        metadata: {
          section_title: section.title,
          section_type: section.type,
          chunk_index: chunks.length,
          total_chunks: chunks.length + 1,
        },
        embedding: null,
      });
    }

    // total_chunks を更新
    chunks.forEach((chunk) => {
      chunk.metadata.total_chunks = chunks.length;
    });

    return chunks;
  }

  // セクションコンテキストの追加
  private addSectionContext(
    content: string,
    sectionTitle: string
  ): string {
    return `## ${sectionTitle}\n\n${content}`;
  }

  // チャンクIDの生成
  private generateChunkId(): string {
    return `chunk_${Date.now()}_${Math.random()
      .toString(36)
      .substr(2, 9)}`;
  }

  // オーバーラップテキストの取得
  private getOverlapText(
    text: string,
    overlapSize: number
  ): string {
    const sentences = text
      .split(/[。!?\.\!\?]+/)
      .filter((s) => s.trim());
    let overlapText = '';
    let currentLength = 0;

    for (let i = sentences.length - 1; i >= 0; i--) {
      const sentence = sentences[i];
      if (currentLength + sentence.length > overlapSize) {
        break;
      }
      overlapText = sentence + overlapText;
      currentLength += sentence.length;
    }

    return overlapText.trim() + (overlapText ? ' ' : '');
  }
}

チャンク分割では以下の戦略を採用することを推奨します:

構造保持:見出しや段落構造を保持することで、文脈の一貫性を維持します。

適応的サイズ:文書の種類に応じてチャンクサイズを調整します(技術文書:800-1200 文字、小説:1200-1800 文字)。

メタデータ活用:各チャンクにセクション情報や位置情報を付与し、検索精度を向上させます。

ベクトル化と検索インデックス構築

チャンク分割された文書をベクトル化し、検索可能なインデックスを構築します。

typescript// ベクトル化とインデックス構築
class VectorIndexBuilder {
  private embeddingGenerator: EmbeddingGenerator;
  private vectorDB: VectorDatabaseManager;
  private batchSize: number = 50;

  constructor(
    embeddingGenerator: EmbeddingGenerator,
    vectorDB: VectorDatabaseManager
  ) {
    this.embeddingGenerator = embeddingGenerator;
    this.vectorDB = vectorDB;
  }

  // 文書のベクトル化とインデックス構築
  async buildIndex(chunks: Chunk[]): Promise<void> {
    console.log(
      `${chunks.length} 個のチャンクをベクトル化中...`
    );

    // バッチ処理でベクトル化
    for (
      let i = 0;
      i < chunks.length;
      i += this.batchSize
    ) {
      const batch = chunks.slice(i, i + this.batchSize);
      await this.processBatch(batch, i);
    }

    console.log('インデックス構築完了');
  }

  // バッチ処理
  private async processBatch(
    batch: Chunk[],
    startIndex: number
  ): Promise<void> {
    try {
      // 並列でベクトル化
      const embeddingPromises = batch.map(
        async (chunk, index) => {
          const embedding =
            await this.embeddingGenerator.generateEmbedding(
              chunk.content
            );

          return {
            id: chunk.id,
            embedding,
            metadata: {
              ...chunk.metadata,
              content: chunk.content,
              content_length: chunk.content.length,
              created_at: new Date().toISOString(),
            },
          };
        }
      );

      const vectorData = await Promise.all(
        embeddingPromises
      );

      // ベクトルデータベースに挿入
      await this.vectorDB.upsertVectors(vectorData);

      console.log(
        `バッチ ${
          Math.floor(startIndex / this.batchSize) + 1
        } 処理完了 ` +
          `(${startIndex + 1}-${startIndex + batch.length})`
      );

      // レート制限を避けるための待機
      await this.delay(1000);
    } catch (error) {
      console.error(
        `バッチ処理エラー (${startIndex}):`,
        error
      );
      throw error;
    }
  }

  // インデックスの品質チェック
  async validateIndex(
    sampleQueries: string[]
  ): Promise<ValidationResult> {
    const results: ValidationResult = {
      totalQueries: sampleQueries.length,
      successfulQueries: 0,
      averageScore: 0,
      issues: [],
    };

    let totalScore = 0;

    for (const query of sampleQueries) {
      try {
        const queryEmbedding =
          await this.embeddingGenerator.generateEmbedding(
            query
          );
        const searchResults =
          await this.vectorDB.searchSimilar(
            queryEmbedding,
            5
          );

        if (searchResults.length > 0) {
          results.successfulQueries++;
          totalScore += searchResults[0].score;

          // 低スコアの結果をチェック
          if (searchResults[0].score < 0.7) {
            results.issues.push({
              query,
              topScore: searchResults[0].score,
              issue: 'Low similarity score',
            });
          }
        } else {
          results.issues.push({
            query,
            topScore: 0,
            issue: 'No results found',
          });
        }
      } catch (error) {
        results.issues.push({
          query,
          topScore: 0,
          issue: `Search error: ${error.message}`,
        });
      }
    }

    results.averageScore =
      totalScore / results.successfulQueries;
    return results;
  }

  // ユーティリティ:遅延処理
  private delay(ms: number): Promise<void> {
    return new Promise((resolve) =>
      setTimeout(resolve, ms)
    );
  }
}

インデックス構築時の重要なポイント:

バッチ処理:大量の文書を効率的に処理するため、適切なバッチサイズで処理します。

エラーハンドリング:ネットワークエラーやレート制限に対する適切な対処を実装します。

品質保証:構築されたインデックスの品質を検証し、問題があれば調整します。

検索クエリの最適化

ユーザーのクエリを最適化して、より関連性の高い文書を検索する仕組みを実装します。

typescript// クエリ最適化エンジン
class QueryOptimizer {
  private synonymDict: Map<string, string[]>;
  private stopWords: Set<string>;
  private queryExpander: QueryExpander;

  constructor() {
    this.synonymDict = this.loadSynonymDictionary();
    this.stopWords = this.loadStopWords();
    this.queryExpander = new QueryExpander();
  }

  // クエリの最適化
  async optimizeQuery(
    originalQuery: string
  ): Promise<OptimizedQuery> {
    // 1. 基本的なクリーニング
    let cleanedQuery = this.cleanQuery(originalQuery);

    // 2. 同義語展開
    const expandedQueries =
      this.expandSynonyms(cleanedQuery);

    // 3. クエリ拡張
    const contextualQueries =
      await this.queryExpander.expand(cleanedQuery);

    // 4. 複数クエリの生成
    const multipleQueries =
      this.generateMultipleQueries(cleanedQuery);

    return {
      original: originalQuery,
      cleaned: cleanedQuery,
      expanded: expandedQueries,
      contextual: contextualQueries,
      multiple: multipleQueries,
      searchStrategies:
        this.determineSearchStrategies(cleanedQuery),
    };
  }

  // クエリのクリーニング
  private cleanQuery(query: string): string {
    return query
      .toLowerCase()
      .replace(
        /[^\w\s\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/g,
        ' '
      )
      .replace(/\s+/g, ' ')
      .trim();
  }

  // 同義語展開
  private expandSynonyms(query: string): string[] {
    const words = query.split(' ');
    const expandedQueries: string[] = [query];

    for (const word of words) {
      const synonyms = this.synonymDict.get(word);
      if (synonyms) {
        for (const synonym of synonyms) {
          const expandedQuery = query.replace(
            word,
            synonym
          );
          expandedQueries.push(expandedQuery);
        }
      }
    }

    return [...new Set(expandedQueries)];
  }

  // 複数クエリの生成
  private generateMultipleQueries(query: string): string[] {
    const queries: string[] = [];

    // 疑問文への変換
    queries.push(`${query}とは何ですか`);
    queries.push(`${query}について教えて`);
    queries.push(`${query}の方法`);
    queries.push(`${query}の手順`);

    // キーワード抽出
    const keywords = this.extractKeywords(query);
    if (keywords.length > 1) {
      queries.push(keywords.join(' '));
    }

    return queries.filter((q) => q !== query);
  }

  // 検索戦略の決定
  private determineSearchStrategies(
    query: string
  ): SearchStrategy[] {
    const strategies: SearchStrategy[] = [];

    // クエリの特性に基づいて戦略を選択
    if (query.includes('方法') || query.includes('手順')) {
      strategies.push({
        type: 'procedural',
        weight: 0.8,
        filters: { section_type: 'procedure' },
      });
    }

    if (query.includes('とは') || query.includes('定義')) {
      strategies.push({
        type: 'definitional',
        weight: 0.9,
        filters: { section_type: 'definition' },
      });
    }

    if (query.length > 50) {
      strategies.push({
        type: 'semantic',
        weight: 0.7,
        searchMethod: 'vector',
      });
    } else {
      strategies.push({
        type: 'keyword',
        weight: 0.6,
        searchMethod: 'hybrid',
      });
    }

    return strategies;
  }

  // 同義語辞書の読み込み
  private loadSynonymDictionary(): Map<string, string[]> {
    const synonyms = new Map<string, string[]>();

    // 技術用語の同義語
    synonyms.set('実装', ['導入', '構築', '開発', '作成']);
    synonyms.set('方法', [
      '手順',
      'やり方',
      'プロセス',
      '手法',
    ]);
    synonyms.set('問題', [
      '課題',
      'エラー',
      'トラブル',
      '不具合',
    ]);
    synonyms.set('設定', [
      '構成',
      'コンフィグ',
      '設置',
      'セットアップ',
    ]);

    return synonyms;
  }

  // ストップワードの読み込み
  private loadStopWords(): Set<string> {
    return new Set([
      'の',
      'に',
      'は',
      'を',
      'が',
      'で',
      'と',
      'から',
      'まで',
      'について',
      'に関して',
      'において',
      'によって',
      'a',
      'an',
      'the',
      'and',
      'or',
      'but',
      'in',
      'on',
      'at',
    ]);
  }

  // キーワード抽出
  private extractKeywords(query: string): string[] {
    const words = query.split(' ');
    return words.filter(
      (word) => word.length > 2 && !this.stopWords.has(word)
    );
  }
}

// クエリ拡張器
class QueryExpander {
  private llmClient: LLMClient;

  constructor() {
    this.llmClient = new LLMClient();
  }

  // LLMを使用したクエリ拡張
  async expand(query: string): Promise<string[]> {
    const prompt = `
以下の質問を、同じ意味で異なる表現の質問に3つ書き換えてください。
元の質問の意図を保ちながら、より具体的または一般的な表現を含めてください。

元の質問: ${query}

書き換えた質問:
1.
2.
3.
`;

    try {
      const response = await this.llmClient.complete(
        prompt,
        {
          temperature: 0.7,
          max_tokens: 200,
        }
      );

      const expandedQueries =
        this.parseExpandedQueries(response);
      return expandedQueries;
    } catch (error) {
      console.error('クエリ拡張エラー:', error);
      return [];
    }
  }

  // 拡張クエリの解析
  private parseExpandedQueries(response: string): string[] {
    const lines = response.split('\n');
    const queries: string[] = [];

    for (const line of lines) {
      const match = line.match(/^\d+\.\s*(.+)$/);
      if (match) {
        queries.push(match[1].trim());
      }
    }

    return queries;
  }
}

生成プロンプトの設計

効果的なプロンプト設計は、RAG システムの回答品質を大きく左右します。

typescript// プロンプトテンプレート管理
class PromptTemplateManager {
  private templates: Map<string, PromptTemplate>;

  constructor() {
    this.templates = new Map();
    this.initializeDefaultTemplates();
  }

  // デフォルトテンプレートの初期化
  private initializeDefaultTemplates(): void {
    // 基本的なQAテンプレート
    this.templates.set('basic_qa', {
      name: 'Basic Q&A',
      template: `
あなたは専門知識を持つAIアシスタントです。
提供された文書の内容に基づいて、ユーザーの質問に正確に答えてください。

## 回答ルール:
1. 提供された文書の内容のみを根拠として回答する
2. 文書に記載されていない情報は推測しない
3. 不明な点は「文書に記載されていません」と明記する
4. 回答の根拠となる文書の部分を引用する
5. 簡潔で理解しやすい日本語で回答する

## 提供された文書:
{{context}}

## ユーザーの質問:
{{question}}

## 回答:
`,
      variables: ['context', 'question'],
      category: 'qa',
    });

    // 手順説明テンプレート
    this.templates.set('procedural', {
      name: 'Procedural Guide',
      template: `
あなたは技術文書の専門家です。
提供された文書から手順や方法について説明してください。

## 回答形式:
1. 概要の説明
2. 必要な前提条件
3. 段階的な手順(番号付きリスト)
4. 注意事項やトラブルシューティング

## 提供された文書:
{{context}}

## ユーザーの質問:
{{question}}

## 手順説明:
`,
      variables: ['context', 'question'],
      category: 'procedural',
    });

    // 比較分析テンプレート
    this.templates.set('comparison', {
      name: 'Comparison Analysis',
      template: `
あなたは分析の専門家です。
提供された文書の内容を基に、比較分析を行ってください。

## 分析観点:
- 特徴の比較
- メリット・デメリット
- 適用場面
- 推奨事項

## 提供された文書:
{{context}}

## ユーザーの質問:
{{question}}

## 比較分析:
`,
      variables: ['context', 'question'],
      category: 'analysis',
    });
  }

  // テンプレートの選択
  selectTemplate(
    query: string,
    context: string[]
  ): PromptTemplate {
    // クエリの特性に基づいてテンプレートを選択
    if (this.isProceduralQuery(query)) {
      return this.templates.get('procedural')!;
    }

    if (this.isComparisonQuery(query)) {
      return this.templates.get('comparison')!;
    }

    return this.templates.get('basic_qa')!;
  }

  // プロンプトの生成
  generatePrompt(
    template: PromptTemplate,
    variables: Record<string, string>
  ): string {
    let prompt = template.template;

    for (const [key, value] of Object.entries(variables)) {
      const placeholder = `{{${key}}}`;
      prompt = prompt.replace(
        new RegExp(placeholder, 'g'),
        value
      );
    }

    return prompt;
  }

  // 手順関連クエリの判定
  private isProceduralQuery(query: string): boolean {
    const proceduralKeywords = [
      '方法',
      '手順',
      'やり方',
      'プロセス',
      '実装',
      'インストール',
      '設定',
      '構築',
      '作成',
    ];

    return proceduralKeywords.some((keyword) =>
      query.includes(keyword)
    );
  }

  // 比較関連クエリの判定
  private isComparisonQuery(query: string): boolean {
    const comparisonKeywords = [
      '違い',
      '比較',
      '差',
      'vs',
      '対',
      'どちら',
      'メリット',
      'デメリット',
      '特徴',
    ];

    return comparisonKeywords.some((keyword) =>
      query.includes(keyword)
    );
  }
}

// 動的プロンプト最適化
class DynamicPromptOptimizer {
  private performanceTracker: PerformanceTracker;
  private templateManager: PromptTemplateManager;

  constructor() {
    this.performanceTracker = new PerformanceTracker();
    this.templateManager = new PromptTemplateManager();
  }

  // プロンプトの最適化
  async optimizePrompt(
    query: string,
    context: string[],
    userFeedback?: UserFeedback
  ): Promise<OptimizedPrompt> {
    // 1. 基本テンプレートの選択
    const baseTemplate =
      this.templateManager.selectTemplate(query, context);

    // 2. コンテキストの最適化
    const optimizedContext = this.optimizeContext(
      context,
      query
    );

    // 3. 動的調整
    const adjustedTemplate =
      await this.adjustTemplateBasedOnPerformance(
        baseTemplate,
        query,
        userFeedback
      );

    // 4. プロンプトの生成
    const finalPrompt = this.templateManager.generatePrompt(
      adjustedTemplate,
      {
        context: optimizedContext.join('\n\n---\n\n'),
        question: query,
      }
    );

    return {
      prompt: finalPrompt,
      template: adjustedTemplate,
      optimizations: this.getOptimizationLog(),
    };
  }

  // コンテキストの最適化
  private optimizeContext(
    context: string[],
    query: string
  ): string[] {
    // 1. 関連度によるソート(既に検索時にソート済みと仮定)

    // 2. 重複の除去
    const uniqueContext =
      this.removeDuplicateContent(context);

    // 3. 長さの調整
    const trimmedContext = this.trimContextToFit(
      uniqueContext,
      4000
    );

    // 4. 構造化
    return this.structureContext(trimmedContext);
  }

  // 重複コンテンツの除去
  private removeDuplicateContent(
    context: string[]
  ): string[] {
    const unique: string[] = [];
    const seen = new Set<string>();

    for (const content of context) {
      const normalized = content
        .replace(/\s+/g, ' ')
        .toLowerCase();
      const signature =
        this.generateContentSignature(normalized);

      if (!seen.has(signature)) {
        seen.add(signature);
        unique.push(content);
      }
    }

    return unique;
  }

  // コンテンツ署名の生成(類似度チェック用)
  private generateContentSignature(
    content: string
  ): string {
    const words = content.split(' ').slice(0, 10); // 最初の10単語
    return words.join('_');
  }

  // コンテキストの長さ調整
  private trimContextToFit(
    context: string[],
    maxTokens: number
  ): string[] {
    let totalLength = 0;
    const trimmed: string[] = [];

    for (const content of context) {
      const estimatedTokens = content.length / 4; // 大まかなトークン数推定

      if (totalLength + estimatedTokens > maxTokens) {
        break;
      }

      trimmed.push(content);
      totalLength += estimatedTokens;
    }

    return trimmed;
  }

  // コンテキストの構造化
  private structureContext(context: string[]): string[] {
    return context.map((content, index) => {
      return `## 文書 ${index + 1}\n${content}`;
    });
  }

  // パフォーマンスに基づくテンプレート調整
  private async adjustTemplateBasedOnPerformance(
    template: PromptTemplate,
    query: string,
    feedback?: UserFeedback
  ): Promise<PromptTemplate> {
    const performance =
      await this.performanceTracker.getTemplatePerformance(
        template.name
      );

    // 低パフォーマンスの場合、テンプレートを調整
    if (performance.averageScore < 0.7) {
      return this.enhanceTemplate(
        template,
        performance.commonIssues
      );
    }

    return template;
  }

  // テンプレートの強化
  private enhanceTemplate(
    template: PromptTemplate,
    issues: string[]
  ): PromptTemplate {
    let enhancedTemplate = { ...template };

    if (issues.includes('incomplete_answer')) {
      enhancedTemplate.template += `\n\n## 回答チェックリスト:
- 質問のすべての部分に答えているか?
- 具体例や詳細が含まれているか?
- 実用的な情報が提供されているか?`;
    }

    if (issues.includes('source_citation')) {
      enhancedTemplate.template += `\n\n## 情報源の明示:
回答の各部分について、どの文書から得た情報かを明記してください。`;
    }

    return enhancedTemplate;
  }

  // 最適化ログの取得
  private getOptimizationLog(): string[] {
    return [
      'コンテキストの重複除去実行',
      'コンテキスト長の調整実行',
      'テンプレートのパフォーマンス評価実行',
    ];
  }
}

検索精度の最適化テクニック

ハイブリッド検索の実装

単一の検索手法では限界があるため、複数の検索手法を組み合わせたハイブリッド検索を実装します。

typescript// ハイブリッド検索エンジン
class HybridSearchEngine {
  private vectorSearch: VectorSearchEngine;
  private keywordSearch: KeywordSearchEngine;
  private semanticSearch: SemanticSearchEngine;
  private fusionAlgorithm: ScoreFusionAlgorithm;

  constructor() {
    this.vectorSearch = new VectorSearchEngine();
    this.keywordSearch = new KeywordSearchEngine();
    this.semanticSearch = new SemanticSearchEngine();
    this.fusionAlgorithm = new ScoreFusionAlgorithm();
  }

  // ハイブリッド検索の実行
  async search(
    query: string,
    options: HybridSearchOptions
  ): Promise<SearchResult[]> {
    const searchTasks = [];

    // 1. ベクトル検索
    if (options.enableVector) {
      searchTasks.push(
        this.vectorSearch.search(query, {
          topK: options.topK * 2,
          threshold: options.vectorThreshold,
        })
      );
    }

    // 2. キーワード検索
    if (options.enableKeyword) {
      searchTasks.push(
        this.keywordSearch.search(query, {
          topK: options.topK * 2,
          fuzzy: options.fuzzyMatch,
        })
      );
    }

    // 3. セマンティック検索
    if (options.enableSemantic) {
      searchTasks.push(
        this.semanticSearch.search(query, {
          topK: options.topK * 2,
          contextWindow: options.contextWindow,
        })
      );
    }

    // 並列実行
    const searchResults = await Promise.all(searchTasks);

    // 結果の融合
    const fusedResults = this.fusionAlgorithm.fuse(
      searchResults,
      options.fusionMethod
    );

    // 再ランキング
    if (options.enableReranking) {
      return await this.rerank(
        query,
        fusedResults,
        options.topK
      );
    }

    return fusedResults.slice(0, options.topK);
  }

  // 再ランキング
  private async rerank(
    query: string,
    results: SearchResult[],
    topK: number
  ): Promise<SearchResult[]> {
    const rerankingModel = new RerankingModel();

    const rerankingInput = results.map((result) => ({
      query,
      document: result.content,
      metadata: result.metadata,
    }));

    const rerankingScores = await rerankingModel.score(
      rerankingInput
    );

    // 新しいスコアで並び替え
    const rerankedResults = results.map(
      (result, index) => ({
        ...result,
        score: rerankingScores[index],
        originalScore: result.score,
      })
    );

    return rerankedResults
      .sort((a, b) => b.score - a.score)
      .slice(0, topK);
  }
}

// スコア融合アルゴリズム
class ScoreFusionAlgorithm {
  // RRF(Reciprocal Rank Fusion)アルゴリズム
  fuse(
    searchResults: SearchResult[][],
    method: 'rrf' | 'weighted' | 'max' = 'rrf'
  ): SearchResult[] {
    switch (method) {
      case 'rrf':
        return this.reciprocalRankFusion(searchResults);
      case 'weighted':
        return this.weightedFusion(searchResults);
      case 'max':
        return this.maxScoreFusion(searchResults);
      default:
        return this.reciprocalRankFusion(searchResults);
    }
  }

  // RRF融合
  private reciprocalRankFusion(
    searchResults: SearchResult[][]
  ): SearchResult[] {
    const k = 60; // RRFパラメータ
    const documentScores = new Map<string, number>();
    const documentData = new Map<string, SearchResult>();

    searchResults.forEach((results, searchIndex) => {
      results.forEach((result, rank) => {
        const docId = result.id;
        const rrfScore = 1 / (k + rank + 1);

        if (documentScores.has(docId)) {
          documentScores.set(
            docId,
            documentScores.get(docId)! + rrfScore
          );
        } else {
          documentScores.set(docId, rrfScore);
          documentData.set(docId, result);
        }
      });
    });

    // スコア順でソート
    const sortedResults = Array.from(
      documentScores.entries()
    )
      .sort(([, a], [, b]) => b - a)
      .map(([docId, score]) => ({
        ...documentData.get(docId)!,
        score,
        fusionMethod: 'rrf',
      }));

    return sortedResults;
  }

  // 重み付き融合
  private weightedFusion(
    searchResults: SearchResult[][]
  ): SearchResult[] {
    const weights = [0.6, 0.3, 0.1]; // ベクトル、キーワード、セマンティック
    const documentScores = new Map<string, number>();
    const documentData = new Map<string, SearchResult>();

    searchResults.forEach((results, searchIndex) => {
      const weight = weights[searchIndex] || 0.1;

      results.forEach((result) => {
        const docId = result.id;
        const weightedScore = result.score * weight;

        if (documentScores.has(docId)) {
          documentScores.set(
            docId,
            documentScores.get(docId)! + weightedScore
          );
        } else {
          documentScores.set(docId, weightedScore);
          documentData.set(docId, result);
        }
      });
    });

    return Array.from(documentScores.entries())
      .sort(([, a], [, b]) => b - a)
      .map(([docId, score]) => ({
        ...documentData.get(docId)!,
        score,
        fusionMethod: 'weighted',
      }));
  }
}

検索結果のランキング調整

検索結果の品質を向上させるため、複数の要因を考慮したランキング調整を実装します。

typescript// 高度なランキングシステム
class AdvancedRankingSystem {
  private rankingFactors: RankingFactor[];
  private learningModel: RankingLearningModel;

  constructor() {
    this.rankingFactors = this.initializeRankingFactors();
    this.learningModel = new RankingLearningModel();
  }

  // ランキング要因の初期化
  private initializeRankingFactors(): RankingFactor[] {
    return [
      {
        name: 'semantic_similarity',
        weight: 0.4,
        calculator:
          this.calculateSemanticSimilarity.bind(this),
      },
      {
        name: 'keyword_match',
        weight: 0.25,
        calculator: this.calculateKeywordMatch.bind(this),
      },
      {
        name: 'document_quality',
        weight: 0.15,
        calculator:
          this.calculateDocumentQuality.bind(this),
      },
      {
        name: 'freshness',
        weight: 0.1,
        calculator: this.calculateFreshness.bind(this),
      },
      {
        name: 'user_engagement',
        weight: 0.1,
        calculator: this.calculateUserEngagement.bind(this),
      },
    ];
  }

  // 総合ランキングスコアの計算
  async calculateRankingScore(
    query: string,
    document: SearchResult,
    userContext: UserContext
  ): Promise<number> {
    let totalScore = 0;
    const factorScores: Record<string, number> = {};

    for (const factor of this.rankingFactors) {
      const score = await factor.calculator(
        query,
        document,
        userContext
      );
      factorScores[factor.name] = score;
      totalScore += score * factor.weight;
    }

    // 学習モデルによる調整
    const adjustedScore = await this.learningModel.adjust(
      totalScore,
      factorScores,
      userContext
    );

    return Math.min(Math.max(adjustedScore, 0), 1);
  }

  // セマンティック類似度の計算
  private async calculateSemanticSimilarity(
    query: string,
    document: SearchResult,
    userContext: UserContext
  ): Promise<number> {
    // 既存のベクトル類似度を基準とする
    return document.score || 0;
  }

  // キーワードマッチの計算
  private calculateKeywordMatch(
    query: string,
    document: SearchResult,
    userContext: UserContext
  ): number {
    const queryWords = this.extractKeywords(query);
    const docWords = this.extractKeywords(document.content);

    let matchCount = 0;
    let exactMatchCount = 0;

    for (const queryWord of queryWords) {
      if (docWords.includes(queryWord)) {
        exactMatchCount++;
      } else if (
        docWords.some(
          (docWord) =>
            this.calculateEditDistance(
              queryWord,
              docWord
            ) <= 2
        )
      ) {
        matchCount++;
      }
    }

    const exactMatchScore =
      exactMatchCount / queryWords.length;
    const fuzzyMatchScore =
      (matchCount / queryWords.length) * 0.5;

    return exactMatchScore + fuzzyMatchScore;
  }

  // 文書品質の計算
  private calculateDocumentQuality(
    query: string,
    document: SearchResult,
    userContext: UserContext
  ): number {
    let qualityScore = 0;

    // 文書の長さ(適度な長さが良い)
    const lengthScore = this.calculateLengthScore(
      document.content.length
    );
    qualityScore += lengthScore * 0.3;

    // 構造化度(見出し、リストなどの存在)
    const structureScore = this.calculateStructureScore(
      document.content
    );
    qualityScore += structureScore * 0.3;

    // 読みやすさ
    const readabilityScore = this.calculateReadabilityScore(
      document.content
    );
    qualityScore += readabilityScore * 0.4;

    return qualityScore;
  }

  // 新しさの計算
  private calculateFreshness(
    query: string,
    document: SearchResult,
    userContext: UserContext
  ): number {
    const createdAt = document.metadata.created_at;
    if (!createdAt) return 0.5; // デフォルト値

    const daysSinceCreation =
      (Date.now() - new Date(createdAt).getTime()) /
      (1000 * 60 * 60 * 24);

    // 新しい文書ほど高スコア(1年で半減)
    return Math.exp(-daysSinceCreation / 365);
  }

  // ユーザーエンゲージメントの計算
  private calculateUserEngagement(
    query: string,
    document: SearchResult,
    userContext: UserContext
  ): number {
    const engagement =
      document.metadata.user_engagement || {};

    const viewCount = engagement.view_count || 0;
    const likeCount = engagement.like_count || 0;
    const shareCount = engagement.share_count || 0;

    // 正規化されたエンゲージメントスコア
    const viewScore = Math.min(viewCount / 1000, 1) * 0.5;
    const likeScore = Math.min(likeCount / 100, 1) * 0.3;
    const shareScore = Math.min(shareCount / 50, 1) * 0.2;

    return viewScore + likeScore + shareScore;
  }

  // ユーティリティメソッド
  private extractKeywords(text: string): string[] {
    return text
      .toLowerCase()
      .replace(
        /[^\w\s\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/g,
        ' '
      )
      .split(/\s+/)
      .filter((word) => word.length > 2);
  }

  private calculateEditDistance(
    str1: string,
    str2: string
  ): number {
    const dp = Array(str1.length + 1)
      .fill(null)
      .map(() => Array(str2.length + 1).fill(0));

    for (let i = 0; i <= str1.length; i++) dp[i][0] = i;
    for (let j = 0; j <= str2.length; j++) dp[0][j] = j;

    for (let i = 1; i <= str1.length; i++) {
      for (let j = 1; j <= str2.length; j++) {
        if (str1[i - 1] === str2[j - 1]) {
          dp[i][j] = dp[i - 1][j - 1];
        } else {
          dp[i][j] = Math.min(
            dp[i - 1][j] + 1,
            dp[i][j - 1] + 1,
            dp[i - 1][j - 1] + 1
          );
        }
      }
    }

    return dp[str1.length][str2.length];
  }

  private calculateLengthScore(length: number): number {
    // 500-2000文字が最適
    if (length < 100) return 0.2;
    if (length < 500) return 0.6;
    if (length <= 2000) return 1.0;
    if (length <= 5000) return 0.8;
    return 0.4;
  }

  private calculateStructureScore(content: string): number {
    let score = 0;

    // 見出しの存在
    if (content.match(/^#+\s/m)) score += 0.3;

    // リストの存在
    if (
      content.match(/^[\-\*\+]\s/m) ||
      content.match(/^\d+\.\s/m)
    )
      score += 0.3;

    // 段落の適切な分割
    const paragraphs = content.split('\n\n');
    if (paragraphs.length > 1) score += 0.4;

    return Math.min(score, 1);
  }

  private calculateReadabilityScore(
    content: string
  ): number {
    const sentences = content
      .split(/[。!?\.\!\?]+/)
      .filter((s) => s.trim());
    const words = content.split(/\s+/);

    if (sentences.length === 0) return 0;

    const avgWordsPerSentence =
      words.length / sentences.length;

    // 適度な文章長が読みやすい
    if (avgWordsPerSentence < 10) return 0.6;
    if (avgWordsPerSentence <= 25) return 1.0;
    if (avgWordsPerSentence <= 40) return 0.8;
    return 0.4;
  }
}

コンテキストウィンドウの管理

効果的なコンテキスト管理により、関連性の高い情報を適切な量で提供します。

typescript// コンテキストウィンドウマネージャー
class ContextWindowManager {
  private maxTokens: number;
  private reservedTokens: number;
  private compressionRatio: number;

  constructor(
    maxTokens: number = 4000,
    reservedTokens: number = 1000,
    compressionRatio: number = 0.7
  ) {
    this.maxTokens = maxTokens;
    this.reservedTokens = reservedTokens;
    this.compressionRatio = compressionRatio;
  }

  // コンテキストの最適化
  async optimizeContext(
    query: string,
    documents: SearchResult[]
  ): Promise<OptimizedContext> {
    const availableTokens =
      this.maxTokens - this.reservedTokens;

    // 1. 文書の重要度計算
    const scoredDocuments = await this.scoreDocuments(
      query,
      documents
    );

    // 2. トークン予算の配分
    const tokenAllocation = this.allocateTokens(
      scoredDocuments,
      availableTokens
    );

    // 3. 文書の圧縮と選択
    const optimizedDocuments = await this.compressDocuments(
      scoredDocuments,
      tokenAllocation
    );

    // 4. コンテキストの構造化
    const structuredContext = this.structureContext(
      optimizedDocuments
    );

    return {
      context: structuredContext,
      totalTokens: this.estimateTokens(structuredContext),
      documentCount: optimizedDocuments.length,
      compressionApplied: true,
    };
  }

  // 文書の重要度スコア計算
  private async scoreDocuments(
    query: string,
    documents: SearchResult[]
  ): Promise<ScoredDocument[]> {
    return documents.map((doc, index) => ({
      ...doc,
      importanceScore: this.calculateImportanceScore(
        doc,
        index,
        query
      ),
      originalLength: doc.content.length,
    }));
  }

  // 重要度スコアの計算
  private calculateImportanceScore(
    document: SearchResult,
    position: number,
    query: string
  ): number {
    // 検索ランキング(位置による重み)
    const positionScore = 1 / (1 + position * 0.1);

    // 類似度スコア
    const similarityScore = document.score || 0;

    // クエリとの直接マッチ
    const directMatchScore = this.calculateDirectMatch(
      query,
      document.content
    );

    // 文書の完全性
    const completenessScore = this.calculateCompleteness(
      document.content
    );

    return (
      positionScore * 0.3 +
      similarityScore * 0.4 +
      directMatchScore * 0.2 +
      completenessScore * 0.1
    );
  }

  // トークン予算の配分
  private allocateTokens(
    documents: ScoredDocument[],
    availableTokens: number
  ): TokenAllocation[] {
    const totalImportance = documents.reduce(
      (sum, doc) => sum + doc.importanceScore,
      0
    );

    return documents.map((doc) => ({
      documentId: doc.id,
      allocatedTokens: Math.floor(
        (doc.importanceScore / totalImportance) *
          availableTokens
      ),
      importanceScore: doc.importanceScore,
    }));
  }

  // 文書の圧縮
  private async compressDocuments(
    documents: ScoredDocument[],
    allocations: TokenAllocation[]
  ): Promise<CompressedDocument[]> {
    const compressed: CompressedDocument[] = [];

    for (let i = 0; i < documents.length; i++) {
      const doc = documents[i];
      const allocation = allocations[i];

      if (allocation.allocatedTokens < 50) {
        continue; // 最小トークン数以下はスキップ
      }

      const compressedContent = await this.compressContent(
        doc.content,
        allocation.allocatedTokens
      );

      compressed.push({
        ...doc,
        content: compressedContent,
        originalLength: doc.originalLength,
        compressedLength: compressedContent.length,
        compressionRatio:
          compressedContent.length / doc.originalLength,
      });
    }

    return compressed;
  }

  // コンテンツの圧縮
  private async compressContent(
    content: string,
    targetTokens: number
  ): Promise<string> {
    const estimatedTokens = this.estimateTokens(content);

    if (estimatedTokens <= targetTokens) {
      return content; // 圧縮不要
    }

    // 1. 重要文の抽出
    const sentences = this.extractSentences(content);
    const importantSentences =
      this.selectImportantSentences(
        sentences,
        targetTokens
      );

    // 2. 要約生成(必要に応じて)
    if (
      importantSentences.join(' ').length >
      targetTokens * 4
    ) {
      return await this.generateSummary(
        content,
        targetTokens
      );
    }

    return importantSentences.join(' ');
  }

  // 重要文の選択
  private selectImportantSentences(
    sentences: string[],
    targetTokens: number
  ): string[] {
    const scoredSentences = sentences.map((sentence) => ({
      sentence,
      score: this.calculateSentenceImportance(sentence),
      tokens: this.estimateTokens(sentence),
    }));

    // スコア順でソート
    scoredSentences.sort((a, b) => b.score - a.score);

    const selected: string[] = [];
    let totalTokens = 0;

    for (const item of scoredSentences) {
      if (totalTokens + item.tokens <= targetTokens) {
        selected.push(item.sentence);
        totalTokens += item.tokens;
      }
    }

    return selected;
  }

  // 文の重要度計算
  private calculateSentenceImportance(
    sentence: string
  ): number {
    let score = 0;

    // 長さによるスコア(適度な長さが重要)
    const length = sentence.length;
    if (length > 20 && length < 200) score += 0.3;

    // キーワードの存在
    const keywords = [
      '方法',
      '手順',
      '重要',
      '必要',
      'ポイント',
      '注意',
    ];
    const keywordCount = keywords.filter((keyword) =>
      sentence.includes(keyword)
    ).length;
    score += keywordCount * 0.2;

    // 数値や具体例の存在
    if (sentence.match(/\d+/)) score += 0.1;
    if (
      sentence.includes('例えば') ||
      sentence.includes('具体的に')
    )
      score += 0.2;

    return score;
  }

  // トークン数の推定
  private estimateTokens(text: string): number {
    // 簡易的な推定(実際にはtiktokenなどを使用)
    return Math.ceil(text.length / 4);
  }

  // 文の抽出
  private extractSentences(content: string): string[] {
    return content
      .split(/[。!?\.\!\?]+/)
      .map((s) => s.trim())
      .filter((s) => s.length > 10);
  }

  // 要約生成
  private async generateSummary(
    content: string,
    targetTokens: number
  ): Promise<string> {
    // LLMを使用した要約生成の実装
    const summaryPrompt = `
以下の文書を${targetTokens * 4}文字程度で要約してください。
重要なポイントと具体的な情報を含めてください。

文書:
${content}

要約:
`;

    try {
      const llmClient = new LLMClient();
      const summary = await llmClient.complete(
        summaryPrompt,
        {
          max_tokens: targetTokens,
          temperature: 0.3,
        }
      );

      return summary.trim();
    } catch (error) {
      console.error('要約生成エラー:', error);
      // フォールバック:重要文の抽出
      return this.selectImportantSentences(
        this.extractSentences(content),
        targetTokens
      ).join(' ');
    }
  }

  // コンテキストの構造化
  private structureContext(
    documents: CompressedDocument[]
  ): string {
    return documents
      .map((doc, index) => {
        const header = `## 参考文書 ${index + 1}`;
        const metadata = doc.metadata.title
          ? `**タイトル**: ${doc.metadata.title}`
          : '';
        const content = doc.content;

        return [header, metadata, content]
          .filter(Boolean)
          .join('\n');
      })
      .join('\n\n---\n\n');
  }

  // 直接マッチの計算
  private calculateDirectMatch(
    query: string,
    content: string
  ): number {
    const queryWords = query.toLowerCase().split(/\s+/);
    const contentLower = content.toLowerCase();

    const matches = queryWords.filter((word) =>
      contentLower.includes(word)
    ).length;

    return matches / queryWords.length;
  }

  // 完全性の計算
  private calculateCompleteness(content: string): number {
    let score = 0;

    // 構造化要素の存在
    if (
      content.includes('手順') ||
      content.includes('方法')
    )
      score += 0.3;
    if (content.match(/\d+\./)) score += 0.2; // 番号付きリスト
    if (
      content.includes('例') ||
      content.includes('具体的')
    )
      score += 0.2;
    if (content.length > 500) score += 0.3; // 十分な情報量

    return Math.min(score, 1);
  }
}

検索フィルタリングの活用

メタデータを活用した効果的なフィルタリングシステムを実装します。

typescript// 高度なフィルタリングシステム
class AdvancedFilteringSystem {
  private filterProcessors: Map<string, FilterProcessor>;
  private dynamicFilters: DynamicFilterGenerator;

  constructor() {
    this.filterProcessors = new Map();
    this.initializeFilterProcessors();
    this.dynamicFilters = new DynamicFilterGenerator();
  }

  // フィルタープロセッサーの初期化
  private initializeFilterProcessors(): void {
    this.filterProcessors.set(
      'category',
      new CategoryFilter()
    );
    this.filterProcessors.set(
      'date',
      new DateRangeFilter()
    );
    this.filterProcessors.set('author', new AuthorFilter());
    this.filterProcessors.set(
      'language',
      new LanguageFilter()
    );
    this.filterProcessors.set(
      'difficulty',
      new DifficultyFilter()
    );
    this.filterProcessors.set(
      'content_type',
      new ContentTypeFilter()
    );
  }

  // 検索フィルターの適用
  async applyFilters(
    query: string,
    documents: SearchResult[],
    userContext: UserContext
  ): Promise<FilteredSearchResult> {
    // 1. クエリからフィルター条件を抽出
    const extractedFilters =
      await this.extractFiltersFromQuery(query);

    // 2. ユーザーコンテキストからフィルターを推定
    const contextualFilters =
      this.generateContextualFilters(userContext);

    // 3. 動的フィルターの生成
    const dynamicFilters =
      await this.dynamicFilters.generate(
        query,
        documents,
        userContext
      );

    // 4. フィルターの統合
    const combinedFilters = this.combineFilters([
      extractedFilters,
      contextualFilters,
      dynamicFilters,
    ]);

    // 5. フィルターの適用
    const filteredDocuments = await this.executeFilters(
      documents,
      combinedFilters
    );

    return {
      documents: filteredDocuments,
      appliedFilters: combinedFilters,
      originalCount: documents.length,
      filteredCount: filteredDocuments.length,
      filteringRatio:
        filteredDocuments.length / documents.length,
    };
  }

  // クエリからフィルター条件を抽出
  private async extractFiltersFromQuery(
    query: string
  ): Promise<FilterCondition[]> {
    const filters: FilterCondition[] = [];

    // 日付関連の抽出
    const datePatterns = [
      /(\d{4})年以降/g,
      /最近(\d+)日/g,
      /(\d{4})年(\d{1,2})月/g,
    ];

    for (const pattern of datePatterns) {
      const matches = query.match(pattern);
      if (matches) {
        filters.push({
          type: 'date',
          operator: 'gte',
          value: this.parseDateFromMatch(matches[0]),
        });
      }
    }

    // カテゴリ関連の抽出
    const categoryKeywords = {
      tutorial: ['チュートリアル', '入門', '基礎'],
      reference: ['リファレンス', '仕様', 'API'],
      troubleshooting: ['トラブル', 'エラー', '問題'],
      best_practices: [
        'ベストプラクティス',
        '推奨',
        'コツ',
      ],
    };

    for (const [category, keywords] of Object.entries(
      categoryKeywords
    )) {
      if (
        keywords.some((keyword) => query.includes(keyword))
      ) {
        filters.push({
          type: 'category',
          operator: 'eq',
          value: category,
        });
      }
    }

    // 難易度関連の抽出
    const difficultyKeywords = {
      beginner: ['初心者', '入門', '基本'],
      intermediate: ['中級', '応用'],
      advanced: ['上級', '高度', '専門'],
    };

    for (const [level, keywords] of Object.entries(
      difficultyKeywords
    )) {
      if (
        keywords.some((keyword) => query.includes(keyword))
      ) {
        filters.push({
          type: 'difficulty',
          operator: 'eq',
          value: level,
        });
      }
    }

    return filters;
  }

  // コンテキストベースのフィルター生成
  private generateContextualFilters(
    userContext: UserContext
  ): FilterCondition[] {
    const filters: FilterCondition[] = [];

    // ユーザーの言語設定
    if (userContext.preferredLanguage) {
      filters.push({
        type: 'language',
        operator: 'eq',
        value: userContext.preferredLanguage,
      });
    }

    // ユーザーのスキルレベル
    if (userContext.skillLevel) {
      filters.push({
        type: 'difficulty',
        operator: 'lte',
        value: userContext.skillLevel,
      });
    }

    // 最近のアクセス履歴に基づく
    if (
      userContext.recentCategories &&
      userContext.recentCategories.length > 0
    ) {
      filters.push({
        type: 'category',
        operator: 'in',
        value: userContext.recentCategories,
      });
    }

    return filters;
  }

  // フィルターの統合
  private combineFilters(
    filterGroups: FilterCondition[][]
  ): FilterCondition[] {
    const combined: FilterCondition[] = [];
    const filterMap = new Map<string, FilterCondition[]>();

    // フィルタータイプごとにグループ化
    for (const group of filterGroups) {
      for (const filter of group) {
        if (!filterMap.has(filter.type)) {
          filterMap.set(filter.type, []);
        }
        filterMap.get(filter.type)!.push(filter);
      }
    }

    // 同じタイプのフィルターを統合
    for (const [type, filters] of filterMap) {
      const processor = this.filterProcessors.get(type);
      if (processor) {
        const mergedFilter = processor.merge(filters);
        if (mergedFilter) {
          combined.push(mergedFilter);
        }
      }
    }

    return combined;
  }

  // フィルターの実行
  private async executeFilters(
    documents: SearchResult[],
    filters: FilterCondition[]
  ): Promise<SearchResult[]> {
    let filteredDocuments = [...documents];

    for (const filter of filters) {
      const processor = this.filterProcessors.get(
        filter.type
      );
      if (processor) {
        filteredDocuments = await processor.apply(
          filteredDocuments,
          filter
        );
      }
    }

    return filteredDocuments;
  }

  // 日付のパース
  private parseDateFromMatch(match: string): Date {
    // 簡単な日付パース実装
    const yearMatch = match.match(/(\d{4})/);
    if (yearMatch) {
      return new Date(parseInt(yearMatch[1]), 0, 1);
    }

    const daysMatch = match.match(/(\d+)日/);
    if (daysMatch) {
      const days = parseInt(daysMatch[1]);
      const date = new Date();
      date.setDate(date.getDate() - days);
      return date;
    }

    return new Date();
  }
}

// カテゴリフィルタープロセッサー
class CategoryFilter implements FilterProcessor {
  async apply(
    documents: SearchResult[],
    filter: FilterCondition
  ): Promise<SearchResult[]> {
    return documents.filter((doc) => {
      const category = doc.metadata.category;

      switch (filter.operator) {
        case 'eq':
          return category === filter.value;
        case 'in':
          return (
            Array.isArray(filter.value) &&
            filter.value.includes(category)
          );
        case 'ne':
          return category !== filter.value;
        default:
          return true;
      }
    });
  }

  merge(
    filters: FilterCondition[]
  ): FilterCondition | null {
    if (filters.length === 0) return null;

    // OR条件で統合
    const values = filters.map((f) => f.value).flat();
    return {
      type: 'category',
      operator: 'in',
      value: [...new Set(values)],
    };
  }
}

// 日付範囲フィルタープロセッサー
class DateRangeFilter implements FilterProcessor {
  async apply(
    documents: SearchResult[],
    filter: FilterCondition
  ): Promise<SearchResult[]> {
    return documents.filter((doc) => {
      const docDate = new Date(
        doc.metadata.created_at || doc.metadata.updated_at
      );
      const filterDate = new Date(filter.value);

      switch (filter.operator) {
        case 'gte':
          return docDate >= filterDate;
        case 'lte':
          return docDate <= filterDate;
        case 'eq':
          return (
            docDate.toDateString() ===
            filterDate.toDateString()
          );
        default:
          return true;
      }
    });
  }

  merge(
    filters: FilterCondition[]
  ): FilterCondition | null {
    if (filters.length === 0) return null;

    // 日付範囲の統合(最も制限的な条件を採用)
    let minDate: Date | null = null;
    let maxDate: Date | null = null;

    for (const filter of filters) {
      const date = new Date(filter.value);

      if (filter.operator === 'gte') {
        minDate = minDate
          ? new Date(
              Math.max(minDate.getTime(), date.getTime())
            )
          : date;
      } else if (filter.operator === 'lte') {
        maxDate = maxDate
          ? new Date(
              Math.min(maxDate.getTime(), date.getTime())
            )
          : date;
      }
    }

    // 最も制限的な条件を返す
    if (minDate && maxDate) {
      return minDate > maxDate
        ? { type: 'date', operator: 'gte', value: minDate }
        : {
            type: 'date',
            operator: 'between',
            value: [minDate, maxDate],
          };
    }

    return minDate
      ? { type: 'date', operator: 'gte', value: minDate }
      : maxDate
      ? { type: 'date', operator: 'lte', value: maxDate }
      : null;
  }
}

実践的なコード実装例

TypeScript での RAG 実装

実際のプロダクション環境で使用できる、完全な RAG システムの実装例をご紹介します。

typescript// メインのRAGシステムクラス
export class ProductionRAGSystem {
  private difyClient: DifyAPIClient;
  private vectorDB: VectorDatabaseManager;
  private embeddingGenerator: EmbeddingGenerator;
  private queryOptimizer: QueryOptimizer;
  private contextManager: ContextWindowManager;
  private rankingSystem: AdvancedRankingSystem;
  private filteringSystem: AdvancedFilteringSystem;
  private config: RAGConfig;

  constructor(config: RAGConfig) {
    this.config = config;
    this.initializeComponents();
  }

  // コンポーネントの初期化
  private initializeComponents(): void {
    this.difyClient = new DifyAPIClient({
      apiKey: this.config.dify.apiKey,
      baseUrl: this.config.dify.baseUrl,
    });

    this.vectorDB = new VectorDatabaseManager({
      provider: this.config.vectorDB.provider,
      connectionConfig: this.config.vectorDB.connection,
      indexConfig: this.config.vectorDB.index,
    });

    this.embeddingGenerator = new EmbeddingGenerator(
      this.config.embedding.model
    );

    this.queryOptimizer = new QueryOptimizer();
    this.contextManager = new ContextWindowManager(
      this.config.context.maxTokens,
      this.config.context.reservedTokens
    );

    this.rankingSystem = new AdvancedRankingSystem();
    this.filteringSystem = new AdvancedFilteringSystem();
  }

  // RAGクエリの実行
  async query(
    userQuery: string,
    userContext?: UserContext,
    options?: QueryOptions
  ): Promise<RAGResponse> {
    try {
      const startTime = Date.now();

      // 1. クエリの最適化
      const optimizedQuery =
        await this.queryOptimizer.optimizeQuery(userQuery);

      // 2. ベクトル検索の実行
      const searchResults = await this.performSearch(
        optimizedQuery,
        userContext,
        options
      );

      // 3. 結果のフィルタリング
      const filteredResults =
        await this.filteringSystem.applyFilters(
          userQuery,
          searchResults,
          userContext || {}
        );

      // 4. ランキングの調整
      const rankedResults = await this.rerank(
        userQuery,
        filteredResults.documents,
        userContext
      );

      // 5. コンテキストの最適化
      const optimizedContext =
        await this.contextManager.optimizeContext(
          userQuery,
          rankedResults
        );

      // 6. 回答の生成
      const response = await this.generateResponse(
        userQuery,
        optimizedContext,
        userContext
      );

      // 7. 応答時間の記録
      const responseTime = Date.now() - startTime;

      return {
        answer: response.answer,
        sources: rankedResults.slice(0, 5),
        metadata: {
          query: userQuery,
          optimizedQueries: optimizedQuery.multiple,
          searchResultCount: searchResults.length,
          filteredResultCount:
            filteredResults.filteredCount,
          contextTokens: optimizedContext.totalTokens,
          responseTime,
          confidence: response.confidence,
        },
        debug: this.config.debug
          ? {
              appliedFilters:
                filteredResults.appliedFilters,
              rankingFactors: response.rankingFactors,
              contextOptimization: optimizedContext,
            }
          : undefined,
      };
    } catch (error) {
      console.error('RAGクエリ実行エラー:', error);
      throw new RAGError(
        `クエリ実行に失敗しました: ${error.message}`,
        error
      );
    }
  }

  // 文書の追加
  async addDocuments(documents: Document[]): Promise<void> {
    const chunker = new AdvancedChunker();
    const indexBuilder = new VectorIndexBuilder(
      this.embeddingGenerator,
      this.vectorDB
    );

    for (const document of documents) {
      try {
        // 1. 文書の前処理
        const preprocessor = new DataPreprocessor();
        const processedDoc =
          await preprocessor.processDocument(document);

        // 2. チャンク分割
        const chunks = await chunker.chunkDocument(
          processedDoc
        );

        // 3. ベクトル化とインデックス追加
        await indexBuilder.buildIndex(chunks);

        console.log(
          `文書追加完了: ${document.metadata.title}`
        );
      } catch (error) {
        console.error(
          `文書追加エラー: ${document.metadata.title}`,
          error
        );
        throw error;
      }
    }
  }
}

API 統合とデータ連携

外部システムとの統合を考慮した実装例です。

typescript// Express.js との統合例
export class ExpressRAGServer {
  private app: express.Application;
  private ragSystem: ProductionRAGSystem;

  constructor(ragSystem: ProductionRAGSystem) {
    this.app = express();
    this.ragSystem = ragSystem;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json({ limit: '10mb' }));
    this.app.use(cors());
    this.app.use(helmet());
  }

  private setupRoutes(): void {
    // クエリエンドポイント
    this.app.post('/api/v1/query', async (req, res) => {
      try {
        const { query, userId, language, maxResults } =
          req.body;

        const response = await this.ragSystem.query(
          query,
          { userId, preferredLanguage: language },
          { topK: maxResults || 10 }
        );

        res.json(response);
      } catch (error) {
        res.status(500).json({ error: error.message });
      }
    });

    // ヘルスチェックエンドポイント
    this.app.get('/health', async (req, res) => {
      try {
        const health = await this.ragSystem.healthCheck();
        res
          .status(health.status === 'healthy' ? 200 : 503)
          .json(health);
      } catch (error) {
        res.status(503).json({
          status: 'unhealthy',
          error: error.message,
        });
      }
    });
  }

  start(port: number = 3000): void {
    this.app.listen(port, () => {
      console.log(
        `RAG API サーバーがポート ${port} で起動しました`
      );
    });
  }
}

エラーハンドリングとフォールバック

本格的な運用に必要なエラーハンドリングとフォールバック機能の実装です。

typescript// 包括的なエラーハンドリングシステム
export class ErrorHandlingSystem {
  private fallbackStrategies: Map<string, FallbackStrategy>;
  private circuitBreaker: CircuitBreaker;

  constructor() {
    this.fallbackStrategies = new Map();
    this.initializeFallbackStrategies();
    this.circuitBreaker = new CircuitBreaker({
      failureThreshold: 5,
      timeout: 30000,
      resetTimeout: 60000,
    });
  }

  // フォールバック戦略の初期化
  private initializeFallbackStrategies(): void {
    // ベクトル検索失敗時のフォールバック
    this.fallbackStrategies.set('vector_search_failure', {
      name: 'キーワード検索フォールバック',
      execute: async (context) => {
        return await this.performKeywordSearch(
          context.query,
          context.options
        );
      },
      priority: 1,
    });

    // LLM生成失敗時のフォールバック
    this.fallbackStrategies.set('llm_generation_failure', {
      name: 'テンプレート回答フォールバック',
      execute: async (context) => {
        return this.generateTemplateResponse(
          context.query,
          context.documents
        );
      },
      priority: 2,
    });
  }

  // エラーハンドリングの実行
  async handleError(
    error: Error,
    context: ErrorContext
  ): Promise<ErrorHandlingResult> {
    const errorType = this.classifyError(error);
    const strategy = this.selectStrategy(
      errorType,
      context
    );

    try {
      // サーキットブレーカーのチェック
      if (this.circuitBreaker.isOpen()) {
        return await this.executeEmergencyFallback(context);
      }

      const result = await strategy.execute(context);
      this.circuitBreaker.recordSuccess();

      return {
        success: true,
        result,
        strategy: strategy.name,
      };
    } catch (fallbackError) {
      this.circuitBreaker.recordFailure();
      return await this.executeEmergencyFallback(context);
    }
  }

  // 基本回答の生成
  private generateBasicResponse(query: string): string {
    return `
申し訳ございませんが、現在システムに一時的な問題が発生しており、
ご質問「${query}」にお答えできません。

システムの復旧をお待ちいただくか、以下の方法をお試しください:
- しばらく時間をおいて再度お試しください
- より具体的な質問に変更してみてください
- サポートチームにお問い合わせください

ご不便をおかけして申し訳ございません。
`;
  }
}

これらの実装例を参考に、堅牢で実用的な RAG システムを構築することができます。

まとめ

本記事では、Dify プラットフォームで RAG(Retrieval-Augmented Generation)システムを実装する包括的な方法について詳しく解説いたしました。

重要なポイントの振り返り

RAG システム構築において最も重要な要素は以下の通りです:

#重要要素推奨アプローチ期待効果
1データ品質構造化された前処理とチャンク分割検索精度の向上
2埋め込みモデル選択用途に応じた適切なモデル選択コストと精度のバランス
3ハイブリッド検索ベクトル・キーワード・セマンティック検索の組み合わせ検索の網羅性向上
4コンテキスト管理動的な最適化と圧縮応答品質の向上
5エラーハンドリング多層的なフォールバック戦略システムの安定性確保

実装時の注意点

実際に RAG システムを構築する際は、以下の点に特に注意を払うことが重要です:

段階的な実装:すべての機能を一度に実装するのではなく、基本機能から始めて段階的に高度な機能を追加していくことをお勧めします。

継続的な改善:ユーザーフィードバックや性能メトリクスを基に、継続的にシステムを改善していくことが成功の鍵となります。

コスト管理:埋め込み生成や LLM 呼び出しにはコストが発生するため、適切な最適化と監視が必要です。

今後の発展

RAG 技術は急速に進歩しており、以下のような発展が期待されます:

  • マルチモーダル対応:テキストだけでなく、画像や音声を含む検索と生成
  • リアルタイム学習:ユーザーとの対話から継続的に学習するシステム
  • 専門ドメイン特化:特定分野に特化した高精度な RAG システム

Dify プラットフォームはこれらの技術進歩に対応しており、今後もより高度な RAG システムの構築が可能になると予想されます。

本記事で紹介した実装方法を参考に、ぜひ効果的な RAG システムの構築にチャレンジしてみてください。適切に実装された RAG システムは、ユーザーに正確で有用な情報を提供し、大きな価値を生み出すことができるでしょう。

関連リンク