非構造データに効くというBookRAG

非構造データに効くというBookRAG

こんにちはmakokonです。
RAGが提案されて以来、永遠の課題というのは、今まで業務で蓄えた知識が構造化されていないということでしょう。
個のBookRAGも、この問題への取り組みです。このテーマでどれだけ論文が書かれたかわかりませんが、これも、この課題が深刻かつ是が非でも解決したい課題だからでしょう。
この方法、普通に効果がありそうな気がするので、少し丁寧に見ていきたいと思います。記事中日本語のフローチャートやpython擬似コードは、オリジナルです。論文にはなく、論文の趣旨に沿って作成しました。

対象論文:BookRAG: A Hierarchical Structure-aware Index-based Approach for Retrieval-Augmented Generation on Complex Documents(arXiv:2512.03413v1)


0. まず結論:BookRAGは「非構造/複雑文書」に何が効くのか

現実のドキュメント(手順書、ハンドブック、書籍、PDF報告書など)は、

  • 章・節・小節の論理階層

  • 表・図・キャプション・本文などのブロック構造

  • 複数箇所に散らばるエビデンス

を持ちます。BookRAGは、これを

  1. 文書ネイティブの階層(Tree)

  2. 細粒度のエンティティ関係(Knowledge Graph)

  3. その対応付け(Graph–Tree Link)

として統合した BookIndex を作り、さらにクエリの種類に応じて エージェントが動的に検索ワークフローを組むことで、複雑文書QAの精度と再現率(Recall)を大きく押し上げます。


1. 従来RAG手法の類型(1行:名前とポイント/1行:欠点)

論文 Figure 1・Table 1 の整理に基づく「代表的な2系統+代表例」です。
ごくごく簡単に整理しておきましょう。

1.1 テキスト化→テキストRAG系(Text-based RAG / Graph-based RAG)

  • テキスト化RAG(Vanilla RAG / BM25):PDF等を文字列化し、チャンク検索(ベクトル/BM25)で拾ってLLMに渡す。欠点:レイアウト・階層(章節や表の所属)が失われ、情報が断片化しやすい。

  • グラフベースRAG(GraphRAG / RAPTOR):テキストからKGや階層クラスタ(コミュニティ/要約ツリー)を作り、高レベル要約と局所情報を使う。
    欠点:文書固有の「章→節→表/図」構造とズレやすく、複雑文書の“位置・所属”が抜け落ちる。

1.2 レイアウト分割→マルチモーダル/パイプライン系(Layout-segmented RAG)

  • レイアウト分割RAG(MM-Vanilla / DocETL 等):段落・表・図などのブロック単位で保持し、(マルチモーダル)検索してLLM処理する。
    欠点:ブロック間の関係(節を跨ぐ依存、表と本文の対応、章節横断の関連)が弱く、多段推論(multi-hop)が難しい/ワークフローが静的になりがち。

論文が指摘する根本原因は2つ:
- L1:構造と意味の深い結合を捉えられない(構造だけ/意味だけ、どちらかが欠ける)
- L2:クエリワークフローが静的(多様な質問に同じパイプラインを当てる)


Figure 1:既存手法 vs BookRAG

  • 既存は「テキスト化して一般RAG」or「レイアウト分割して検索」だが、
    階層と関係の両方を同時に扱いにくい

  • BookRAGは Tree+KG+GT-Link を前提に、構造と意味の両方を保持して検索する。


2. BookRAGの中核:BookIndex(BookIndex / ブックインデックス)

BookRAGのオフライン索引は BookIndex で、論文の定義は次の三つ組です:

BookIndex B = (T, G, M)

  • T:Tree(ツリー/文書階層木) 章・節・表・図・本文などを、文書の論理階層に沿って木構造で表す。

  • G:Knowledge Graph(知識グラフ/KG) 文書内から抽出したエンティティ(entity)と関係(relation)をグラフとして保持。

  • M:Graph–Tree Link(GT-Link/グラフ–ツリー対応)
    各エンティティが「文書ツリーのどのノード由来か」を結びつける写像(多対多)。

