T-CREATOR

LlamaIndex とは?2025 年版 RAG フレームワークの全体像と強みを徹底解説

LlamaIndex とは?2025 年版 RAG フレームワークの全体像と強みを徹底解説

AI を活用したアプリケーション開発において、データの検索と取得は非常に重要な要素です。特に RAG(Retrieval-Augmented Generation)技術を用いる場面では、膨大なドキュメントから正確に必要な情報を取り出し、LLM へ適切に提供する必要があります。

LlamaIndex は、このデータの検索と取得に特化したオープンソースのフレームワークとして、2025 年現在、多くの企業や開発者に支持されています。本記事では、LlamaIndex の全体像や最新機能、他のフレームワークとの違いについて、初心者にもわかりやすく解説していきますね。

背景

RAG 技術の重要性

LLM は学習データに基づいて応答を生成しますが、最新の情報や企業固有のデータには対応できません。この課題を解決するのが RAG(Retrieval-Augmented Generation)技術です。

RAG は、外部のデータソースから関連する情報を検索・取得し、それを LLM のコンテキストとして渡すことで、より正確で最新の回答を生成できるようにします。この仕組みにより、企業の内部文書や専門的な知識ベースを活用した AI アプリケーションの構築が可能になりました。

データ検索における課題

しかし、RAG を実装する際には多くの技術的な課題があります。

まず、多様なデータソース(PDF、データベース、API など)からデータを統一的に扱う必要があります。次に、大量のドキュメントを効率的にインデックス化し、検索可能な形式に変換しなければなりません。そして、ユーザーのクエリに対して最も関連性の高い情報を素早く取得する仕組みも必要です。

これらの課題を一つひとつ実装するのは、非常に時間がかかりますし、専門的な知識も求められます。

LlamaIndex の登場

こうした背景から、データの検索と取得に特化したフレームワークとして LlamaIndex が誕生しました。LlamaIndex は、データの取り込みからインデックス化、検索、そして LLM への統合まで、RAG に必要な機能を包括的に提供してくれます。

2025 年現在、LlamaIndex は月間 400 万回以上ダウンロードされ、1,500 人以上の貢献者、20,000 人以上のコミュニティメンバーを持つ、オープンソースプロジェクトとして成長しています。

以下の図は、RAG システムにおける LlamaIndex の位置づけを示したものです。

mermaidflowchart LR
    datasource["データソース<br/>(PDF/DB/API)"] -->|データ取り込み| llamaindex["LlamaIndex<br/>フレームワーク"]
    llamaindex -->|インデックス化| vectorstore[("ベクトル<br/>ストア")]
    user["ユーザー"] -->|クエリ| llamaindex
    llamaindex -->|関連情報検索| vectorstore
    vectorstore -->|検索結果| llamaindex
    llamaindex -->|コンテキスト付与| llm["LLM<br/>(GPT-4等)"]
    llm -->|回答生成| user

図で理解できる要点:

  • LlamaIndex はデータソースと LLM の間に位置し、データの橋渡し役となります
  • データの取り込み、インデックス化、検索という一連の流れを統合的に管理します
  • ベクトルストアを活用した高速な情報検索を実現しています

課題

従来の RAG 実装における問題点

RAG システムを自力で構築しようとすると、いくつかの大きな課題に直面します。

データ取り込みの複雑さ

企業で扱うデータは、PDF、Word 文書、Excel、データベース、Web API など、さまざまな形式で存在しています。これらのデータを統一的に扱うには、それぞれの形式に対応したパーサーやコネクタを実装する必要があり、開発工数が大幅に増加してしまいます。

特に PDF ドキュメントでは、テーブルや画像が含まれていたり、複数カラムのレイアウトが使われていたりするため、単純な OCR では正確に情報を抽出できないケースも多いですね。

検索精度の限界

単純なキーワード検索やベクトル検索だけでは、ユーザーの意図を正確に汲み取れないことがあります。例えば、「最新の売上レポート」というクエリに対して、日付情報を考慮せずに類似度だけで検索すると、古い文書が上位に表示されてしまう可能性があります。

また、複数のドキュメントにまたがる情報を統合する必要がある場合や、文書の階層構造を考慮した検索が必要な場合など、高度な検索ロジックが求められるシーンも増えています。

スケーラビリティの問題

データ量が増加するにつれて、インデックス作成や検索のパフォーマンスが低下してしまうことがあります。数千、数万のドキュメントを扱う場合、効率的なデータ構造とアルゴリズムの選択が不可欠です。

さらに、本番環境での運用を考えると、可観測性(observability)やモニタリング機能も必要になりますが、これらを一から実装するのは容易ではありません。

以下の図は、従来の RAG 実装で直面する主な課題を示しています。

mermaidflowchart TD
    rag["RAG システム<br/>実装の課題"]
    rag --> challenge1["データ取り込み<br/>の複雑さ"]
    rag --> challenge2["検索精度<br/>の限界"]
    rag --> challenge3["スケーラビリティ<br/>の問題"]

    challenge1 --> c1_1["多様な形式<br/>(PDF/DB/API)"]
    challenge1 --> c1_2["複雑な文書構造<br/>(表/画像/レイアウト)"]

    challenge2 --> c2_1["単純な類似度検索<br/>の不十分さ"]
    challenge2 --> c2_2["複数文書の<br/>統合困難"]

    challenge3 --> c3_1["大量データでの<br/>パフォーマンス低下"]
    challenge3 --> c3_2["運用監視機能<br/>の不足"]

