PaPoo
cover
technews
Author
technews
世界の技術ニュースをリアルタイムでキャッチし、日本語でわかりやすく発信。AI・半導体・スタートアップから規制動向まで、グローバルテックシーンの「今」をお届けします。

PDFの「目次」を掘り起こして、RAGに章単位で読ませる話

目次があるのに、機械には見えない

この記事が扱っているのは、かなり「あるある」なPDFの悩みです。人間が開くと、きちんと目次ページがある。ところがPDFビューアのブックマーク欄は空っぽ。つまり、ページ上には文字として目次が印刷されているのに、ソフトウェアが使える構造情報としては入っていない、という状態です。

image_0001.jpg

著者はこれを、RAGシステムの文脈でかなり実務的に見ています。文書解析の段階で toc_df という表を作るのですが、ここには leveltitlestart_pageend_pagebreadcrumb のような情報が入る想定です。これがあると、検索は章単位で絞れるし、chunking(文書を小分けにする処理)も見出し境界で切れる。逆にこれが空だと、検索はページを総当たりしがちで、結果もぼやける。RAGって、LLMの派手さの裏で、こういう地味な構造復元にかなり支えられているんだよな、と改めて思います。

ここで大事なのは、この問題を2種類に分けている点です。
1つは、PDFに「リンク付き」の目次ページがあるケース。
もう1つは、見た目は立派な目次ページなのに、リンクはなく、ただ印刷されているだけのケース。
後者のほうがずっと面倒です。

image_0002.svg

まずは「リンクがあるか」を見る

リンク付きの目次は、かなり幸運なケースです。PDF内リンクが目次の各項目に貼ってあれば、そこから飛ぶ先のページ番号をそのまま取れるからです。著者は PyMuPDF を使って、各ページ上のリンクを調べ、内部リンク LINK_GOTO が一定数以上あるページを「目次ページらしい」と見なしています。

image_0004.png

このやり方のいいところは、ほぼ迷いがないことです。タイトルのテキストをリンク領域から拾って、リンク先の物理ページを取る。これで終わりです。人間の目で見ても「ああ、たしかに目次だな」と一致しやすい。記事では NIST Cybersecurity Framework のような例が出ていて、実際に目次ページ上の各項目がクリック可能になっている文書なら、かなり素直に復元できると説明しています。

個人的には、ここは「PDFにちゃんと構造を入れておいてくれれば、こんなに楽なのに」という話でもあります。PDFって見た目は整っているのに、中身の構造は驚くほど粗いことがある。紙としては完成しているのに、機械には不親切。まさにその典型です。

image_0005.png

本当に厄介なのは、印刷された目次しかない場合

問題は、リンクがない目次です。たとえば「Contents」や「Table of contents」と題されたページがあって、項目名、点線のリーダー、右端にページ番号、というおなじみの見た目。でもそこに実際のリンクはない。FIPS 202 がこのタイプです。

image_0006.png

このとき、単に目次ページの文字を拾うだけでは足りません。なぜなら、目次に書かれている数字は「表示上のページ番号」であって、「PDFファイルの中で何ページ目か」という物理ページ番号とはズレていることがあるからです。

ここ、かなり重要です。
PDFの前半には表紙、奥付、前書き、目次そのものなどの front matter が入ることが多いので、本文のページ番号は途中から振り直されます。すると目次に「Introduction 1」とあっても、実際のPDFファイル上の1ページ目がIntroductionとは限らない。これを見落とすと、章の開始位置を全部ずらしてしまいます。

image_0007.png

著者が強調しているのは、​目次の数字はラベルであって、物理ページではないという点です。地味ですが、ここを雑に扱うと後工程が全部崩れます。RAGで「第2章を読ませたつもりが、実際には表紙のあたりを読ませていた」なんてことが起きたら、そりゃ答えもおかしくなります。

2段階に分ける、という発想が気持ちいい

image_0008.png

この記事の設計で面白いのは、目次復元を2段階に分けていることです。

最初にやるのは、目次ページから「項目名」と「表示上のページ番号」を読み取ること。これは、正規表現で Introduction ......... 12 のような行を拾えばよい。点線の並びや、スペースで区切られた番号を目印にします。いわば、見た目の目次を読む工程です。

image_0009.png

次にやるのが、その表示上のページ番号を実ページに合わせる作業です。ここで著者は、まず「一定のずれ」があると仮定します。つまり、
physical_page = displayed_page + shift
のような関係を想定して、いろいろな shift を試し、どれが一番多くの項目と合うかを探します。

これはかなり実用的な発想だと思います。全部を複雑な推論で解こうとせず、まずは「前書きが何ページかぶんあるだけでは?」という素朴な仮説を試す。文書処理って、意外とこういう荒い仮説が強いんですよね。きれいな理論より、まず当たる簡単な仮定。しかも実際の文書では、これで十分なことが多い。

image_0010.png

FIPS 202 の例では、前半の front matter のぶんだけページがずれていて、目次の「1」は実ページではかなり後ろにずれます。著者はこのシフトを推定し、目次のラベルと実ページの対応を再構成しています。

これがRAGに効く理由は、かなり現実的

image_0011.png

この話、単なるPDFパーサーの小技に見えるかもしれません。でも、RAGの実務ではかなり効きます。理由は単純で、検索対象の範囲が章単位で切れるようになるからです。

たとえば、ユーザーが「第3章だけで答えて」と言ったとき、目次がなければ文書全体をざっとなめるしかない。でも toc_df があれば、第3章の範囲を取り出して、その範囲だけを検索すればいい。無駄なページを読まなくて済むし、答えの根拠も明確になります。

image_0012.png

要するに、構造化された文書は、それだけでLLMにとってのノイズが減るわけです。大げさに言えば、目次は人間のための便利機能ではなく、機械にとっての地図です。PDFがその地図を出し忘れたなら、なんとかして描き直す必要がある。この記事はその作業を、かなり実務寄りにやっています。

こういう地味な処理が、実は一番ありがたい

image_0013.jpeg

派手なモデルや派手なプロンプトの話ではないですが、こういう「壊れた構造を復元する」処理は、現場では本当にありがたいです。しかも、この記事のアプローチは無理にLLMに頼らない。リンクがあるならリンクを使い、なければ文字列パターンとページずれの推定で攻める。筋がいいと思います。

とくに好感が持てるのは、「全部を一気に解こうとしない」ことです。
まずは目次の存在を検出する。
次に、リンクがあればそれを使う。
なければ印刷された目次を読む。
そのあとでページずれを補正する。
この順番が自然なんです。

image_0014.jpeg

PDF解析って、万能な一発解がないことのほうが多い。だからこそ、こういう段階的な設計のほうが強い。読んでいて、「そうそう、現実はこうだよな」とうなずきました。


参考: Reconstructing the Table of Contents a PDF Forgot to Ship, So RAG Can Scope by Section | Towards Data Science

同じ著者の記事