前回の記事でこう書いた。「前処理の品質が精度の天井を決める。Difyはその入れ物に過ぎない」と。ではその前処理を自前でどう設計するのか。問いはシンプルだ。PDFが増えるたびに手作業でアップロードしていては自動化の意味がない。差分はどう検知するか。処理が途中で失敗したらどうリカバーするか。「n8nが成功と報告した」だけでは不十分で、本当に全チャンクが書き込まれたかをどう検証するか。この記事はその3つの設計を、実際に稼働しているシステム(現在3,642チャンク・20論文取り込み済み)の実装記録として残す。
3ワークフローに分割した理由
最初は1つのワークフローで差分検知からembedding生成まで書こうとした。試してすぐやめた。理由は3つだ。
1つ目、「どこで失敗したか」が追えない。差分検知・PDF変換・チャンク分割・embedding生成がひとつのフローに連なっていると、失敗ログを見ても原因箇所の特定に時間がかかる。2つ目、1論文の処理失敗が他の論文の取り込みを止める。巨大なPDFがタイムアウトしたとき、残り19論文も道連れになる。3つ目、n8nのSplit in Batchesをネストすると2件目以降のループがリセットされない既知の制限がある(詳細は前の記事の問題7を参照)。この問題を避けるには、内側のループをサブワークフローに分離するしかなかった。
結果として3つのワークフローに分けた。
WORKFLOW 1
Zotero論文同期
差分検知専用。PDF処理は一切しない軽量設計。毎日2:50 JSTにスケジュール実行。Zoteroライブラリの追加・削除を検知してDBへ反映する
WORKFLOW 2
論文取り込みリカバリー
27ノード構成の本体処理。毎日3:00 JSTに実行。pending・failedの論文を拾ってPDF変換→チャンク分割→embedding→DB書き込みまで行う
WORKFLOW 3(サブ)
チャンク埋め込みサブ
5ノード構成。Workflow 2からExecute Workflowで呼び出す。1論文分のチャンクをOllamaに送り、ベクトルを生成して返す。ネストループ問題の回避策でもある
差分検知と安全装置(Workflow 1)
Workflow 1はシンプルだ。8ノードで動く。処理の流れは順番に追える。
1
Zotero APIで全アイテム取得
GET /users/21437214/items でライブラリ全件を取得する。レスポンスには各論文の zotero_key(一意識別子)が含まれる
↓
2
DBの既存 zotero_key 一覧を取得
SELECT zotero_key FROM documents でDB側の現状を取得する。PostgreSQLノードで実行
↓
3
突き合わせてINSERT / DELETE対象を判定
Codeノードで差分計算。Zotero側にあってDBにない → INSERT対象。DB側にあってZotero側にない → DELETE対象。安全装置の判定もここで行う
↓
4
INSERT / DELETE を実行
新規 → INSERT … processing_status=‘pending’ で登録。削除 → DELETE FROM documents WHERE zotero_key = …。knowledge_chunksは ON DELETE CASCADE で自動削除される
このフローで最重要なのは、ステップ3の安全装置だ。
安全装置 — Zotero APIが0件を返した場合はDELETE判定を一切行わない
Zotero APIが一時障害で空レスポンスを返すことがある。安全装置なしでこれを受け取ると、「Zotero側に論文が0件」→「DB側の全論文をDELETE対象と判定」という誤作動が起きる。knowledge_chunks は ON DELETE CASCADE のため、documentsのDELETEに連動して3,642チャンクが全消滅する。
対処は単純だ。APIレスポンスが0件のときはDELETE処理をスキップして早期終了する。この1チェックがないと、API障害のたびにデータを全損する。
PDF取り込みパイプライン(Workflow 2の核心)
Workflow 2が本体だ。27ノード構成で、PDF取得からembedding生成まで一貫して処理する。
📁
NASのPDFファイル
Zotero子アイテムAPIでPDF添付ファイルのパスを特定。ReadWriteFileノードでNASからバイナリを読み込む
↓ ReadWriteFileノード
📄
MinerU(POST /file_parse)
multipart/form-dataでPDFを送信。タイムアウト30分。学術PDF構造を解析してMarkdownテキストに変換する
↓ 30分タイムアウト
✂️
チャンク分割(Codeノード)
1000字・重複150字・段落境界優先。NUL文字(\x00)をPostgresがTEXT型に格納できないため事前除去(text.replace(/\x00/g, ‘’))
↓ NUL文字除去 + expected_chunk_count記録
🤖
チャンク埋め込みサブ呼び出し(Workflow 3)
Execute WorkflowノードでWorkflow 3を同期呼び出し。Ollama(qwen3-embedding:8b)でベクトルを生成して返す。タイムアウト600秒
↓ Ollama qwen3-embedding:8b(4096次元)
🗄️
knowledge_chunksテーブルへ書き込み
INSERT INTO knowledge_chunks … で1チャンクずつ書き込む。チャンクテキスト・ベクトル・document_idを格納
↓
✅
完全性検証(expected vs actual)
チャンク分割直後に記録した expected_chunk_count と、実際のDB件数(SELECT COUNT(*))を突き合わせ。一致で completed、不一致で failed
MinerUについて
一般的なPDFパーサーは2カラムレイアウトを1行おきに断片化する。左カラム1行・右カラム1行・左カラム2行目・右カラム2行目という順で読んでしまうからだ。結果としてチャンクは無意味な断片の集合になる。MinerUは学術PDF向けに最適化されており、数式・表・2カラム構造を正しくMarkdownに変換する。backend=pipeline オプションがGPUなし環境での必須指定だ。GPUが使えない環境でデフォルトの推論バックエンドを指定すると、処理が開始されずにエラーになる。
リトライと完全性検証
ステータス状態遷移
Workflow 2の設計の核心は、この状態遷移にある。
pending
Workflow 1が新規論文を検知したときに登録するステータス。Workflow 2は毎日このステータスを持つ論文をすべて処理対象にする
↓ 取り込み成功 + chunk数一致
completed
完全性検証を通過した論文のみこのステータスになる。以降Workflow 2は処理対象にしない
↓ chunk数不一致 または 処理中エラー
failed
failure_count をインクリメント。翌日のWorkflow 2実行時に再びpickupされる。一時的な障害(Ollamaのタイムアウト等)は翌日に自動リカバーする
↓ failure_count が3以上
permanently_failed
3回連続失敗で確定。以後Workflow 2の処理対象から外れる。LINE Messaging APIで通知を送り、人間が手動確認する
完全性検証の必要性
n8nは「エラーが発生しなかった」ことを成功と判断する。実際に何チャンク書き込まれたかは確認しない。この問題は前の記事の問題1で詳述したが、部分成功を黙ってcompletedにする挙動は本番では危険だ。
対処として、チャンク分割直後に UPDATE documents SET expected_chunk_count = N で期待チャンク数を先に記録する。全チャンク書き込み後に SELECT COUNT(*) FROM knowledge_chunks WHERE document_id = $1 で実際のDB件数を取得し、期待値と突き合わせる。一致した場合だけ completed に更新する。n8nの成功を信用せず、実データで検証する設計だ。
Decide Statusノードの判定ロジック
expected_chunk_countとSELECT COUNTの結果を比較する。一致 → processing_status = ‘completed’。不一致 → failure_count = failure_count + 1、failure_count >= 3 なら processing_status = ‘permanently_failed’、そうでなければ processing_status = ‘failed’。このロジックが完全性検証の核心だ。
社内ドキュメントへの応用ポイント
このシステムはZotero(論文ライブラリ)を起点に設計しているが、差分検知・リトライ・完全性検証という骨格はデータソースが変わっても流用できる。社内文書に置き換えるときの対応表だ。
この実装
Zotero API(変更検知)
→ SharePoint API / Google Drive APIでの差分検知に置き換え可能。更新日時やETagで変更を検知する
この実装
NASからPDF読み込み
→ SharePointライブラリからのダウンロードURLに置き換え。n8nのHTTP RequestノードでBlobを取得する
この実装
MinerU(PDF変換)
→ そのまま流用可能。社内PDFも2カラム・表・ヘッダーを正しく処理する。GPUなし環境でも動作する
この実装
LINE通知
→ Microsoft Teams Webhookまたは Slack Incoming Webhookに置き換え。n8nにはTeams・Slackの公式ノードがある
この実装
Ollama(ローカルLLM)
→ Azure OpenAI / OpenAI APIへの差し替えも可。Workflow 3のOllamaエンドポイントをAPIエンドポイントに変更するだけだ
差分検知・リトライ設計・完全性検証という3つの設計パターンは、Zoteroであっても社内の文書管理システムであっても変わらない。データの入口が変わるだけで、パイプラインの骨格は流用できる。
参考資料