図で理解できる要点:

  • RAG システム実装には、データ取り込み、検索精度、スケーラビリティという 3 つの大きな課題があります
  • それぞれの課題には複数の技術的な問題が含まれており、包括的な解決策が必要です
  • これらの課題を個別に解決するには、多大な開発リソースと専門知識が求められます

解決策

LlamaIndex が提供する包括的なソリューション

LlamaIndex は、前述した RAG 実装の課題を解決するために、包括的な機能セットを提供しています。2025 年版の LlamaIndex では、さらに進化した機能が追加され、本番環境での利用にも十分対応できるフレームワークとなりました。

コアアーキテクチャの理解

LlamaIndex のアーキテクチャは、いくつかの主要なコンポーネントで構成されています。これらのコンポーネントが連携することで、効率的な RAG システムを実現しているのです。

以下の表は、LlamaIndex の主要コンポーネントをまとめたものです。

#コンポーネント名役割主な機能
1Data Connectorsデータ取り込み多様なデータソースからの統一的なデータ読み込み
2Documentドキュメント管理取り込んだデータの構造化表現
3Nodeデータチャンクドキュメントを分割した処理単位
4Indexインデックス化データを検索可能な形式で保存
5Retriever情報取得クエリに基づく関連情報の検索
6Query Engineクエリ処理検索と回答生成の統合制御
7Chat Engine会話管理会話履歴を考慮した対話型インターフェース

データ取り込みとパース機能

LlamaIndex は、100 種類以上のデータソースに対応した Data Connectors を提供しています。PDF、Word、Excel などのファイル形式はもちろん、SharePoint、Google Drive、Notion、各種データベースなど、エンタープライズ環境でよく使われるデータソースにも対応しています。

特に注目すべきは、2025 年版で大幅に強化された LlamaParse という GenAI ネイティブのドキュメントパーシング機能です。LlamaParse は、従来の OCR ベースのパーシングを超えて、LLM を活用した高精度な文書解析を実現します。

typescriptimport { LlamaParse } from 'llamaparse';

// LlamaParse の初期化
const parser = new LlamaParse({
  apiKey: process.env.LLAMA_CLOUD_API_KEY,
  // パーシングモードの指定
  parsingMode: 'parse_with_llm',
  // システムプロンプトで出力形式を指定
  systemPrompt:
    'テーブル構造を保持してマークダウン形式で出力してください',
});

上記のコードは、LlamaParse を初期化する基本的な設定です。parsingMode で AI を使ったパーシング方法を選択し、systemPrompt で出力形式を細かく制御できます。

typescript// ドキュメントのパース実行
const documents = await parser.loadData(
  './financial_report.pdf'
);

// パース結果の確認
console.log(
  `パース完了: ${documents.length} 件のドキュメント`
);
documents.forEach((doc, index) => {
  console.log(`Document ${index + 1}:`);
  console.log(`  - テキスト長: ${doc.text.length} 文字`);
  console.log(
    `  - メタデータ: ${JSON.stringify(doc.metadata)}`
  );
});

パース実行のコードでは、PDF ファイルを読み込んで構造化されたドキュメントオブジェクトに変換しています。パース結果にはテキスト内容だけでなく、メタデータも含まれるため、後の検索処理で活用できますね。

LlamaParse の 2025 年版での主な改善点は以下の通りです。

#機能説明リリース時期
1新しいパーシングモードParse without AI、Parse with LLM、Parse with Agent の 3 モード2025 年 2 月
2スキュー検出回転した文書(90°、180°、270°)の自動検出と補正2025 年 5 月
3強化されたプロンプトシステムシステムプロンプトとユーザープロンプトの分離による柔軟な制御2025 年 2 月
4マルチファイル対応PDF、PPTX、DOCX、XLSX、HTML、JPEG など多様な形式のサポート継続的に拡張
5テーブル抽出の精度向上複数ページにわたるテーブルの自動結合2025 年 5 月

インデックス構造の選択

LlamaIndex は、用途に応じて複数のインデックスタイプを提供しています。最も一般的に使われるのは VectorStoreIndex ですが、それ以外にも用途に応じた選択肢があります。

typescriptimport { VectorStoreIndex, Document } from 'llamaindex';

// ドキュメントの準備
const documents = [
  new Document({
    text: 'LlamaIndex は RAG に特化したフレームワークです。',
    metadata: { source: 'doc1' },
  }),
  new Document({
    text: '2025 年版では検索精度が 35% 向上しました。',
    metadata: { source: 'doc2' },
  }),
  new Document({
    text: 'エンタープライズ向けの機能が充実しています。',
    metadata: { source: 'doc3' },
  }),
];

まず、インデックス化するドキュメントを準備します。各ドキュメントはテキスト内容とメタデータを持ち、後の検索で活用できるようにしています。