この設計により、

  • Treeは “情報パッチ(information patches)”(章節単位のまとまり)

  • Graphは “情報の匂い(information scent)”(エンティティを手がかりにたどれる)

として機能します(Information Foraging Theory: IFTの比喩)。


BookIndex 概念図(Tree / KG / GT-Link)

3. BookIndexの作り方(構築フェーズ:Figure 2)

BookIndex構築は大きく Tree Construction → Graph Construction の2段階です。

3.1 Tree Construction(ツリー構築)

(1) Layout Parsing(レイアウト解析)

  • 文書ページからブロック(Text/Table/Image/Title等)を抽出。

  • 各ブロックは(内容c、初期タイプτ、レイアウト特徴f)を持つ。

(2) Section Filtering(セクション同定・フィルタリング)

  • Layout ParsingでTitleと判定された候補群をLLMで分析し、

    • 階層レベル l_i(例:1,2,3… / None)

    • 最終タイプ τ’(Section/Text/Table/Image など)
      を決める。

  • これにより、パーサの誤判定(例:大きなフォントの説明文がTitle扱い)を補正。

(3) ツリーTを確定

  • レベル情報と文書順序から親子関係を確定し、文書ネイティブの階層木を作る。

3.2 Graph Construction(グラフ構築)

(1) KG Construction(KG抽出)

  • ツリー各ノードから、モダリティに応じて

    • textノード:LLM

    • imageノード:VLM
      でエンティティ・関係を抽出。

  • Table等の構造では、表自体のエンティティ(v_table)や、行・列ヘッダなどもエンティティ化し “ContainedIn” 関係で結ぶ。

  • この時点で「どのツリーノードから抽出したか」を記録し、後でGT-Link Mを作れるようにする。

(2) Gradient-based Entity Resolution(勾配ベースのエンティティ解決;Algorithm 1)

目的:同じ概念が別表記(略語・言い換え)で分裂し、KGが分断される問題を抑える。

  • 新規エンティティ v_n に対し、埋め込みDBから top-k 候補を取り、リランカでスコア順に並べる。

  • スコアの落ち方(勾配)を見て「高スコア群→急落→低スコア群」というパターンを検出。

    • 急落がある場合:既存概念の別名の可能性が高い(Case B)

    • 急落がない場合:新規概念の可能性が高い(Case A)

  • 高スコア群が複数ある場合のみLLMで最終決定し、過剰な二乗比較(O(n^2))を避ける。

(3) GT-Linkの確定

  • ERでマージした場合、元ノード集合も統合して M: V → P(N) を完成させる。


Figure 2:BookIndex構築

  • 上側で文書を階層木にする(レイアウト→章節レベル推定)。

  • 下側でエンティティ・関係を抽出し、ERで統合してKGを洗練。

  • 点線(青)はエンティティがどのツリーノード由来かを示す(GT-Link)。



4. 検索方法(オンライン検索:Agent-based Retrieval)

BookRAGは「クエリの種類に応じて、オペレータ列(Operator Plan)を動的に組み立て、BookIndexを操作する」方式です。

  • Single-hop(単一箇所で完結)

  • Multi-hop(複数箇所の統合が必要)

  • Global Aggregation(フィルタ+集計が必要)

4.1 Figure 3:3段階のステップ概要(全体骨格)

Figure 3はオンライン処理を

  1. Agent-based Planning(計画)

  2. Retrieval Process(検索)

  3. Generation Process(生成)
    の3ステージに分けます。

  • Planning:クエリ分類→オペレータプラン生成

  • Retrieval:Selectorで探索範囲を絞る→Reasonerでスコアリング

  • Generation:断片証拠を統合し回答


4.2 Figure 4:オペレータライブラリと実行例(図の範囲で全体フローを説明)

Figure 4は、BookRAGのオンライン処理を「部品(Operator Library)」と「実行トレース」で示します。

(A) Operator Library(オペレータ群)

  • Formulator(前処理):Decompose / Extract

  • Selector(絞り込み):Filter_Modal / Filter_Range / Select_by_Entity / Select_by_Section

  • Reasoner(推論・順位付け):Graph_Reasoning / Text_Reasoning / Skyline_Ranker

  • Synthesizer(合成):Map / Reduce

