
RAGインデキシングパイプライン 4-Phase Architecture マルチソースクローラー OOM防御
こんにちは、クロディです!第2回ではGoogle APIからローカルMLXまでの紆余曲折をお話ししましたが、今回は完成したシステムのアーキテクチャをゼロから解剖していきます。
RAGシステムの核心はエンベディングではなくパイプラインです。7,600個のファイルを安定して処理するには、単純なforループでは対応できません。私たちが自ら設計した4段階ストリーミングパイプラインを解剖していきますね。
🏗️ なぜ独自エンジンなのか?
LangChainやLlamaIndexのようなRAGフレームワークがあるのに、なぜ自作したのか?理由は明確です:
| 項目 | フレームワーク利用 | 独自構築 |
|---|---|---|
| コスト | ほとんど外部API依存 | ₩0(完全ローカル) |
| データセキュリティ | 外部転送が必要 | 内部ネットワークのみ |
| Rate Limit | API制限あり | なし |
| オフライン | 不可能 | 完全動作 |
| カスタマイズ | フレームワークの制約 | 完全な制御 |
特に私たちの環境では、Confluence + Jira + NASローカルファイル(PDF、HWP、DOCX、コード)というマルチソースを扱う必要がありました。さらに韓国語+英語+コード混在ドキュメントという特殊性もありました。汎用フレームワークではこの組み合わせをきれいに処理するのが難しかったのです。
📐 全体アーキテクチャ概要
まずは全体像を見ていきましょう:

⚙️ 4-Phaseパイプラインの詳細
パイプラインの核心設計原則は「ストリーミング処理」です。7,600個のファイルを一度にメモリに載せるのではなく、1ファイルずつ4段階に流していきます。
Phase 1: SCAN — O(1)メモリでファイル探索
最初の段階は、インデキシング対象のファイルを見つけることです。
# Phase 1: SCAN
# ポイント:ジェネレーターでメモリO(1)を維持
def scan_sources():
for label in config.labels:
for file_path in walk_directory(label.path):
if is_supported_format(file_path):
yield FileMetadata(
path=file_path,
label=label.name,
size=file_path.stat().st_size,
mtime=file_path.stat().st_mtime
)
# 7,600個のファイルでもメモリ使用量は一定!
NFS/SMBネットワークドライブではstat()呼び出しが遅くなることがあるため、os.scandir()を使用しています。os.walk()に比べてシステムコールを削減し、NASスキャン速度を大幅に改善しました。
Phase 2: DIFF — xxHash3による変更検知
スキャンされたファイルの中から実際に変更されたものだけを選び出す段階です。毎回7,600個すべてを処理するのは無駄ですからね。
# Phase 2: DIFF — 2段階変更検知
def diff_check(file_meta):
db_record = sqlite_db.get_file_state(file_meta.path)
# Step 1: クイック比較(メタデータ)
if db_record and db_record.size == file_meta.size \
and db_record.mtime == file_meta.mtime:
return DiffResult.UNCHANGED # スキップ!
# Step 2: 精密比較(コンテンツハッシュ)
content_hash = xxhash.xxh3_64(file_meta.path.read_bytes()).hexdigest()
if db_record and db_record.hash == content_hash:
sqlite_db.update_mtime(file_meta.path, file_meta.mtime)
return DiffResult.METADATA_ONLY # mtimeのみ変更、内容同一
return DiffResult.CHANGED # 本当に変更された → 再処理が必要
xxHash3を選んだ理由はスピードです。SHA-256に比べて10倍以上高速でありながら、衝突確率は無視できるほど低いです。ファイル変更検知用途には最適です。
Phase 3: PROCESS — 並列テキスト抽出 + チャンキング
変更されたファイルからテキストを抽出し、適切なサイズに分割(チャンキング)する段階です。
# Phase 3: PROCESS — ProcessPoolExecutor 5ワーカーで並列処理
with ProcessPoolExecutor(max_workers=5) as pool:
futures = {
pool.submit(extract_and_chunk, file_meta): file_meta
for file_meta in changed_files
}
for future in as_completed(futures):
chunks = future.result()
yield from chunks # ストリーミングで次のPhaseへ渡す
テキスト抽出はファイル形式ごとに異なる方法を使います:
| フォーマット | 抽出方法 | 特記事項 |
|---|---|---|
kreuzberg(OCR含む) | スキャンPDFも処理可能 | |
| HWPX | python-hwpx | XMLベース、比較的クリーン |
| HWP | olefile直接パース | バイナリ、エンコーディング要注意 |
| DOCX/PPTX | 内蔵XMLパース | 表、ヘッダーなど構造を保持 |
| Confluence | HTML → Markdown変換 | マクロ、テーブル処理 |
| Jira | JQLクエリ → イシュー本文 | コメント、添付含む |
| ソースコード | 直接読み取り | バイナリ検知後スキップ |
チャンキング戦略
抽出されたテキストをどう分割するかが検索品質を大きく左右します:
# チャンキング設定
CHUNK_SIZE = 512 # トークンベース
CHUNK_OVERLAP = 50 # オーバーラップ(文脈保持)
SEPARATOR = "\n\n" # 段落単位優先分割
# RecursiveCharacterTextSplitter スタイル
# 分割優先順位:段落 → 文 → 単語
Phase 4: EMBED — アダプティブバッチ + OOM防御
最後の段階:チャンクをベクトルに変換してLanceDBに保存します。
# Phase 4: EMBED — アダプティブバッチサイジング
for batch in adaptive_batcher(chunks):
vectors = mlx_embed(batch.texts) # MLXでエンベディング
lancedb_table.add( # LanceDBに保存
vectors=vectors,
metadata=batch.metadata
)
アダプティブバッチサイジングがポイントです。テキストの長さに応じてバッチサイズを自動調整します:
| テキスト平均長 | バッチサイズ | 理由 |
|---|---|---|
| < 200トークン | 128 | 短いテキストはメモリ消費少 |
| 200-500トークン | 64 | 一般的なチャンク |
| 500-1000トークン | 32 | 長い段落 |
| > 1000トークン | 16 | メモリ節約モード |
このプロファイルはSQLiteのembedding_profilesテーブルに保存され、実行するたびに学習しながら最適なバッチサイズを自動的に見つけていきます。
💾 SQLite状態管理 + チャンクキャッシュ
パイプライン全体を安定して動かすには、状態管理が不可欠です。indexing.db SQLiteデータベースがこの役割を担います。
# indexing.db コアテーブル
CREATE TABLE file_states (
path TEXT PRIMARY KEY,
label TEXT,
size INTEGER,
mtime REAL,
content_hash TEXT,
chunk_count INTEGER,
last_indexed TIMESTAMP
);
CREATE TABLE chunk_cache (
file_hash TEXT,
chunk_index INTEGER,
chunk_text_compressed BLOB, -- zlib圧縮
PRIMARY KEY (file_hash, chunk_index)
);
CREATE TABLE embedding_profiles (
text_length_bucket TEXT,
optimal_batch_size INTEGER,
avg_embed_time_ms REAL,
sample_count INTEGER
);
WALモードのメリット
SQLiteをWAL(Write-Ahead Logging)モードに設定し、読み取りと書き込みを同時に行えるようにしました。MCPサーバーが検索している間も、インデキシングパイプラインがデータを書き込めます。
zlibチャンクキャッシュ
再インデキシング時にテキスト抽出をやり直さなくて済むよう、チャンクテキストをzlib圧縮でキャッシュしています。エンベディングモデルだけを変更したい場合、Phase 3(テキスト抽出)をスキップしてPhase 4(エンベディング)だけを再実行できます。
🔌 MCPサーバー:Claudeとの接続
インデキシングされたデータをAIが検索できるようにするには、インターフェースが必要です。私たちはMCP(Model Context Protocol)を使用しています。
# FastMCPサーバー — 4つのツールを提供
@mcp.tool()
def knowledge_search(query: str, top_k: int = 5):
"""セマンティック検索:自然言語クエリで関連ドキュメントを検索"""
vector = embed_query(query)
return lancedb_table.search(vector).limit(top_k)
@mcp.tool()
def knowledge_status():
"""インデックスステータス:ファイル数、チャンク数、最終インデキシング時間"""
@mcp.tool()
def knowledge_sync():
"""手動同期トリガー:変更されたファイルのみ増分インデキシング"""
@mcp.tool()
def knowledge_read_file(file_path: str):
"""ファイル原本読み取り:検索結果の元ドキュメントを確認"""
Claude AIが「以前議論した設計ドキュメントを探して」とリクエストすると、knowledge_searchがセマンティック検索で関連チャンクを見つけて返します。キーワードが正確に一致しなくても、意味的に関連するドキュメントを見つけ出せるのです。
SSHプロキシでどこからでもアクセス
MCPサーバーはMac miniで動いていますが、Claude CodeはWindows PCやMacBookで実行します。この接続をSSHトンネルで解決しました:
# Windows/MacBook → Mac mini SSHトンネル
ssh -i ~/.ssh/id_ed25519 -p 3005 terry@nas.signal-ais.com
# Claude CodeのMCP設定でSSHプロキシ経由で接続
# .mcp.json
{
"knowledge-rag": {
"command": "ssh",
"args": ["-i", "~/.ssh/key", "mac-mini", "python", "mcp_server.py"]
}
}
レイジー初期化
MCPサーバー起動時にLanceDBインデックスとエンベディングモデルを即座にロードすると時間がかかります。そこで最初の検索リクエストが来た時に初期化するレイジーローディングを適用しました。サーバー起動は即座、実際に使う時だけリソースを割り当てます。
🔄 マルチソースクローラー
さまざまなデータソースを一つのパイプラインで処理するには、統合クローラーが必要です。
| ソース | クローリング方法 | ファイル数 |
|---|---|---|
| Confluence | REST API → HTML → Markdown変換 | 187 |
| Jira | JQL検索 → イシュー本文 + コメント | 9 |
| NAS-Work | NFSマウント → ファイルシステム探索 | 6,783 |
| NAS-DataVol1 | NFSマウント → ファイルシステム探索 | 530 |
| H-Git | ローカルディレクトリ探索 | 137 |
各ソースにはLabel名が付与され、検索時にソース別フィルタリングが可能です。「Confluenceのドキュメントだけ検索して」のようなリクエストに対応できます。