typescript// VectorStoreIndex の作成
const index = await VectorStoreIndex.fromDocuments(
  documents
);

// インデックスの保存(永続化)
await index.storageContext.persist('./storage');
console.log('インデックスを保存しました');

VectorStoreIndex.fromDocuments() メソッドで、ドキュメントからベクトルインデックスを作成します。作成したインデックスは persist() メソッドで保存でき、次回起動時に再利用できますね。

typescript// 保存したインデックスの読み込み
import { storageContextFromDefaults } from 'llamaindex';

const storageContext = await storageContextFromDefaults({
  persistDir: './storage',
});

const loadedIndex = await VectorStoreIndex.init({
  storageContext: storageContext,
});

console.log('インデックスを読み込みました');

保存したインデックスを読み込む際は、storageContextFromDefaults() でストレージコンテキストを作成し、そこから VectorStoreIndex を初期化します。これにより、毎回インデックスを再構築する必要がなくなります。

以下の表は、LlamaIndex で利用可能な主なインデックスタイプをまとめたものです。

#インデックスタイプ適用シーン特徴
1VectorStoreIndex一般的なセマンティック検索埋め込みベクトルによる類似度検索、最も汎用的
2SummaryIndex全ドキュメントの要約すべてのノードを LLM に渡して包括的な回答を生成
3TreeIndex階層的な情報検索ツリー構造で段階的に情報を絞り込み
4KeywordTableIndexキーワードベースの検索特定のキーワードに基づく高速検索
5KnowledgeGraphIndexエンティティ間の関係検索知識グラフを構築して関係性を考慮した検索

高度な検索機能

2025 年版の LlamaIndex では、単純なベクトル検索を超えた、エージェント型の検索機能が標準となっています。これにより、検索精度が 35% 向上したと報告されています。

typescript// クエリエンジンの作成(基本設定)
const queryEngine = index.asQueryEngine({
  similarityTopK: 5, // 上位 5 件を取得
});

// シンプルなクエリの実行
const response = await queryEngine.query({
  query:
    'LlamaIndex の 2025 年版の新機能について教えてください',
});

console.log('回答:', response.toString());

クエリエンジンは、インデックスから情報を検索し、LLM を使って回答を生成する役割を担います。similarityTopK パラメータで、類似度上位何件を取得するかを指定できます。

typescript// ハイブリッド検索の設定(ベクトル検索 + BM25)
import { RetrieverQueryEngine } from 'llamaindex';

const hybridQueryEngine = index.asQueryEngine({
  retriever: new HybridRetriever({
    vectorRetriever: index.asRetriever({
      similarityTopK: 5,
    }),
    bm25Retriever: new BM25Retriever({ topK: 5 }),
    // ベクトル検索と BM25 検索の重み付け
    alpha: 0.7, // ベクトル検索の重み(0.0 〜 1.0)
  }),
});

ハイブリッド検索では、セマンティック検索(ベクトル検索)とキーワード検索(BM25)を組み合わせることで、より高精度な検索結果を得られます。alpha パラメータで両者のバランスを調整できますね。

typescript// リランキングの適用
import { CohereRerank } from 'llamaindex';

const rerankQueryEngine = index.asQueryEngine({
  retriever: index.asRetriever({ similarityTopK: 10 }),
  // 検索結果の上位 3 件にリランキングを適用
  nodePostprocessors: [
    new CohereRerank({
      apiKey: process.env.COHERE_API_KEY,
      topN: 3,
    }),
  ],
});

リランキングは、初期検索で得られた結果をさらに精緻化する技術です。Cohere などの専用モデルを使用することで、検索精度をさらに向上させることができます。

以下の図は、LlamaIndex の高度な検索フローを示したものです。

mermaidflowchart TD
    query["ユーザークエリ"] --> retriever["Retriever<br/>(情報取得)"]
    retriever --> vector["ベクトル検索<br/>(セマンティック)"]
    retriever --> bm25["BM25 検索<br/>(キーワード)"]

    vector --> merge["検索結果<br/>マージ"]
    bm25 --> merge

    merge --> rerank["リランキング<br/>(Cohere/LLM)"]
    rerank --> postprocess["ポストプロセス<br/>(フィルタリング)"]

    postprocess --> synthesizer["Response Synthesizer<br/>(回答生成)"]
    synthesizer --> llm["LLM<br/>(GPT-4等)"]
    llm --> answer["最終回答"]

図で理解できる要点:

  • 検索は単一の手法ではなく、複数の手法を組み合わせて行われます
  • ベクトル検索とキーワード検索の結果をマージし、リランキングで精緻化します
  • 最終的に Response Synthesizer が LLM を使って自然な回答を生成します

Workflows による複雑な処理の制御

2025 年 6 月に正式リリースされた Workflows 1.0 は、複雑なマルチステップの AI ワークフローを構築するための軽量フレームワークです。従来の線形的な処理フローでは実現できなかった、ループや並列処理、人間によるレビューステップなどを組み込めます。

typescriptimport {
  Workflow,
  StartEvent,
  StopEvent,
} from '@llamaindex/workflow';