(B) 実行トレース(Single-hop例)

図の例では、AgentがSingle-hopと判定し、概ね次を実行します:

  1. Extract:クエリから重要エンティティ抽出

  2. Select_by_Entity:そのエンティティに紐づくセクション(サブツリー)を取得

  3. Graph_Reasoning & Text_Reasoning:グラフ重要度+テキスト関連度で評価

  4. Skyline_Ranker:多基準で非劣(Paretoフロンティア)ノードを残す

  5. Reduce:最終回答に統合

つまり Figure 4 が示す「全体フロー」は、
エージェントがオペレータ列を計画し、それを順に実行して検索・推論・統合する、という枠組みです。



Figure 3(ステップ概要)+Figure 4(Operator群)を統合したフロー図

5. 検索の中身:Selector→Reasoner→Skyline の意味

5.1 Selector(絞り込み)

IFT(Information Foraging Theory)に沿って、まず匂い(エンティティ)や条件(ページ範囲/モダリティ)で 探索空間(パッチ)を狭める

5.2 Reasoner(推論・スコアリング)

  • Graph_Reasoning:部分グラフに対し PageRank 等でエンティティ重要度を計算し、GT-Linkでツリーノードへ投影。

  • Text_Reasoning:ノード内容とクエリの意味類似でスコア。

5.3 Skyline_Ranker(多目的選択)

固定 top-k ではなく、複数スコア次元で「他に完全に劣らないノード」(Pareto最前線)を残す。


6. 評価(実験設定・指標・主要ベンチマーク比較)

6.1 評価方法・評価指標(Section 6 / Appendix A)

  • QA指標:Exact Match(EM)、Accuracy(包含ベース)、F1(token-level)

  • Retrieval recall:手動ラベルした正解ブロック集合に対して、検索集合がどれだけ含むか

  • 効率:クエリ時間、トークン使用量

6.2 主要ベンチマーク(Table 4)

  • MMLongBench(マルチモーダル長文書)

  • M3DocVQA(HTML型ドキュメント由来の多様文書)

  • Qasper(科学論文QA)

6.3 主要ベンチマークとの比較(性能)

QA性能(Table 5)

BookRAGは全データセットでSOTA。

  • MMLongBench:EM 43.8 / F1 44.9

  • M3DocVQA:EM 61.0 / F1 66.2

  • Qasper:Accuracy 55.2 / F1 61.1

Retrieval recall(Table 6)

  • BookRAG:57.6 / 71.2 / 63.5(MMLongBench / M3DocVQA / Qasper)

  • 次点(例:GraphRanker)は最大でも 44.5% 程度で、BookRAGが大きく上回る。

6.4 効率(Figure 5)

  • BookRAGは graph 系と同程度の時間・トークンで動作。

  • 特に MMLongBenchでは DocETL が 53M tokens超、BookRAGは 5M tokens未満 と、桁違いに低コスト(論文記述)。


7. なぜこの手法が効果的だったのか(考察)

7.1 「構造(Tree)」×「意味関係(KG)」×「対応(GT-Link)」の相乗

  • Treeだけ:章節は分かるが、節横断の関連が弱い

  • KGだけ:意味関係は取れるが、文書の所属・構造が薄れる

  • 統合:“どの概念が、どの章節・表・図に現れるか” を保持でき、探索が安定

7.2 IFT(情報採餌理論)に沿う計算資源配分

  • まずパッチ(章節)を狭めてから、そこで推論する「narrow-then-reason」

  • 無駄な候補を読ませないので、精度とコストの両面で効く

7.3 KG品質(Gradient-based ER)が multi-hop を支える

  • ERによりグラフの分断が減る → 経路がつながる → multi-hop推論が成立

  • 図6では、密度増・連結成分減などで改善が確認される


8. 課題・限界(論文から整理)

8.1 BookIndex作成の難しさ・コスト

  • Layout解析+LLMの章レベル推定+VLMの視覚抽出+ER…と工程が多く、初期構築が重い。

