LLM推論ベースのツリーインデックスRAG「PageIndex」でPDF/Markdownから階層構造を抽出する

初めに

github.com

PageIndexは、ベクトルDBやチャンキングを使わず、LLMの推論によって階層ツリーインデックスを構築するRAGシステムです。PDF/Markdownから目次のようなツリー構造を自動生成し、ツリー検索で関連ページを特定します。

従来のRAGはベクトル類似度検索に依存しますが、「類似度≠関連度」という問題があります。PageIndexはLLMの推論能力を活用し、人間の専門家がドキュメントをナビゲートするように関連箇所を特定します。

PageIndexの処理は大きく2ステップに分かれます。

  1. ツリーインデックス構築 — ドキュメントから階層ツリーを生成する
  2. ツリー検索 — 構築済みツリーをたどって関連ページを特定する

このリポジトリは①のツリーインデックス構築を担当します。本記事ではPDFとMarkdownそれぞれの構築方法を紹介します。

開発環境

環境構築

リポジトリをクローンします。

git clone https://github.com/VectifyAI/PageIndex.git
cd PageIndex

依存関係をインストールします。

uv add -r requirements.txt

.envファイルを作成してOpenAI APIキーを設定します。

CHATGPT_API_KEY=your_openai_key_here

PDFの処理

PDF処理では、LLMを複数段階で活用してツリーを構築します。

  1. PDFからテキストを抽出
  2. LLMで目次(TOC)ページを検出
  3. TOCJSON構造に変換(LLM)
  4. 各セクションにページ番号を割当(LLM)
  5. 割当結果を検証・修正(LLM)
  6. 各ノードのサマリを非同期並列生成(LLM)

各ノードは以下のような構造を持ちます。

{
  "title": "INTRODUCTION",
  "node_id": "0000",
  "start_index": 1,
  "end_index": 2,
  "summary": "This section introduces...",
  "nodes": []
}

start_index/end_indexがページ範囲、nodesが子ノードの配列です。

サンプルPDF(earthmover.pdf)でツリー構造を生成します。

python3 run_pageindex.py --pdf_path tests/pdfs/earthmover.pdf

results/earthmover_structure.jsonが生成されます。可視化スクリプトで確認すると以下のようなツリーが得られます。

[doc] earthmover.pdf  (19 nodes)

[0000] INTRODUCTION (p.1-2) [S]
[0001] PRELIMINARIES (p.2) [S]
├── [0002] Computing the EMD (p.3) [S]
└── [0003] Filter-and-Refinement Framework (p.3-4) [S]
[0004] SCALING UP SSP (p.4-5) [S]
[0005] BOOSTING THE REFINEMENT PHASE (p.5) [S]
├── [0006] Analysis of EMD Calculation (p.5-8) [S]
├── [0007] Progressive Bounding (p.8-6) [S]
├── [0008] Sensitivity to Refinement Order (p.6-9) [S]
├── [0009] Dynamic Refinement Ordering (p.9-7) [S]
└── [0010] Running Upper Bound (p.7-8) [S]
[0011] EXPERIMENTAL EVALUATION (p.8) [S]
├── [0012] Performance Improvement (p.8-10) [S]
├── [0013] Scalability Experiments (p.10-11) [S]
└── [0014] Parameter Tuning in DRO (p.11-12) [S]
[0015] RELATED WORK (p.12) [S]
[0016] CONCLUSION (p.12) [S]
[0017] ACKNOWLEDGMENT (p.12) [S]
[0018] REFERENCES (p.12) [S]

12ページのPDFから19ノードのツリーが構築され、各ノードにページ範囲とサマリ([S])が付与されています。

Markdownの処理

Markdownファイルでもツリー構造を生成できます。PDFとは異なり、ヘッダ階層(#, ##, ###...)から直接ツリーを構築するため、TOC検出やページ割当のステップは不要です。LLMはサマリ生成時のみ使用されます。

python3 run_pageindex.py --md_path tests/md/japanese_sample.md

Markdownの場合はヘッダ階層(#, ##, ###...)からツリーを構築します。

[doc] japanese_sample  (25 nodes)

[0000] Webアプリケーション開発ガイド (L1) [S]
├── [0001] 第1章 フロントエンド開発 (L5) [S]
│   ├── [0002] 1.1 HTML/CSSの基礎 (L9) [S]
│   └── [0003] 1.2 JavaScriptフレームワーク (L22) [S]
│       ├── [0004] 1.2.1 React (L26) [S]
│       ├── [0005] 1.2.2 Vue.js (L38) [S]
│       └── [0006] 1.2.3 パフォーマンス最適化 (L42) [S]
├── [0007] 第2章 バックエンド開発 (L51) [S]
│   ├── [0008] 2.1 API設計 (L55) [S]
│   │   ├── [0009] 2.1.1 RESTful API (L59) [S]
│   │   └── [0010] 2.1.2 GraphQL (L69) [S]
│   ├── [0011] 2.2 データベース設計 (L85) [S]
│   │   ├── [0012] 2.2.1 リレーショナルデータベース (L89) [S]
│   │   └── [0013] 2.2.2 NoSQLデータベース (L93) [S]
│   └── [0014] 2.3 認証・認可 (L97) [S]
├── [0015] 第3章 インフラストラクチャ (L106) [S]
│   ├── [0016] 3.1 コンテナ技術 (L110) [S]
│   │   ├── [0017] 3.1.1 Docker (L114) [S]
│   │   └── [0018] 3.1.2 Kubernetes (L128) [S]
│   ├── [0019] 3.2 CI/CDパイプライン (L132) [S]
│   └── [0020] 3.3 監視とオブザーバビリティ (L151) [S]
└── [0021] 第4章 開発プラクティス (L160) [S]
    ├── [0022] 4.1 テスト戦略 (L164) [S]
    ├── [0023] 4.2 コードレビュー (L174) [S]
    └── [0024] 4.3 アジャイル開発 (L185) [S]

Markdownのヘッダ構造がそのままツリーに反映され、25ノードが生成されています。PDFと異なりページ番号ではなく行番号(L)で位置が示されます。

主なオプション

run_pageindex.pyにはいくつかのオプションがあります。

  • --if-add-node-summary yes/no — 各ノードにLLM生成のサマリを付与(デフォルト: yes)
  • --if-add-doc-description yes/no — ドキュメント全体の説明文を生成(デフォルト: no)
  • --if-thinning yes/noMarkdown用。トークン数の少ないノードを親に統合してツリーを簡略化(デフォルト: no)
  • --model — 使用するOpenAIモデル(デフォルト: gpt-4o-2024-11-20)
  • --max-pages-per-node — PDF用。1ノードあたりの最大ページ数(デフォルト: 10)
  • --max-tokens-per-node — PDF用。1ノードあたりの最大トークン数(デフォルト: 20000)