// カスタムイベントの定義
class DocumentProcessedEvent extends Event {
  constructor(
    public documentId: string,
    public content: string
  ) {
    super();
  }
}

Workflows では、イベント駆動型のアーキテクチャを採用しています。まず、ワークフロー内で使用するカスタムイベントを定義します。

typescript// ワークフローの定義
class DocumentProcessingWorkflow extends Workflow {
  @step()
  async loadDocument(event: StartEvent) {
    console.log('ドキュメントを読み込んでいます...');
    const content = await this.loadFromSource(
      event.data.source
    );

    // 次のステップへイベントを送信
    return new DocumentProcessedEvent(
      event.data.id,
      content
    );
  }

  @step()
  async parseDocument(event: DocumentProcessedEvent) {
    console.log(
      `ドキュメント ${event.documentId} をパース中...`
    );
    const parsedContent = await this.parse(event.content);

    return {
      documentId: event.documentId,
      parsed: parsedContent,
    };
  }

  @step()
  async indexDocument(event: {
    documentId: string;
    parsed: any;
  }) {
    console.log(
      `ドキュメント ${event.documentId} をインデックス化中...`
    );
    await this.addToIndex(event.parsed);

    // ワークフロー終了
    return new StopEvent({
      success: true,
      documentId: event.documentId,
    });
  }
}

ワークフローは複数のステップ(@step() デコレータで定義)で構成されます。各ステップはイベントを受け取り、処理を行い、次のイベントを返します。この仕組みにより、柔軟で保守性の高いワークフローを構築できますね。

typescript// ワークフローの実行
const workflow = new DocumentProcessingWorkflow();

const result = await workflow.run({
  source: './documents/report.pdf',
  id: 'doc-001',
});

console.log('処理結果:', result);

ワークフローの実行は非常にシンプルです。run() メソッドに初期データを渡すだけで、定義した一連のステップが自動的に実行されます。

Workflows の主な特徴を以下の表にまとめました。

#特徴説明メリット
1イベント駆動型ステップ間をイベントで接続柔軟な処理フローの構築が可能
2完全非同期すべての処理が非同期で実行高速なパフォーマンスを実現
3型安全TypeScript の型システムを活用開発時のエラーを削減
4一時停止・再開ワークフローの途中で一時停止可能人間によるレビューステップの組み込み
5並列処理対応複数のステップを同時実行処理時間の短縮

エンタープライズ向け機能

2025 年版の LlamaIndex は、本番環境での利用を前提とした機能が充実しています。

typescriptimport {
  CallbackManager,
  LlamaDebugHandler,
} from 'llamaindex';

// デバッグハンドラーの設定
const debugHandler = new LlamaDebugHandler();

const callbackManager = new CallbackManager({
  onLLMStart: (event) => {
    console.log('LLM 呼び出し開始:', event.prompt);
  },
  onLLMEnd: (event) => {
    console.log('LLM 呼び出し終了:', event.response);
    console.log('トークン数:', event.tokenUsage);
  },
  onRetrievalStart: (event) => {
    console.log('検索開始:', event.query);
  },
  onRetrievalEnd: (event) => {
    console.log(
      '検索終了:',
      event.nodes.length,
      '件のノードを取得'
    );
  },
});

コールバックマネージャーを使用することで、LLM の呼び出しや検索処理の詳細をモニタリングできます。これは本番環境でのデバッグやパフォーマンス分析に非常に有効です。

typescript// OpenTelemetry との統合
import { OpenTelemetryInstrumentation } from 'llamaindex';

OpenTelemetryInstrumentation.instrument({
  serviceName: 'llamaindex-rag-service',
  endpoint: 'http://localhost:4318/v1/traces',
});

// これ以降の LlamaIndex の処理は自動的にトレースされます
const queryEngine = index.asQueryEngine();
const response = await queryEngine.query({ query: '...' });

OpenTelemetry との統合により、分散トレーシングやメトリクス収集が可能になります。これにより、本番環境でのパフォーマンス監視やボトルネックの特定が容易になりますね。

以下の図は、LlamaIndex のエンタープライズ機能の全体像を示しています。

mermaidflowchart TD
    app["LlamaIndex<br/>アプリケーション"]

    app --> obs["可観測性<br/>(Observability)"]
    app --> security["セキュリティ"]
    app --> scale["スケーラビリティ"]

    obs --> callback["Callback Manager<br/>(ログ/メトリクス)"]
    obs --> telemetry["OpenTelemetry<br/>(分散トレーシング)"]
    obs --> debug["Debug Handler<br/>(デバッグ情報)"]

    security --> auth["認証/認可"]
    security --> encrypt["データ暗号化"]
    security --> audit["監査ログ"]

    scale --> cache["キャッシング"]
    scale --> batch["バッチ処理"]
    scale --> distributed["分散インデックス"]

図で理解できる要点:

  • エンタープライズ機能は、可観測性、セキュリティ、スケーラビリティの 3 つの柱で構成されます
  • 各カテゴリには複数の具体的な機能が含まれ、本番環境での運用を支援します
  • これらの機能を組み合わせることで、信頼性の高い RAG システムを構築できます