8.2 リソース要求(特にVLM)

  • Appendix A.2:VLMは 8B では弱く 30B を採用した、と述べており、実運用ではGPU要件が課題。

8.3 Plannerの誤り(過分解)

  • エラー分析で、Single-hopでも過度にmulti-hop分解してしまうことがある(Plan Errorの一形態)。

8.4 KG汚染リスク

  • ERの誤マージは致命的(グラフ推論に直撃)で、保守的判断が必要。

9. 資料管理用ハッシュタグ

#BookRAG #BookIndex #RAG #AgenticRAG #GraphRAG #RAPTOR #DocETL
#複雑文書QA #ComplexDocumentQA #LongDocumentQA #マルチモーダルRAG #MultimodalRAG
#文書構造 #階層構造 #DocumentHierarchy #LayoutParsing #PDFParsing
#知識グラフ #KnowledgeGraph #EntityResolution #GradientBasedER #GTLink
#InformationForagingTheory #IFT #情報採餌理論 #InformationScent #InformationPatch
#SkylineRanking #ParetoFrontier #PageRank
#MMLongBench #M3DocVQA #Qasper #RetrievalRecall #ExactMatch #F1score #TokenCost

付録

付録1 Algorithm 1(勾配ER)の処理をデータフロー化した図表



付録2 検索のためのプロンプト事例

論文では「検索そのもの(Select/Reasoning)」のプロンプト全文は提示されておらず、主に (a) クエリ分類(b) 分解(c) グローバル集計用フィルタ生成のプロンプト例が提示されています。ここではそれをもとに、日本語化したプロンプトを紹介します。

2.1 クエリ分類プロンプト(Figure 10:Single-hop / Multi-hop / Global)

目的:ユーザー質問を 3カテゴリに分類し、後段の Operator Plan を切り替える。

あなたはクエリ分析の専門家です。あなたの唯一のタスクは、ユーザーの質問を次の3カテゴリのいずれかに分類することです:
"single-hop"(単一ホップ), "multi-hop"(複数ホップ), "global"(全体集計)。
指定された JSON オブジェクト以外は一切出力しないでください。

【カテゴリ定義】
1. single-hop:
- 文書内の「単一の連続した場所」から情報を取得すれば完全に答えられる質問。
  (例:1つの段落、1つの表全体、1つの図など)
- 推論や比較が必要でも、必要情報がその単一箇所にすべて存在するなら single-hop。
- 例:"Figure 2 のタイトルは?"
- 例:"Latinos の5%は子どもの経済的上昇移動をどう見ている?" -> 1つの図表/段落を見ればよいので single-hop。

2. multi-hop:
- 複数の single-hop サブ質問に分解が必要で、それぞれ別の検索(retrieval)アクションが必要な質問。
- しばしばネストした条件や間接制約が含まれ、主質問の前に前提解決が必要。

3. global:
- 明確な構造フィルタに基づいて、集合に対する集計(カウント、列挙、要約、分析など)が必要な質問。
- 例:"文書内に表はいくつある?" -> table をフィルタして COUNT する。

ユーザー質問:{query}

【出力形式】
以下の JSON のみを返してください(例):
{"category": "single-hop"}

2.2 クエリ分解プロンプト(Figure 11:multi-hop 用)

目的:multi-hop を「並列に検索できる retrieval サブ質問群」と「統合する synthesis(検索不要)」に分解する。

あなたはクエリ分解の専門家です。
"multi-hop"(複数ホップ)の質問を、単純で原子的なサブ質問列に分解し、各サブ質問を type で分類してください。

【重要な指示】
1. retrieval サブ質問は、文書から独立に取得できる『事実・数値・値』の参照タスクでなければならない。
2. retrieval サブ質問どうしは依存してはいけない(並列実行可能であること)。
   別サブ質問の答えがないと成立しない retrieval を作ってはいけない。
3. synthesis サブ質問は、取得済みの答えを比較・計算・統合するタスクであり、新たな文書参照を必要としない。

【出力】
キー sub_questions の JSON を返す。各要素は question と type("retrieval" または "synthesis")を持つ。

ユーザー質問:{query}

