第 12 章: RAG データ パイプライン¶
章の概要¶
Retrieval-Augmented Generation (RAG) は、エンタープライズ LLM 導入に推奨されるアーキテクチャになっています。ただし、多くの RAG システムはデモでは優れていますが、実稼働環境では取得精度が低いために失敗します。この章では、RAG の中核となる真実を明らかにします。取得品質の上限は、データの解析とチャンクの粒度によって決定されます。非構造化データの「ラスト マイル」である文書解析を掘り下げ、PDF テーブルの復元と複数列認識の課題を調査します。セマンティックChunkingと親子インデックス付けの比較。Embeddingモデルの微調整とベクトル データベースの最適化クリティカル パスの分析。
12.0 学習目標¶
- 非構造化解析戦略をマスターする: 複数列の PDF や複雑な表に対して正しい解析ツール (ルールベースとビジョンベース) を選択する方法を学びます。
- 高度なチャンク アルゴリズムの実装: コンテキスト損失の問題を解決する「親子インデックス作成」戦略の Python コードを作成します。
- ハイブリッド検索パイプラインの構築: 高密度Embeddingと BM25 キーワード検索および RRF ランキングの融合を組み合わせたマスター。
- 評価と最適化: 取得品質評価とドメイン固有のEmbeddingモデル微調整のための RAGAS フレームワークの使用方法を学びます。
シナリオの紹介¶
チームのエンタープライズ ナレッジ Q&A システムがついに開始されました。 CEO は興奮気味に次のように入力しました。「2023 年第 4 四半期の収益報告によると、中国東部地域の純利益はいくらですか?」
システムは自信を持ってこう答えました。「レポートによると、純利益は 15% です。」 CEO は眉をひそめました。「私が知りたいのは利益率ではなく、具体的な金額です。それは全社的なものであり、中国東部地域のものではありません。」
データ リーダーとして、あなたはログを緊急に調査し、ソースに問題があることを発見しました。 1. 解析エラー: 収益 PDF は 2 列レイアウトを使用しています。通常の解析ツールは 1 行ずつ読み取るため、左列のテキストと右列のデータがマージされ、意味上の混乱が生じます。 2. 表の損失: 中国東部のデータはページをまたいだ表にありました。解析ツールはテーブル構造を完全に無視し、文字化けした文字列に変えました。 3. チャンクの断片化: 「中国東部地域」ヘッダーと特定の番号が異なるチャンクに分割されました。ベクトル検索でコンテキストの関連付けが失われました。
この「失敗」シーンは、Garbage In、Garbage Out という RAG の残酷な現実を明らかにします。プレトレーニングが「お腹いっぱいごちそうを食べる」ことだとすれば、RAGは「精密な手術」です。データ解析の逸脱は、取得および生成の段階で無限に増幅されます。
12.1 ディープドキュメント解析: 非構造化データの「ラストマイル」を克服する¶
RAG データ フローで最も処理が難しいのは、多くの場合、純粋なテキストではなく、エンタープライズの中核知識を含む PDF、PPT、スキャンされたドキュメントです。これらの形式は「人間が読む」ことを目的として設計されており、機械にとっては非常に不親切です。
12.1.1 複雑な PDF 処理: テキスト抽出以上のもの¶
PDF は本質的には描画命令のコレクションであり、構造化データではありません。通常の Python ライブラリ (PyPDF2 など) はテキスト ストリームのみを抽出でき、レイアウト情報を理解できません。
問題点 1: 複数列レイアウト 学術論文や技術マニュアルでは、2 段または 3 段のレイアウトが一般的です。単純なテキスト抽出では列をまたいで読み取り、「左列の最初の行 + 右列の最初の行」のような無意味な連結が生成されます。これを解決する鍵となるのが レイアウト分析 です。最新のツール (Microsoft の LayoutLM シリーズなど) は、ビジョン モデルを使用して、最初にレイアウト ブロックを識別し、次に読み上げ順にテキストを抽出します。
問題点 2: テーブルの修復
テーブルは RAG にとって悪夢です。表がテキストにフラット化されると、行と列の対応が失われます。
* ルールベース: PDF の線描画命令を使用してグリッドを再構築します (例: pdfplumber)。ネイティブ PDF に適しています。
* ビジョンベース: PDF を画像に変換し、オブジェクト検出モデルを使用してセル構造を識別し、コンテンツの OCR を結合します。これは、スキャンされたドキュメントと複雑なネストされたテーブルに対する唯一のアプローチです。
12.1.2 パーサーツールの選択の比較¶
複雑な企業ドキュメントに直面すると、ドキュメントの種類と予算に基づいて解析パイプラインを構築する必要があります。
| 特集 | 非構造化 (オープンソース) | LlamaParse (プロプライエタリ) | PyPDF/PDFMiner (基本) |
|---|---|---|---|
| 基本原則 | ルール+基本OCRハイブリッドモデル | 大規模モデルのビジョン理解 (Vision LLM) | 基礎となるテキスト ストリームを抽出する |
| テーブルの取り扱い | 中 (テーブル領域を識別できます。複雑なヘッダーは混乱を招きやすい) | 非常に強力 (Markdown テーブルとして再構築され、セマンティクスが保持されます) | 悪い (行/列が完全に乱れている) |
| 複数列の認識 | サポートあり (検出モデルベース) | サポートあり (ネイティブ レイアウトの理解) | サポートされていません (列間の読み取り) |
| コスト | 低 (ローカル コンピューティング) | 高 (API ページごとの課金) | 非常に低い |
| 使用例 | シンプルな Word/HTML、ルール修正された PDF | 複雑な収益レポート、スキャンされたドキュメント、ネストされたテーブル | 純粋なテキストの電子書籍 |
推奨事項: 主要なビジネス ドキュメント (契約書、収益報告書) については、LlamaParse または Azure Document Intelligence を優先します。大量の通常のドキュメントの場合は、コストを削減するために非構造化をクリーニングに使用します。
図 12-1: 従来の解析とインテリジェント解析 —— インテリジェント解析は、レイアウト分析を通じて複数列の順序とテーブル構造を保持します
12.2 Chunking戦略: コンテキストと取得の粒度のバランスを取る技術¶
ドキュメントを解析した後、それをモデルで処理可能なチャンクに分割する必要があります。チャンク戦略は取得精度を直接決定します。
12.2.1 基本戦略: 再帰的文字分割¶
最も単純な方法は、文字数を固定して分割する (たとえば、500 文字ごとにカットする) 方法です。しかし、これにより、完全な文や論理的な段落が半分になってしまうことがよくあります。
再帰的Chunking が現在のベースラインです。区切り文字の優先順位 (例: \n\n > \n > . > ) を定義し、段落の分割を優先し、次に文の分割を優先します。これにより、意味上の整合性が可能な限り維持されます。
12.2.2 高度な戦略: セマンティックチャンク化¶
再帰的なチャンク化でも、2 つの段落が同じトピックについて説明しているかどうかを判断することはできません。 セマンティック Chunking はEmbeddingモデルを使用します。 1. 隣接する文間のベクトル類似度を計算します。 2. しきい値を設定します。隣接する文の類似性が急激に低下した場合(トピックシフト)、そこで分割します。 この方法では、可変長のチャンクが生成されますが、セマンティック純度は非常に高く、「1 つのチャンクには半分の製品イントロと半分のアフターセールス ポリシーが含まれている」というノイズが回避されます。
12.2.3 高度な戦略: 親子インデックス作成¶
これは、RAG の 「検索の粒度」と「生成コンテキスト」 の矛盾を解決するための最終兵器です。 * 矛盾: チャンクが小さい = より焦点を絞ったセマンティクス、より正確なベクトル検索。しかし、小さすぎる = コンテキストが失われるため、LLM は包括的な回答を生成できません。 * 解決策: 1. ドキュメントを 大きなチャンク (親チャンク) (例: 1000 トークン) に分割します。 2. 各大きなチャンクをさらに 小さなチャンク (子チャンク) (例: 200 トークン) に分割します。 3. 小さなチャンクをベクトル化し、インデックスを作成します。 4. 取得時に、小さいチャンクを照合しますが、照合された小さいチャンクを含む 大きいチャンク を LLM に返します。
この「小規模から大規模な検索」戦略 (小規模から大規模な検索) により、生成に十分なコンテキストを提供しながら検索の精度が保証されます。
図 12-2: 親子インデックス作成メカニズム —— 取得された子ノードは実際に親ノードを返し、精度とコンテキストのバランスをとります
12.3 ベクトル化と保存: 教育機械業界の「専門用語」¶
データのチャンク化後、Embedding モデルを介してベクトルに変換し、ベクトル データベースに保存します。この段階では、一般的な解決策では不十分なことがよくあります。
12.3.1 Embeddingモデルの微調整¶
一般的なEmbeddingモデル (OpenAI text-embedding-3 や BGE-M3 など) は、一般的なコーパスでトレーニングされます。垂直ドメインでは、パフォーマンスが低下する可能性があります。 たとえば、医療分野では、「風邪」と「発熱」は一般的な意味では意味的に近いものですが、診断ロジックではまったく異なる病状を指す場合があります。 微調整 は、ベクトル空間の分布を調整して、類似した専門的な概念がより近くに集まるようにすることを目的としています。通常は 対照学習 損失を使用します。
\(d^+\) が正 (正しい文書) である場合、\(d^-\) は負 (間違った文書) です。微調整用に「クエリ関連ドキュメント」のポジティブ ペアと「クエリに関連しないドキュメント」のネガティブ ペアを構築することで、ドメイン固有の検索再現率を大幅に向上させることができます。
12.3.2 ベクトルデータベースとハイブリッド検索¶
ベクトル検索 (Dense Retrieval) のみに依存することには欠点があります。固有名詞、正確な数値、製品モデル、キーワード マッチングに影響されません。 Enterprise RAG は ハイブリッド検索 を採用する必要があります。 * ベクトル検索: 意味的な関連性をキャプチャします (例: 「リンゴ」と「果物」)。 * キーワード検索 (BM25): 完全一致を取得します (例: 製品 ID「A123-X」)。
Reciprocal Rank Fusion (RRF) を使用して両方の結果を再ランク付けすると、強みが組み合わされます。さらに、ベクトル データベースの選択 (Milvus、Pinecone、Weaviate) では、計算を大幅に削減するために、「year=2023」などの事前取得フィルタリングのメタデータ フィルタリングのパフォーマンスを考慮する必要があります。
12.4 エンジニアリング実装: 親子インデックス作成パイプラインの構築¶
このセクションでは、前述の核となる戦略である 親子インデックス作成 を実装します。 Python を使用して再利用可能な処理クラスを定義し、ドキュメントの読み込みからベクター ストレージまでのプロセス全体をシミュレートします。
12.4.1 依存関係¶
12.4.2 コアコードの内訳¶
パッケージ化された高レベル API を直接呼び出すのではなく、ロジックを分解してデータ フローを理解します。
import uuid
from typing import List, Dict, Any
from dataclasses import dataclass
@dataclass
class Document:
page_content: str
metadata: Dict[str, Any]
doc_id: str = None
class ParentChildIndexer:
"""
Implements Parent-Child Indexing strategy:
1. Parent Chunk: For storage and generation, preserves full context.
2. Child Chunk: For vectorization and retrieval, ensures semantic precision.
"""
def __init__(self, parent_chunk_size=1000, child_chunk_size=200):
self.parent_size = parent_chunk_size
self.child_size = child_chunk_size
# Simulate vector database (KV Store + Vector Store)
self.doc_store = {} # Store Parent documents: {doc_id: content}
self.vector_index = [] # Store Child vectors: [(vector, parent_doc_id)]
def process_documents(self, raw_docs: List[Document]):
"""Step 1: Data processing pipeline"""
for doc in raw_docs:
# Generate unique ID
if not doc.doc_id:
doc.doc_id = str(uuid.uuid4())
# 1. Store Parent Document (KV Store)
self.doc_store[doc.doc_id] = doc
# 2. Generate Child Chunks
child_chunks = self._create_child_chunks(doc)
# 3. Vectorize and build index
self._index_children(child_chunks, doc.doc_id)
def _create_child_chunks(self, parent_doc: Document) -> List[str]:
"""
Step 2: Chunking logic
Simplified to fixed character split here; production recommend RecursiveCharacterTextSplitter
"""
text = parent_doc.page_content
children = []
for i in range(0, len(text), self.child_size):
end = min(i + self.child_size, len(text))
children.append(text[i:end])
return children
def _index_children(self, children: List[str], parent_id: str):
"""Step 3: Vectorization logic (pseudocode)"""
for child_text in children:
# Simulate Embedding process
# vector = embedding_model.encode(child_text)
vector = [0.1, 0.2] # Placeholder
# Key: Store Parent ID in Child metadata
self.vector_index.append({
"vec": vector,
"text": child_text,
"parent_id": parent_id
})
def retrieve(self, query: str) -> List[Document]:
"""
Step 4: Retrieval logic (Small-to-Big)
Retrieve matching Child -> Return Parent
"""
# 1. Vector retrieval finds Top-K Children (simulated)
# top_children = vector_db.search(query)
# Note: Example only; should sort by vector similarity
top_child = self.vector_index[0] # Assume first match
# 2. Trace back to Parent
parent_id = top_child["parent_id"]
parent_doc = self.doc_store.get(parent_id)
print(f"Retrieved chunk: {top_child['text'][:20]}...")
print(f"Traced parent doc ID: {parent_id}")
return [parent_doc]
# --- Usage Example ---
indexer = ParentChildIndexer()
doc = Document(page_content="RAG system core lies in data quality..." * 50, metadata={"source": "manual.pdf"})
indexer.process_documents([doc])
result = indexer.retrieve("data quality")
12.4.3 プロのヒント¶
💡 ヒント: ID 管理は重要です 運用環境では、
doc_idは決定的である必要があります (例:hash(file_path + update_time))。そうしないと、ソース ファイルが更新されて再実行されると、ベクター データベースに削除できない大量の「ゾンビ チャンク」が蓄積されてしまいます。
12.5 性能と評価 (性能と評価)¶
RAG のパフォーマンスは単に「回答の精度」だけではなく、インデックスの構築コストや取得の待ち時間も含まれます。
12.5.1 評価指標¶
| メトリック | 説明 | 対象(参考) |
|---|---|---|
| ヒット率 (Recall@K) | 正解を含む上位 K 件の検索ドキュメントの割合 | > 85% |
| MRR (平均逆順位) | 検索リスト内の正しい文書の重み付けランキング | > 0.7 |
| 忠実さ | 生成された回答が、取得されたコンテキストを忠実に反映しているかどうか (幻覚防止) | > 90% (RAGAS ベース) |
12.5.2 ベンチマーク¶
サーバー (デュアル Xeon 6226R + 1x RTX 3090) で 10,000 ページの PDF ドキュメント (テキストと表が混在) をテストしました。
- 解析時間 (非構造化):
※CPUのみ:28分
- GPU アクセラレーション (OCR): 11 分 (2.5 倍の高速化)
- 取得レイテンシ (10M ベクトル):
- Pure Dense 取得: 9ms
- ハイブリッド取得 (密 + スパース + RRF): 45 ミリ秒
- 結論: ハイブリッド取得によりレイテンシーが増加しますが、高精度のシナリオ (契約レビューなど) では、36 ミリ秒の追加オーバーヘッドは十分に価値があります。
12.6 よくある誤解と落とし穴¶
誤解 1: 「PDF の解析には PyPDF で十分である」¶
多くの初心者は PDF の複雑さを過小評価しています。チャートや複数の列を含む収益レポートやマニュアルの場合、単純なテキスト抽出では重大な情報損失が発生します。プロジェクト開始時にレイアウト分析ツールを導入することをお勧めします。
誤解 2: 「チャンクは小さい方が良い」¶
チャンクが小さすぎると、検索のコサイン類似性は向上しますが、「文脈から外れ」ます。 LLM には、正しい答えを推測するための十分なコンテキストがありません。
誤解 3: 「メタデータを無視する」¶
メタデータ (ファイル名、ページ番号、発行日) なしでテキスト ベクトルのみを保存すると、時間フィルタリングやソース トレースができなくなり、システムの使いやすさが低下します。
章の概要¶
RAG の核となる競争力はデータ処理の精度にあります。この章では、RAG データ パイプラインの 3 つの主要なチェックポイントを解析しました。 1. 解析チェックポイント: 文書構造を視覚的なレベルで理解し、表や複数列の問題を解決する必要があります。 2. Chunking チェックポイント: 単一の固定分割を超えて移動します。親子インデックス付けまたはセマンティック Chunkingを採用して、取得精度とコンテキストの整合性のバランスをとります。 3. 取得チェックポイント: 微調整を通じてEmbeddingモデルをドメイン知識に適応させます。ハイブリッド検索を組み合わせて、ベクトル マッチングの制限を補います。
これらを導入すると、RAG システムは「使える」から「役に立つ」に進化します。
図 12-3: エンタープライズ RAG データ パイプライン アーキテクチャ —— 非構造化解析からハイブリッド検索までのフルフローの最適化を重視
さらに読む¶
ツールとフレームワーク * LlamaIndex: 現在最も先進的な RAG データ フレームワーク。豊富なデータローダーとインデックス作成戦略 (親子インデックス作成を含む)。 * RAGAS: RAG パイプラインのパフォーマンスを評価するためのフレームワーク。取得の精度と生成の忠実性に重点を置いています。
コアペーパー * Lewis et al. の 2020 Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks は、RAG の基礎的な研究です。 * Karpukhin らの Dense Passage Retrieval for Open-Domain Question Answering (DPR) は、現代のデュアルタワー ベクトル検索の基礎を築きました。