LangChain との比較と使い分け

LlamaIndex と並んでよく比較されるのが LangChain です。両者は補完的な関係にあり、用途に応じて使い分けることが重要です。

以下の表は、2025 年版における LlamaIndex と LangChain の比較をまとめたものです。

#比較項目LlamaIndexLangChain
1主な焦点データ検索と取得ワークフロー全般の制御
2得意分野ドキュメント中心のアプリケーションマルチステップの AI パイプライン
3検索精度2025 年版で 35% 向上標準的な検索機能
4データパースLlamaParse による高精度パース基本的なローダーを提供
5インデックスタイプ5 種類以上の専門的なインデックスベクトルストア中心
6エージェント機能検索に特化したエージェント汎用的なエージェントフレームワーク
7学習曲線RAG に集中、比較的シンプル多機能で初期学習コストが高い
8適用例社内ナレッジベース、法務文書検索カスタマーサポート自動化、複雑なワークフロー

使い分けの指針

LlamaIndex を選ぶべきケース:

  • ドキュメント検索と取得が中心的な要件である場合
  • 高精度なドキュメントパースが必要な場合(PDF、表、画像を含む複雑な文書)
  • 社内ナレッジベース、技術文書、法務文書などの検索システム
  • RAG に特化したシンプルな実装を求める場合

LangChain を選ぶべきケース:

  • 複数のツールや API を連携させた複雑なワークフローが必要な場合
  • エージェントが自律的に判断して行動するシステム
  • マルチモーダル(動画、音声、画像など)なデータソースを扱う場合
  • 既存のツールやサービスとの幅広い統合が必要な場合

ハイブリッドアプローチ:

2025 年現在、多くのエンタープライズシステムでは、LlamaIndex と LangChain を組み合わせたハイブリッドアプローチが採用されています。例えば、データの検索と取得は LlamaIndex で行い、その結果を使った複雑なワークフローの制御は LangChain で行うといった使い方ですね。

typescript// ハイブリッドアプローチの例
import { VectorStoreIndex } from 'llamaindex';
import { LLMChain } from 'langchain';

// LlamaIndex で高精度な検索
const index = await VectorStoreIndex.fromDocuments(
  documents
);
const retriever = index.asRetriever({ similarityTopK: 5 });
const relevantDocs = await retriever.retrieve(
  'ユーザーの質問'
);

// LangChain で複雑なワークフロー制御
const chain = new LLMChain({
  llm: new OpenAI({ temperature: 0.7 }),
  prompt: customPromptTemplate,
});

const result = await chain.call({
  context: relevantDocs.map((doc) => doc.text).join('\n'),
  question: 'ユーザーの質問',
});

このコードは、LlamaIndex の検索能力と LangChain のワークフロー制御能力を組み合わせた例です。それぞれのフレームワークの強みを活かすことで、より高度なシステムを構築できます。

具体例

実践的な RAG アプリケーションの構築

ここでは、LlamaIndex を使って実際に動作する RAG アプリケーションを構築する手順を、段階的に解説していきます。今回は、企業の技術文書を検索できる社内ナレッジベースシステムを例として取り上げますね。

プロジェクトのセットアップ

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

bash# プロジェクトディレクトリの作成
mkdir llamaindex-knowledge-base
cd llamaindex-knowledge-base

# package.json の初期化
yarn init -y

プロジェクトディレクトリを作成し、Yarn でパッケージ管理を初期化します。

bash# LlamaIndex 関連パッケージのインストール
yarn add llamaindex

# OpenAI API の利用(LLM として使用)
yarn add openai

# 環境変数管理
yarn add dotenv

# TypeScript 開発環境
yarn add -D typescript @types/node tsx

必要なパッケージをインストールします。llamaindex がメインのフレームワークで、openai は LLM として GPT-4 を使用するために必要です。

bash# TypeScript の設定ファイル作成
npx tsc --init

TypeScript の設定ファイル(tsconfig.json)を生成します。

環境変数の設定も行います。

bash# .env ファイルの作成
cat > .env << EOF
OPENAI_API_KEY=your-openai-api-key-here
LLAMA_CLOUD_API_KEY=your-llamacloud-api-key-here
EOF

API キーを .env ファイルに保存することで、コード内に直接記述せずに安全に管理できます。実際の開発では、.env ファイルを .gitignore に追加して、バージョン管理から除外することを忘れないでくださいね。

ドキュメントの準備とパース

次に、検索対象となるドキュメントを準備し、LlamaParse でパースします。

typescript// src/parsers/documentParser.ts
import 'dotenv/config';
import { LlamaParse } from 'llamaparse';
import { SimpleDirectoryReader } from 'llamaindex';
import * as fs from 'fs/promises';

/**
 * ドキュメントをパースするクラス
 */
export class DocumentParser {
  private parser: LlamaParse;

  constructor() {
    // LlamaParse の初期化
    this.parser = new LlamaParse({
      apiKey: process.env.LLAMA_CLOUD_API_KEY!,
      parsingMode: 'parse_with_llm', // LLM を使った高精度パース
      resultType: 'markdown', // マークダウン形式で出力
      systemPrompt: `
        以下のルールに従ってドキュメントをパースしてください:
        - テーブル構造を保持する
        - 見出し階層を維持する
        - コードブロックはマークダウン形式で出力する
        - 画像の説明文を含める
      `,
    });
  }
}