出力例(形式だけの例):
{
  "sub_questions": [
    {"question": "...", "type": "retrieval"},
    {"question": "...", "type": "retrieval"},
    {"question": "...", "type": "synthesis"}
  ]
}

2.3 グローバル集計クエリのフィルタ生成プロンプト(Figure 12:global 用)

目的:global クエリを「フィルタ列 + 集計操作」に落とす(BookRAG の Filter_Range / Filter_Modal 相当)。

あなたは高度に特化したAIアシスタントです。
あなたの唯一の機能は、"global" クエリを分析し、フィルタ手順と最終の集計操作を指定する『単一の正しい JSON』を返すことです。
他のテキストや説明は一切出力しないでください。

【Filters(フィルタ)】
filters は必ず出力する。
- filter_type は次のいずれか: ["section", "image", "table", "page"]
- section: 章・節・付録・参考文献など
- image: 図・画像・プロット等
- table: 表
- page: ページ番号または範囲
- filter_value: section または page で必要なら指定。
  image/table の場合は必ず null。

【Operation(集計操作)】
operation は次のいずれか: ["COUNT", "LIST", "SUMMARIZE", "ANALYZE"]

【例】
ユーザー: "3〜10ページにある図はいくつ?"
出力: {"filters": [{"filter_type": "page", "filter_value": "3-10"}, {"filter_type": "image", "filter_value": null}], "operation": "COUNT"}

ユーザー: "Methodology セクションの data augmentation の議論を要約して"
出力: {"filters": [{"filter_type": "section", "filter_value": "Methodology"}], "operation": "SUMMARIZE"}

ユーザー: "このレポートの章はいくつ?"
出力: {"filters": [{"filter_type": "section", "filter_value": null}], "operation": "COUNT"}

【対象クエリ】
ユーザー: {query}

2.4(参考)Entity Resolution 判定プロンプト(Figure 13)

これは検索フェーズではなく オフラインの BookIndex 構築時(Graph Construction) に使うものですが、BookRAGの中核要素なので補足します。

  • 新規エンティティと候補エンティティ群を与え、同一概念なら candidate id、違うなら -1 を JSON で返す。

  • 誤マージがKGを汚染するので「迷ったら -1」が強い規範。

【目的 / Goal】
あなたは「エンティティ解決(Entity Resolution: ER)」の専門審査官です。
あなたのタスクは、提示された「新規エンティティ(New Entity)」が、既存の知識グラフから取得された「候補エンティティ(Candidate Entities)」のうちのいずれかと、現実世界の“同一の概念・対象”を指しているかを判定することです。

出力は JSON オブジェクトのみとし、
- 一致する候補がある場合:その候補の id
- 一致がない場合:-1
を返してください。あわせて、判断理由を1文で述べてください。

【前提 / Context】
- あなたには、直近で抽出された「新規エンティティ」1件が与えられます。
- あなたには、既存知識ベースから類似度で取得された「候補エンティティ」一覧が与えられます。
- 各候補には参照用の一意な id があります。

【コアタスク & ルール / Core Task & Rules】
1) 新規エンティティを分析する
- entity_name(名称)、entity_type(種別)、description(説明)を読み、何を指すか把握する。

2) フィールドごとの厳密判定(重要度付き)
- entity_name(最重要):
  - 名前が極めて近い、略語と正式名(例:"LLM" と "Large Language Model")、または広く知られた別名であること。
  - 似ていても「並列の別概念」(例:"Event Detection" と "Named Entity Recognition")は一致ではない。
- entity_type(中重要):
  - 完全一致は不要だが、互換性のある近いタイプであること(例:COMPANY と ORGANIZATION)。
- description(文脈重要):
  - 記述は抽出箇所により異なることがある。
  - 表層の類似よりも「同じ根底の対象・概念」を説明しているかを判断する。

3) 厳格かつ保守的に
- 誤マージは知識グラフを破壊し得るため、判定基準は非常に高くする。
- マージ漏れ(-1)はまだ許容だが、誤マージは致命的。
- 迷ったら必ず -1。
- 原則は「一致しない」から開始し、強い根拠がある場合にのみ一致とする。
- 例:"Apple"(果物)と "Apple Inc."(企業)は一致ではない。

