RAGでは、文書のセクションに沿ったチャンクが欲しいときに、PDFをMarkdownに変換します。各見出しが分割点になり、チャンクの上位にある見出しがメタデータとしてチャンクに付いてきます。インデックスを作る前に見出し、表、OCRのテキストを確認し、答えがすでにわかっている質問で検索を試してください。

このガイドは、ポリシー、マニュアル、契約書、研究資料といった非公開の文書に対する検索を構築する開発者向けです。埋め込みモデルとベクトルストアはすでに決まっているものとします。

ノートパソコンの隣に積まれた罫線入りのインデックスカードと紙片。1つの束は淡い紫のリボンで結ばれている。
文書のセクションに沿ったチャンク。

見出しがあるとチャンクが良くなる理由

固定サイズのスプリッターは、切れ目がどこであっても、決まった長さでテキストを切ります。チャンクがセクションの途中から始まり、何についての内容かを示す見出しが失われることがあります。「リクエストは1分あたり100件までに制限されます」という文は、「Sandbox」の下と「Production」の下では意味が異なります。見出しがなければ、検索エンジンもモデルも、どちらなのか判断できません。

Markdownは、文書のアウトラインを#、##、###で始まる行として保持します。見出しベースのスプリッターはそれらの行で切るので、各チャンクは1つのセクションに収まり、チャンクの上位にある見出し(セクションパス)も記録されます。 LangChainのMarkdown分割ガイド でも、同じ考え方が説明されています。テキストを、それが属する見出しと一緒に保つことです。

セクションの長さはさまざまなので、長いセクションにはサイズによる2回目の分割が必要です。セクションパスは、その各部分に適用されます。

PDFを変換して見出しを確認する

MD Factoryで、PDFの入ったフォルダーを追加します。各Markdownファイルは、PDFと同じ名前でその隣に保存され、画像はその隣のフォルダーに入ります。スキャンしたPDFは、PC上でOCRにかけられます。フォルダー内のWordファイルなどの形式も、同じように変換されます。

分割のコードを書く前に、いくつかの結果を開いてみましょう。

  • 章、セクション、サブセクションが#、##、###として出力されていることを確認します。PDFでは見出しが大きいフォントや太いフォントでしか示されていないことが多く、どのコンバーターも、セクションがどこから始まりどう入れ子になるかを推測する必要があります。レベルが間違っている箇所は直してください。
  • Wordファイルでは、作成者がWordの見出しスタイルを使っていれば、見出しが引き継がれます。太字や大きな文字にしただけのテキストは、通常の段落のままです。
  • テキストの途中に、ランニングヘッダー、フッター、ページ番号が混ざっていないか探します。
  • ファイルの隣の要確認をクリックすると、確認すべき点がわかります。PDFの場合、OCRでテキストの一部が読み取れなかったページや、テキストがまったく見つからなかったページが一覧に含まれることがあります。

インデックスを作る前に、Markdownファイルの問題を直してください。すべてのチャンクがその問題を引き継ぎます。

コードで見出しごとに分割する

LangChainのMarkdownHeaderTextSplitter は、指定した見出しレベルで分割し、各見出しをチャンクのメタデータに入れます。各セクション内のサイズは、2つ目のスプリッターで制限します:

from pathlib import Path
from langchain_text_splitters import (
    MarkdownHeaderTextSplitter,
    RecursiveCharacterTextSplitter,
)

corpus = Path("corpus")
by_heading = MarkdownHeaderTextSplitter(
    [("#", "h1"), ("##", "h2"), ("###", "h3")], strip_headers=False
)
by_size = RecursiveCharacterTextSplitter(chunk_size=1500, chunk_overlap=150)

chunks = []
for path in sorted(corpus.rglob("*.md")):
    sections = by_heading.split_text(path.read_text(encoding="utf-8"))
    for chunk in by_size.split_documents(sections):
        chunk.metadata["source"] = path.relative_to(corpus).as_posix()
        chunk.metadata["section"] = " > ".join(
            chunk.metadata[h] for h in ("h1", "h2", "h3") if h in chunk.metadata
        )
        chunks.append(chunk)

ここでのチャンクサイズとオーバーラップは出発点です。お使いの埋め込みモデルに合わせて調整してください。チャンクのテキストに見出しの行を残し、埋め込むテキストにもセクション名が入るように、この例では次を設定しています: strip_headers=False.

LlamaIndexのMarkdownNodeParser も、見出しによる同じ分割を行います。SimpleDirectoryReaderでファイルを読み込むと、各ノードにファイル名も付きます:

from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import MarkdownNodeParser

documents = SimpleDirectoryReader(
    "corpus", recursive=True, required_exts=[".md"]
).load_data()
nodes = MarkdownNodeParser().get_nodes_from_documents(documents)
print(nodes[0].metadata["file_name"], nodes[0].metadata["header_path"])

header_path は、ノードより上の見出しを一覧にしたものです。例: /Employee handbook/Expenses/。ノード自体の見出しは、そのテキストの最初の行になります。

すべてのチャンクに出典を残す

各チャンクと一緒に、少なくとも次の3つを保存します。

  • ファイル。Markdownファイルは、元のPDFと同じ名前なので、handbook.mdのチャンクからhandbook.pdfにたどれます。コーパスのフォルダーからの相対パスを保存してください。別のフォルダーにあるterms.mdという名前の2つのファイルは、別の文書だからです。
  • セクションパス。回答がどのセクションを使ったかを示せるようにし、読む人がその箇所に直接移動できるようにします。
  • 版または日付。ポリシーが変わったときは、古いファイルのチャンクを削除して新しいものをインデックスに登録します。両方のバージョンを返さないようにするためです。

アクセス権の異なる人が同じインデックスに問い合わせる場合は、各文書を誰が読めるかも保存し、検索時にそれで絞り込みます。検索エンジンは、質問に一致するチャンクをすべて返します。

MD Factoryが出力するMarkdownには、ページ番号がありません。回答でページを示す必要がある場合は、元のPDFで該当箇所を探してください。

表とOCRのテキストは慎重に扱う

Markdownの表では、見出し行は先頭に1回しかありません。表がチャンクサイズより長い場合、サイズによるスプリッターが表を切り、後ろの部分は列名なしになります。LangChain(langchain-text-splitters 1.1.3)でのテストでは、40行の表を300文字のチャンクに分割すると4つの断片になり、見出し行が含まれていたのは最初の1つだけでした。

対処法は2つあります。表をほかより長くても1つのチャンクにまとめるか、すべての断片の先頭に見出し行を繰り返すかです。

WordとExcelのファイルでは、結合されたセルや複雑なセルが簡素化された場合、MD Factoryはその文書を要確認にします。PDFの表にはそのような印は付かないので、特に結合された見出しのある表は、いくつかを元のファイルと見比べてください。

OCRのテキストには、別の確認が必要です。読み誤った数字、名前、コードは、そのまま回答に入り込み、チャンクを見ても異常に見えません。いくつかのチャンクの数値をスキャンと見比べ、フラグが付いたページは、修正するまでインデックスから外しておいてください。確認すべき点は、当サイトの スキャンしたPDFのOCRを確認するガイド.

実際の質問で検索を試す

実際に人が尋ねそうな質問を書き出し、それぞれについて、答えが入っているファイルとセクションも書いておきます。まず、1ダースから2ダース程度から始めましょう。

  1. 各質問を検索エンジンに通し、上位の結果を見ます。
  2. 正しいセクションが含まれているかを記録します。
  3. 外れた質問ごとに、返ってきたチャンクと、返るべきだったチャンクを比べます。よくある原因は、見出しレベルの誤り、見出しから切り離された表、重要な用語のOCRの誤り、答えが埋もれるほど長いセクションです。
  4. チャンクサイズ、見出しレベル、表の分割方法など、1つだけ変更して、同じ質問をもう一度実行します。

質問はコーパスの隣のファイルに保存しておき、文書や分割のコードが変わるたびに再実行します。

MD Factoryを使う場合とライブラリを使う場合

MD Factoryは、Windows用のデスクトップアプリです。自分のPCで変換して目で確認したい、非公開のコレクションに向いています。フォルダーを追加し、ステータスを見て、確認が必要なファイルを開き、直してから、インデックス作成のスクリプトをそのフォルダーに向けます。

MD Factoryは、変換中、文書をPC上にとどめます。そのあとで、ホスト型の埋め込みAPIでチャンクを埋め込んだり、ホスト型のモデルに送ったりすると、そのテキストはそのプロバイダーに送られます。文書をネットワークの外に出せない場合は、ローカルのモデルを使ってください。

文書が絶えず届く場合、サーバー上にある場合、または人が見ていなくても変換しなければならない場合は、代わりにコード内で変換ライブラリを使ってください。選択肢については、当サイトの PDFからMarkdownへのコンバーターの比較.