DocumentParser クラスを定義し、LlamaParse を初期化します。systemPrompt でパースのルールを細かく指定することで、出力形式を制御できます。

typescript  /**
   * 指定ディレクトリ内のドキュメントをパース
   */
  async parseDocuments(directoryPath: string) {
    console.log(`ディレクトリ ${directoryPath} からドキュメントを読み込んでいます...`);

    // ディレクトリ内のファイル一覧を取得
    const files = await fs.readdir(directoryPath);
    const pdfFiles = files.filter(file => file.endsWith(".pdf"));

    console.log(`${pdfFiles.length} 件の PDF ファイルを発見しました`);

    const allDocuments = [];

    // 各 PDF ファイルをパース
    for (const file of pdfFiles) {
      const filePath = `${directoryPath}/${file}`;
      console.log(`パース中: ${file}`);

      try {
        const documents = await this.parser.loadData(filePath);

        // メタデータを追加
        documents.forEach(doc => {
          doc.metadata = {
            ...doc.metadata,
            fileName: file,
            parsedAt: new Date().toISOString(),
          };
        });

        allDocuments.push(...documents);
        console.log(`  ✓ 完了: ${documents.length} 件のドキュメントを抽出`);
      } catch (error) {
        console.error(`  ✗ エラー: ${file} のパースに失敗`, error);
      }
    }

    return allDocuments;
  }

parseDocuments() メソッドは、指定されたディレクトリ内のすべての PDF ファイルをパースします。各ドキュメントにファイル名とパース日時のメタデータを追加することで、後の検索で情報源を特定しやすくしていますね。

インデックスの作成と保存

パースしたドキュメントからインデックスを作成します。

typescript// src/indexing/indexBuilder.ts
import {
  VectorStoreIndex,
  Document,
  Settings,
} from 'llamaindex';
import { OpenAI, OpenAIEmbedding } from 'llamaindex';

/**
 * インデックスを作成するクラス
 */
export class IndexBuilder {
  constructor() {
    // LLM と埋め込みモデルの設定
    Settings.llm = new OpenAI({
      model: 'gpt-4',
      temperature: 0.1, // 低めに設定して一貫性を重視
    });

    Settings.embedModel = new OpenAIEmbedding({
      model: 'text-embedding-3-large',
      dimensions: 1024, // 埋め込みベクトルの次元数
    });
  }
}

Settings で LLM と埋め込みモデルをグローバルに設定します。temperature を低く設定することで、回答の一貫性を高めています。

typescript  /**
   * ドキュメントからインデックスを作成
   */
  async buildIndex(documents: Document[]) {
    console.log(`${documents.length} 件のドキュメントからインデックスを作成中...`);

    // VectorStoreIndex の作成
    const index = await VectorStoreIndex.fromDocuments(documents, {
      // チャンクサイズの設定
      chunkSize: 512, // 1 チャンクあたり 512 トークン
      chunkOverlap: 128, // チャンク間のオーバーラップ
    });

    console.log("インデックスの作成が完了しました");
    return index;
  }

VectorStoreIndex.fromDocuments() でドキュメントをベクトルインデックスに変換します。chunkSizechunkOverlap を調整することで、検索精度とパフォーマンスのバランスを取ることができますね。

typescript  /**
   * インデックスを永続化
   */
  async saveIndex(index: VectorStoreIndex, persistDir: string = "./storage") {
    console.log(`インデックスを ${persistDir} に保存中...`);
    await index.storageContext.persist(persistDir);
    console.log("インデックスの保存が完了しました");
  }

  /**
   * 保存したインデックスを読み込み
   */
  async loadIndex(persistDir: string = "./storage") {
    console.log(`インデックスを ${persistDir} から読み込み中...`);
    const { storageContextFromDefaults } = await import("llamaindex");

    const storageContext = await storageContextFromDefaults({
      persistDir,
    });

    const index = await VectorStoreIndex.init({ storageContext });
    console.log("インデックスの読み込みが完了しました");
    return index;
  }

インデックスの保存と読み込み機能を実装しています。一度インデックスを作成すれば、次回からは読み込むだけで済むため、起動時間を大幅に短縮できます。

検索機能の実装

作成したインデックスを使って検索機能を実装します。

typescript// src/querying/queryEngine.ts
import {
  VectorStoreIndex,
  RetrieverQueryEngine,
} from 'llamaindex';

/**
 * クエリエンジンを管理するクラス
 */
export class QueryEngineManager {
  private index: VectorStoreIndex;

  constructor(index: VectorStoreIndex) {
    this.index = index;
  }
}

QueryEngineManager クラスで、検索機能を一元管理します。

typescript  /**
   * 基本的なクエリエンジンを作成
   */
  createBasicQueryEngine() {
    return this.index.asQueryEngine({
      similarityTopK: 5, // 類似度上位 5 件を取得
      // レスポンスモードの設定
      responseMode: "compact", // コンパクトな回答を生成
    });
  }