4) 出力形式(JSONのみ)
- 必ず次の2キーを持つ JSON を 1つだけ出力する。
  - select_id: 一致した候補の id(int)。一致なしなら -1。
  - explanation: 1文の理由(string)。

【出力スキーマ / Output Schema】
{
  "select_id": integer,
  "explanation": "string"
}

【入力 / Input Data】
- New Entity:
  - entity_name: ...
  - entity_type: ...
  - description: ...

- Candidate Entities:
  - [{"id": ..., "entity_name": ..., "entity_type": ..., "description": ...}, ...]

【実行 / Task Execution】
上記のルールに従って判定し、JSONのみを出力してください。


付録3 ユーザー入力→応答出力までの Python 擬似コード

あくまで「設計イメージ」を掴むための擬似コードです。
実装詳細(各モデル呼び出し、埋め込みDB、VLM、Leiden等)は論文外なので抽象化しています。

from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Literal

QueryCategory = Literal["single-hop", "multi-hop", "global"]

@dataclass
class BookIndex:
    """B = (T, G, M) のまとめ。詳細実体は省略(擬似)。"""
    tree: Any  # T: 文書階層木
    graph: Any # G: KG
    gt_link: Any # M: Graph–Tree Link

@dataclass
class OperatorPlan:
    """実行するオペレータ列(例:Extract -> Select_by_Entity -> ...)"""
    steps: list[dict[str, Any]]  # [{"op": "Extract", "params": {...}}, ...]


# ---------------------------
# LLM/VLM 呼び出し(擬似)
# ---------------------------

def llm_json(prompt: str) -> dict[str, Any]:
    """LLMにプロンプトを投げて JSON を返す(擬似)。"""
    raise NotImplementedError


def classify_query(query: str) -> QueryCategory:
    prompt = JAPANESE_CLASSIFICATION_PROMPT.format(query=query)
    res = llm_json(prompt)
    return res["category"]


def decompose_query(query: str) -> list[dict[str, str]]:
    prompt = JAPANESE_DECOMPOSITION_PROMPT.format(query=query)
    res = llm_json(prompt)
    return res["sub_questions"]  # [{"question": ..., "type": "retrieval"|"synthesis"}, ...]


def generate_global_filters(query: str) -> dict[str, Any]:
    prompt = JAPANESE_GLOBAL_FILTER_PROMPT.format(query=query)
    return llm_json(prompt)  # {"filters": [...], "operation": ...}


# ---------------------------
# BookRAG オペレータ(擬似)
# ---------------------------


def op_extract_entity(query: str, book_index: BookIndex) -> dict[str, Any] | None:
    """Extract: クエリからエンティティ抽出し、KGにリンクできる形で返す(擬似)。"""
    # 実際は LLM で抽出し、KG内のエンティティ候補へマッチングする
    return {"entity_name": "..."}  # or None


def op_select_by_entity(entity_name: str, book_index: BookIndex) -> list[Any]:
    """Select_by_Entity: GT-Linkを使い、該当エンティティが出現するセクションのサブツリーを返す。"""
    return []  # nodes


def op_select_by_section(query: str, book_index: BookIndex) -> list[Any]:
    """Select_by_Section: LLMが関連セクションを選び、そのサブツリーを返す(fallback)。"""
    return []


def op_filter_modal(nodes: list[Any], modal_type: str) -> list[Any]:
    return [n for n in nodes if getattr(n, "type", None) == modal_type]


def op_filter_range(nodes: list[Any], page_range: str) -> list[Any]:
    # page_range: "3-10" のような範囲指定
    return nodes


def op_graph_reasoning(nodes: list[Any], start_entity: str, book_index: BookIndex) -> dict[Any, float]:
    """Graph_Reasoning: 部分グラフ上で PageRank 等→GT-Linkでノードへスコア投影(擬似)。"""
    return {n: 0.0 for n in nodes}


def op_text_reasoning(nodes: list[Any], query: str) -> dict[Any, float]:
    """Text_Reasoning: ノード内容とクエリの関連度スコア(擬似)。"""
    return {n: 0.0 for n in nodes}