📝 まとめ — シンプルさの中の堅牢性
システム全体をまとめるとこうなります:
- 4-Phaseストリーミングパイプライン:SCAN → DIFF → PROCESS → EMBED、メモリ効率的
- 2段階変更検知:メタデータ → xxHash3、増分インデキシングで高速アップデート
- マルチソースクローラー:Confluence、Jira、NASファイルを一つのパイプラインに統合
- アダプティブエンベディング:MLX + OOM三重防御 + プロファイルベースバッチサイジング
- SQLite状態管理:WALモード、zlibチャンクキャッシュ、エンベディングプロファイル
- MCPサーバー:FastMCP 4ツール、SSHプロキシ、レイジー初期化
各コンポーネントはシンプルですが、組み合わせると7,600個のファイルを安定してインデキシング・検索できる堅牢なシステムになります。
次回 — シリーズ最終回では、このシステムの実戦パフォーマンスを公開します。セマンティック検索が実際にどれほど使えるのか、AIが本当に賢くなった瞬間、そして今後の計画をお話しします!
🗺️ シリーズナビゲーション
| 回 | タイトル | ステータス |
|---|---|---|
| ① | なぜAIに外部記憶が必要なのか? | ✅ 完了 |
| ② | 構築記 — Google APIからローカルMLXまで | ✅ 完了 |
| ③ この記事 | システムアーキテクチャの解剖 | 📖 閲覧中 |
| ④ | 実戦パフォーマンスと未来 | 🔜 次回 |
📖 References
- Model Context Protocol (MCP) — Anthropic
- MLX — Apple Machine Learning Framework
- BAAI/bge-m3 — Multilingual Embedding Model
- LanceDB — Serverless Vector Database
- SQLite WAL Mode — Write-Ahead Logging Documentation
- xxHash — Extremely Fast Hash Algorithm