基本的なクエリエンジンを作成します。similarityTopK で検索件数を、responseMode で回答の詳細度を制御できます。

typescript  /**
   * 高度なクエリエンジン(リランキング付き)を作成
   */
  createAdvancedQueryEngine() {
    // Retriever の作成(多めに取得)
    const retriever = this.index.asRetriever({
      similarityTopK: 10,
    });

    // リランキング用のポストプロセッサを設定
    return this.index.asQueryEngine({
      retriever,
      // メタデータフィルタリング
      filters: {
        // 例: 特定のファイルのみを検索対象にする
        // fileName: "technical_guide.pdf"
      },
    });
  }

高度なクエリエンジンでは、より多くの候補を取得してから絞り込むことで、検索精度を向上させています。フィルタ機能を使えば、特定の条件に合致するドキュメントのみを検索対象にできますね。

typescript  /**
   * クエリを実行
   */
  async query(queryEngine: any, question: string) {
    console.log(`\n質問: ${question}`);
    console.log("回答を生成中...\n");

    const startTime = Date.now();
    const response = await queryEngine.query({ query: question });
    const endTime = Date.now();

    console.log(`回答: ${response.toString()}`);
    console.log(`\n処理時間: ${endTime - startTime}ms`);

    // 検索元のドキュメント情報を表示
    if (response.sourceNodes) {
      console.log("\n参照元:");
      response.sourceNodes.forEach((node: any, index: number) => {
        console.log(`  ${index + 1}. ${node.metadata.fileName} (スコア: ${node.score?.toFixed(3)})`);
      });
    }

    return response;
  }

query() メソッドは、質問を受け取って回答を生成します。処理時間や参照元ドキュメントも表示することで、システムの透明性を高めています。

メインプログラムの実装

すべてのコンポーネントを統合したメインプログラムを作成します。

typescript// src/main.ts
import 'dotenv/config';
import { DocumentParser } from './parsers/documentParser';
import { IndexBuilder } from './indexing/indexBuilder';
import { QueryEngineManager } from './querying/queryEngine';
import * as fs from 'fs/promises';

/**
 * メインプログラム
 */
async function main() {
  console.log(
    '=== LlamaIndex ナレッジベースシステム ===\n'
  );

  const STORAGE_DIR = './storage';
  const DOCUMENTS_DIR = './documents';

  // ストレージディレクトリが存在するか確認
  const storageExists = await fs
    .access(STORAGE_DIR)
    .then(() => true)
    .catch(() => false);

  let index;

  if (storageExists) {
    // 既存のインデックスを読み込み
    console.log('既存のインデックスを読み込みます...\n');
    const indexBuilder = new IndexBuilder();
    index = await indexBuilder.loadIndex(STORAGE_DIR);
  } else {
    // 新規にインデックスを作成
    console.log('新規にインデックスを作成します...\n');

    // ステップ 1: ドキュメントのパース
    const parser = new DocumentParser();
    const documents = await parser.parseDocuments(
      DOCUMENTS_DIR
    );

    if (documents.length === 0) {
      console.error(
        'エラー: パース可能なドキュメントが見つかりませんでした'
      );
      return;
    }

    // ステップ 2: インデックスの作成
    const indexBuilder = new IndexBuilder();
    index = await indexBuilder.buildIndex(documents);

    // ステップ 3: インデックスの保存
    await indexBuilder.saveIndex(index, STORAGE_DIR);
  }
}

メインプログラムでは、既存のインデックスがあれば読み込み、なければ新規作成するという処理フローを実装しています。

typescript  // ステップ 4: クエリエンジンの作成
  console.log("\nクエリエンジンを初期化中...\n");
  const queryManager = new QueryEngineManager(index);
  const queryEngine = queryManager.createAdvancedQueryEngine();

  // ステップ 5: 検索の実行
  const questions = [
    "LlamaIndex の主な機能は何ですか?",
    "RAG システムを構築する際の注意点を教えてください",
    "ベクトルインデックスとキーワードインデックスの違いは?",
  ];

  for (const question of questions) {
    await queryManager.query(queryEngine, question);
    console.log("\n" + "=".repeat(80) + "\n");
  }

  console.log("処理が完了しました");
}

// エラーハンドリング付きで実行
main().catch(error => {
  console.error("エラーが発生しました:", error);
  process.exit(1);
});

最後に、複数の質問を順番に実行して、システムの動作を確認します。エラーハンドリングも適切に行うことで、本番環境でも安定して動作するコードになっていますね。

実行方法

作成したプログラムを実行します。

bash# ドキュメントディレクトリの作成
mkdir documents

# サンプル PDF ファイルを配置
# (実際の PDF ファイルを documents/ ディレクトリに配置してください)

# プログラムの実行
yarn tsx src/main.ts

初回実行時はドキュメントのパースとインデックス作成が行われるため、時間がかかります。2 回目以降は保存されたインデックスを読み込むだけなので、起動が高速になります。

以下の図は、構築した RAG アプリケーションの全体的なデータフローを示しています。