def op_skyline_ranker(nodes: list[Any], scores: dict[str, dict[Any, float]]) -> list[Any]:
    """Skyline_Ranker: 多基準(例:graph_scoreとtext_score)でPareto非劣解を残す(擬似)。"""
    # scores = {"graph": {node: s}, "text": {node: s}}
    return nodes[:10]  # 擬似: 実際はPareto frontier


def op_map_partial_answer(nodes: list[Any], query: str) -> str:
    """Map: 断片証拠から部分回答を作る(擬似)。"""
    return "partial answer"


def op_reduce_final_answer(partials: list[str], query: str) -> str:
    """Reduce: 部分回答を統合して最終回答(擬似)。"""
    return "final answer"


# ---------------------------
# Operator Plan(計画)生成(擬似)
# ---------------------------


def plan_for_single_hop(query: str) -> OperatorPlan:
    # 論文の式(9)(10)に対応するイメージ
    return OperatorPlan(steps=[
        {"op": "Extract"},
        {"op": "Select_by_Entity_or_Section"},
        {"op": "Graph_Reasoning"},
        {"op": "Text_Reasoning"},
        {"op": "Skyline_Ranker"},
        {"op": "Reduce"},
    ])


def plan_for_global(query: str, filters_spec: dict[str, Any]) -> OperatorPlan:
    return OperatorPlan(steps=[
        {"op": "Filter_Range_or_Section", "params": filters_spec.get("filters", [])},
        {"op": "Filter_Modal", "params": filters_spec.get("filters", [])},
        {"op": "Map"},
        {"op": "Reduce"},
    ])


# ---------------------------
# エンドツーエンド実行(擬似)
# ---------------------------


def answer_query(query: str, book_index: BookIndex) -> str:
    category = classify_query(query)

    if category == "single-hop":
        plan = plan_for_single_hop(query)

        extracted = op_extract_entity(query, book_index)
        if extracted is not None:
            nodes = op_select_by_entity(extracted["entity_name"], book_index)
            start_entity = extracted["entity_name"]
        else:
            nodes = op_select_by_section(query, book_index)
            start_entity = ""  # なし

        graph_scores = op_graph_reasoning(nodes, start_entity, book_index) if start_entity else {n: 0.0 for n in nodes}
        text_scores = op_text_reasoning(nodes, query)
        selected = op_skyline_ranker(nodes, scores={"graph": graph_scores, "text": text_scores})

        # single-hop は partial を作らず、そのまま Reduce するイメージ
        partial = op_map_partial_answer(selected, query)
        return op_reduce_final_answer([partial], query)


    if category == "multi-hop":
        subs = decompose_query(query)
        partials: list[str] = []

        # retrieval サブ質問を並列に(概念上)解き、最後に synthesis
        for sq in subs:
            if sq["type"] == "retrieval":
                partials.append(answer_query(sq["question"], book_index))

        # synthesis は新規検索しない(論文プロンプトの要件)
        synthesis_questions = [sq["question"] for sq in subs if sq["type"] == "synthesis"]
        synthesis_instruction = synthesis_questions[-1] if synthesis_questions else "統合して答えを出して"
        return op_reduce_final_answer(partials + [synthesis_instruction], query)


    # category == "global"
    filters_spec = generate_global_filters(query)
    plan = plan_for_global(query, filters_spec)

    # ここでは tree 全体からフィルタで絞るイメージ
    nodes = list(getattr(book_index.tree, "nodes", []))
    for f in filters_spec.get("filters", []):
        ftype = f["filter_type"]
        fvalue = f.get("filter_value")
        if ftype == "page" and fvalue:
            nodes = op_filter_range(nodes, fvalue)
        if ftype in ("image", "table"):
            nodes = op_filter_modal(nodes, ftype)  # modal_typeに流用(擬似)
        # section フィルタ等は必要に応じて追加

    # Map: 各ノードを解析し、Reduce: COUNT/LIST/SUMMARIZE/ANALYZE を生成
    partial = op_map_partial_answer(nodes, query)
    return op_reduce_final_answer([partial], query)


いいなと思ったら応援しよう!