mermaidflowchart TD
    docs["PDF ドキュメント"] -->|読み込み| parser["LlamaParse<br/>(ドキュメントパース)"]
    parser -->|構造化テキスト| chunks["チャンク分割<br/>(512 トークン単位)"]
    chunks -->|テキストチャンク| embed["埋め込み生成<br/>(OpenAI Embedding)"]
    embed -->|ベクトル| vectordb[("ベクトル<br/>インデックス")]

    user["ユーザー"] -->|質問| query["Query Engine"]
    query -->|検索クエリ| vectordb
    vectordb -->|上位 10 件| rerank["リランキング"]
    rerank -->|上位 3 件| synthesis["Response Synthesis"]
    synthesis -->|コンテキスト付き| gpt["GPT-4"]
    gpt -->|回答| user

図で理解できる要点:

  • ドキュメントは LlamaParse でパースされ、チャンク単位に分割されます
  • 各チャンクは埋め込みベクトルに変換され、ベクトルインデックスに格納されます
  • ユーザーの質問は検索クエリに変換され、関連する情報を取得します
  • 取得した情報はリランキングで精緻化され、GPT-4 に渡されて最終的な回答が生成されます

Chat Engine を使った会話型インターフェース

単発の質問応答だけでなく、会話の履歴を保持した対話型のインターフェースも構築できます。

typescript// src/chat/chatEngine.ts
import { VectorStoreIndex } from 'llamaindex';

/**
 * チャットエンジンを管理するクラス
 */
export class ChatEngineManager {
  private index: VectorStoreIndex;

  constructor(index: VectorStoreIndex) {
    this.index = index;
  }

  /**
   * チャットエンジンを作成
   */
  createChatEngine() {
    return this.index.asChatEngine({
      chatMode: 'condense_plus_context', // 会話履歴を考慮したモード
      verbose: true, // 詳細ログを出力
    });
  }
}

Chat Engine は、会話の履歴を保持し、文脈を理解した応答を生成できます。chatModecondense_plus_context に設定することで、過去の会話を要約しつつ、新しい質問に答えるモードになります。

typescript  /**
   * 会話を実行
   */
  async chat(chatEngine: any, message: string) {
    console.log(`\nユーザー: ${message}`);

    const response = await chatEngine.chat({ message });

    console.log(`アシスタント: ${response.toString()}`);
    return response;
  }

chat() メソッドは、メッセージを送信して応答を受け取ります。会話履歴は自動的に管理されるため、開発者が明示的に管理する必要はありません。

typescript// 使用例
async function chatExample(index: VectorStoreIndex) {
  const chatManager = new ChatEngineManager(index);
  const chatEngine = chatManager.createChatEngine();

  // 会話の開始
  await chatManager.chat(
    chatEngine,
    'LlamaIndex について教えてください'
  );

  // 前の回答を踏まえた質問(「それ」は LlamaIndex を指す)
  await chatManager.chat(
    chatEngine,
    'それの主な機能は何ですか?'
  );

  // さらに会話を続ける
  await chatManager.chat(
    chatEngine,
    '具体的な使用例を教えてください'
  );
}

この例では、3 つの質問が相互に関連しています。Chat Engine は会話の文脈を理解しているため、「それ」が何を指すのかを正しく認識して回答を生成できますね。

まとめ

LlamaIndex は、RAG(Retrieval-Augmented Generation)に特化したオープンソースのフレームワークとして、2025 年現在、多くの開発者や企業に採用されています。本記事で解説した内容を振り返ってみましょう。

LlamaIndex の主要な特徴

LlamaIndex は、データの検索と取得に関するすべての機能を包括的に提供しています。データの取り込みから、インデックス化、高精度な検索、そして LLM への統合まで、一貫したインターフェースで実装できることが大きな強みです。

2025 年版では、検索精度が 35% 向上し、LlamaParse による高精度なドキュメントパース機能、Workflows によるマルチステップ処理、エンタープライズ向けの可観測性機能などが追加され、本番環境での利用にも十分対応できるフレームワークへと進化しました。

適用シーンと推奨事項

LlamaIndex は、以下のようなシーンで特に力を発揮します。

  • 社内ナレッジベースや技術文書の検索システム
  • 法務文書やコンプライアンス関連の情報検索
  • カスタマーサポートにおける FAQ 検索
  • 研究論文や学術文書のセマンティック検索

一方で、複雑なワークフローの制御や、多様なツールの統合が必要な場合は、LangChain との併用も検討すると良いでしょう。それぞれのフレームワークの強みを活かしたハイブリッドアプローチが、2025 年のベストプラクティスとなっています。

今後の展望

LlamaIndex は、オープンソースコミュニティの活発な貢献により、今後もさらなる進化が期待されます。特に、マルチモーダル対応の強化、エージェント機能の充実、そしてエンタープライズ向けの機能拡張が進んでいくことでしょう。

RAG 技術は、AI アプリケーション開発において不可欠な要素となっており、LlamaIndex はその中心的なツールとしての地位を確立しています。本記事で紹介した内容を参考に、ぜひ実際のプロジェクトで LlamaIndex を活用してみてくださいね。

関連リンク