セクション1: はじめに:Google Drive × Supabase × n8nでRAGを構築する意義
RAG(Retrieval-Augmented Generation)という言葉を耳にする機会が増えました。これは、大規模言語モデル(LLM)が持つ一般的な知識だけでなく、特定の外部データソースから情報を「検索(Retrieval)」し、その情報を基に「生成(Generation)」を行うことで、より正確で最新の、そして企業固有の文脈に沿った回答を可能にする技術です。私自身、この技術の可能性に強く惹かれ、日々の業務や情報管理にどう活かせるかを常に模索しています。
Google Driveに蓄積されたPDFファイルは、多くの企業や個人にとって貴重な情報資産です。しかし、これらのファイルは通常、LLMが直接アクセスして利用することはできません。RAGを導入することで、Google Drive内のPDFをLLMの知識ベースとして活用できるようになります。これにより、例えば社内規定や製品マニュアル、過去の議事録など、膨大なドキュメントの中から必要な情報を瞬時に引き出し、質問応答や要約に利用するといった、これまでは難しかった高度な情報活用が現実のものとなります。
本記事では、このRAGパイプラインを構築するために、ノーコード・ローコード自動化ツールのn8nと、スケーラブルなバックエンドサービスであるSupabaseを組み合わせる手法に焦点を当てます。n8nは、Google Driveのファイル更新をトリガーに、PDFのテキスト抽出、チャンキング、そしてOpenAIの埋め込み生成までの一連のプロセスを自動化します。そして、生成された埋め込みデータはSupabaseのpgvectorに保存され、RAGの基盤となります。この組み合わせにより、開発者は複雑なインフラ構築やコード記述に時間を費やすことなく、効率的かつ堅牢なRAGシステムを構築できるのです。私自身、この自動化の利便性には目を見張るものがあり、一度構築すれば、あとはGoogle DriveにPDFをアップロードするだけで、自動的にRAGデータベースが更新されていくという体験は、まさに未来の情報管理だと感じています。
セクション2: 環境準備:Xserver VPSでn8nのセットアップとdocker-compose.yml編集
Xserver VPSでのn8nインストール状況確認
Xserver VPSでn8nを運用されているとのこと、素晴らしい選択です。まずは、現在のn8nのインストール状況と、docker-compose.ymlファイルの場所を確認することから始めましょう。これは、今後の設定変更やトラブルシューティングの基礎となる非常に重要なステップです。私自身も、新しい環境で作業する際には必ずこの初期診断から入るようにしています。
まず、SSHでXserver VPSに接続します。ターミナルを開き、以下のコマンドを実行してください。
ssh root@[あなたのXserver VPS IP]
接続後、docker-compose.ymlファイルを探します。n8nのDocker版がインストールされている場合、このファイルは通常、n8nのコンテナを管理するために使用されます。以下のコマンドで、システム全体からdocker-compose.ymlファイルを検索できます。
find / -name "docker-compose.yml" 2>/dev/null | head -10
このコマンドは、見つかったdocker-compose.ymlファイルの最初の10件を表示します。多くの場合、n8n関連のファイルは/root/n8n/や/root/docker-compose/n8n/、あるいは/home/user/docker-compose/n8n/のようなディレクトリに配置されています。念のため、これらの一般的な場所も直接確認してみましょう。
ls -la /root/n8n/
ls -la /root/docker-compose/n8n/
ls -la /home/user/docker-compose/n8n/
次に、n8nコンテナが現在稼働しているかを確認します。Docker版のn8nが正しく動作していれば、docker psコマンドでその情報が表示されます。
docker ps -a | grep n8n
もしdocker psでn8nコンテナが見つからない場合でも、Node.js版のn8nが直接プロセスとして動いている可能性もあります。その場合は、以下のコマンドでNode.jsプロセスを確認します。
ps aux | grep -v grep | grep n8n
Docker版とNode.js版にはそれぞれ特徴がありますが、RAGワークフローのように外部ライブラリを多用し、安定した運用を目指す場合は、Docker版が強く推奨されます。Docker版は環境の分離が容易で、依存関係の管理がシンプルになるため、私自身も常にDockerでの運用を基本としています。もしNode.js版が稼働している場合は、後述の「docker-compose.ymlが見つからない場合」のセクションを参考に、Docker版への移行を検討してください。
docker-compose.ymlの編集とn8n再起動手順
前述の診断でdocker-compose.ymlファイルが見つかり、n8nがDockerコンテナとして稼働していることが確認できたと仮定して、次にそのファイルを編集し、必要な環境変数を追加する手順を説明します。これは、n8nのCode Nodeで外部ライブラリ(pdf-parseやlangchain、openaiなど)を使用するために不可欠な設定です。私自身も、Code Nodeで外部モジュールを使うたびにこの設定を忘れてエラーに遭遇し、そのたびに「またか!」と頭を抱えてきました。
まず、見つかったdocker-compose.ymlファイルのあるディレクトリに移動します。例えば、/root/docker-compose/compose/n8nに見つかった場合は、以下のコマンドを実行します。
cd /root/docker-compose/compose/n8n
次に、nanoなどのテキストエディタでdocker-compose.ymlファイルを開きます。
nano docker-compose.yml
ファイルを開いたら、services: n8n: environment:セクションを探してください。このセクションに、以下の環境変数を追加します。
version: '3.8'
services:
n8n:
image: docker.n8n.io/n8n/n8n:latest
restart: always
ports:
- "5678:5678"
environment:
- GENERIC_TIMEZONE=Asia/Tokyo
- WEBHOOK_URL=https://...
# ↓ ここに3行追加
- NODE_FUNCTION_ALLOW_BUILTIN=*
- NODE_FUNCTION_ALLOW_EXTERNAL=pdf-parse,langchain,openai
- EXTRA_NODE_MODULES=pdf-parse langchain openai
volumes:
- ./data:/home/node/.n8n
追加する環境変数の意味は以下の通りです。
NODE_FUNCTION_ALLOW_BUILTIN=*: n8nのCode Node内でNode.jsの組み込みモジュール(fs,pathなど)をすべて許可します。NODE_FUNCTION_ALLOW_EXTERNAL=pdf-parse,langchain,openai: Code Node内でpdf-parse、langchain、openaiといった外部モジュールのrequire()を許可します。これはn8nのVM2サンドボックスのセキュリティ制限を緩和するために必要です。EXTRA_NODE_MODULES=pdf-parse langchain openai: n8nコンテナ起動時に、これらのNode.jsモジュールを自動的にインストールさせます。これにより、Code Nodeからこれらのライブラリを呼び出せるようになります。
編集が完了したら、ファイルを保存してエディタを終了します(nanoの場合: Ctrl+O → Enter → Ctrl+X)。
変更をn8nコンテナに反映させるためには、Dockerコンテナを再起動する必要があります。以下のコマンドを実行してください。
docker-compose down
docker-compose up -d
docker-compose downは現在のコンテナを停止・削除し、docker-compose up -dはdocker-compose.ymlの定義に基づいて新しいコンテナをバックグラウンドで起動します。この際、EXTRA_NODE_MODULESで指定したライブラリがインストールされるため、起動に数分かかる場合があります。
再起動後、n8nコンテナのログを確認して、エラーなく起動していることを確認します。特に、pdf-parseやlangchain、openaiのインストールに関するエラーがないか注意深く見てください。
docker-compose logs -f n8n
ログに「n8n ready on 0.0.0.0, port 5678」や「Server started successfully」のようなメッセージが表示されれば、設定は成功です。これで、n8nのCode Nodeからこれらの外部ライブラリを利用する準備が整いました。この一連の作業は、RAGパイプラインの安定稼働のために非常に重要な土台となります。
セクション3: n8nワークフロー構築:Google DriveからSupabaseまでの完全自動化
Google Driveトリガーとファイルダウンロードノード設定
RAGパイプラインの最初のステップは、Google DriveにアップロードされたPDFファイルを検知し、n8nワークフローに引き渡すことです。ここでは、Google Driveトリガーノードとファイルダウンロードノードの設定方法を詳しく解説します。私自身、この自動化の入り口がスムーズに設定できると、その後のワークフロー構築が非常に楽になることを実感しています。
まず、n8nワークフローエディタを開き、「+」ボタンをクリックして新しいノードを追加します。検索ボックスに「Google Drive」と入力し、「Google Drive Trigger」ノードを選択してください。
Google Drive Triggerノードの設定:
- Credentials (認証情報):
- 初めてGoogle Driveノードを使用する場合、OAuth2認証情報を設定する必要があります。「New Credential」をクリックし、Googleアカウントを連携させてください。n8nがGoogle Driveにアクセスするための許可を求められますので、指示に従って承認します。
- Trigger On (トリガーイベント):
- 「
Created or updated」を選択します。これにより、指定したフォルダに新しいPDFファイルがアップロードされたり、既存のファイルが更新されたりした際にワークフローが起動するようになります。
- 「
- Folder (監視フォルダ):
- RAGの対象となるPDFファイルが保存されるGoogle Drive上の特定のフォルダを指定します。例えば、「
/PDFs」のようなパスを入力します。このフォルダは事前にGoogle Drive上に作成しておく必要があります。
- RAGの対象となるPDFファイルが保存されるGoogle Drive上の特定のフォルダを指定します。例えば、「
- File Types (ファイルタイプ):
- 「
PDF」を選択します。これにより、PDFファイルのみがトリガーの対象となり、他の種類のファイルによる不要なワークフロー実行を防ぐことができます。
- 「
- Polling Interval (ポーリング間隔):
- 「
60」秒など、Google Driveの変更をチェックする間隔を設定します。短すぎるとAPI呼び出しが増え、長すぎるとワークフローの起動が遅れます。通常は60秒から300秒程度が適切です。
- 「
次に、トリガーされたPDFファイルをn8nワークフロー内で処理するために、そのバイナリデータをダウンロードするノードを追加します。Google Drive Triggerノードの後に「+」ボタンをクリックし、「Google Drive」ノードを追加します。
Google Driveノード(ダウンロードモード)の設定:
- Credentials (認証情報):
- Google Drive Triggerノードで設定した既存のOAuth2認証情報を選択します。
- Mode (モード):
- 「
Download」を選択します。これにより、指定されたファイルをダウンロードするモードになります。
- 「
- File ID (ファイルID):
- Google Drive Triggerノードの出力から、ダウンロードしたいPDFファイルのIDを取得します。式エディタ(
=アイコン)をクリックし、「{{ $node["Google Drive Trigger"].data.file_id }}」と入力します。これは、トリガーされたファイルのユニークな識別子です。
- Google Drive Triggerノードの出力から、ダウンロードしたいPDFファイルのIDを取得します。式エディタ(
- Binary Property Name (バイナリプロパティ名):
- ダウンロードしたバイナリデータが格納されるプロパティ名を指定します。デフォルトの「
data」で問題ありません。このプロパティ名が、次のPDF抽出ノードへの入力となります。
- ダウンロードしたバイナリデータが格納されるプロパティ名を指定します。デフォルトの「
これらの設定により、Google DriveにPDFがアップロードされるたびに、n8nがそのファイルを自動的にダウンロードし、次の処理ステップへバイナリデータとして渡す準備が整います。このシームレスな連携は、RAGパイプラインの自動化において非常に強力な基盤となります。
PDF抽出:Community Node『PDF Parse』の導入と活用
PDFファイルからテキストを抽出する工程は、RAGパイプラインの品質を左右する重要な部分です。以前のやり取りでpdf-parseライブラリをCode Nodeで直接利用しようとしましたが、n8nのVM2サンドボックスの制約により、外部モジュールの読み込みに課題が生じました。このような状況において、最も確実で推奨される解決策が、n8nのCommunity Nodeである『PDF Parse』の利用です。私自身も、公式ノードで対応できない機能や、外部ライブラリの導入が難しい場合には、積極的にCommunity Nodeを活用するようにしています。
Community Nodeの利点:
- 環境依存性の低減:
docker-compose.ymlの編集やnpm installといった複雑な手順なしに、n8nのGUIから直接インストール・利用が可能です。 - 安定性: n8nのコミュニティによってメンテナンスされており、公式ノードと同様に安定した動作が期待できます。
- シンプルなインターフェース: ノードとして提供されるため、Code Nodeで複雑なコードを書く必要がなく、直感的に設定できます。
Community Node『PDF Parse』のインストール手順:
- n8nの左側メニューにある「Settings」をクリックします。
- 「Community Nodes」タブを選択します。
- 検索バーに「
pdf」と入力し、「n8n-nodes-pdf-parse」を見つけます。 - ノードをクリックし、「Install」ボタンをクリックします。
- インストールが完了すると、「
Community node installed successfully n8n will restart automatically」というメッセージが表示され、n8nが自動的に再起動します(通常2〜3分かかります)。
n8nが再起動したら、ワークフローエディタに戻ります。Google Drive Downloadノードの後に「+」ボタンをクリックし、検索バーに「PDF Parse」と入力して、新しくインストールされたCommunity Nodeの「PDF Parse」ノードを追加します。
PDF Parseノードの設定:
- Input Data Field (入力データフィールド):
- 前のGoogle Drive Downloadノードから渡されるバイナリデータが格納されているプロパティ名を指定します。通常は「
data」です。
- 前のGoogle Drive Downloadノードから渡されるバイナリデータが格納されているプロパティ名を指定します。通常は「
- Binary Property Name (バイナリプロパティ名):
- 上記と同様に「
data」を指定します。
- 上記と同様に「
これで、PDF ParseノードはGoogle DriveからダウンロードされたPDFのバイナリデータを受け取り、自動的にテキストを抽出して、その結果を次のノードにJSON形式で渡します。出力されるJSONには、抽出されたテキストがtextプロパティとして含まれます。このノードを使うことで、pdf-parseライブラリの導入に関するトラブルを回避し、PDF抽出プロセスを非常にスムーズに進めることができます。私自身、この手の問題に直面した際には、まずCommunity Nodeの存在を確認するようにしています。
テキストチャンキング:LangChain RecursiveCharacterTextSplitterの実装
PDFから抽出した生テキストは、そのままでは大規模言語モデル(LLM)の入力として長すぎたり、文脈が途切れてしまったりする可能性があります。そこで必要となるのが「チャンキング(Chunking)」、つまりテキストを意味のある小さな塊に分割する処理です。RAGの精度は、このチャンキングの質に大きく依存します。ここでは、LangChainのRecursiveCharacterTextSplitterをn8nのCode Node内で実装する方法を解説します。私にとって、このチャンキングのパラメータ調整はRAG構築の醍醐味の一つです。
チャンキングの重要性:
- LLMのトークン制限: ほとんどのLLMには入力トークン数に制限があります。チャンキングにより、この制限内に収まるようにテキストを分割します。
- 関連性の向上: 質問に対する回答に必要な情報が、無関係な情報と混ざり合っていると、LLMの回答精度が低下します。意味のある単位で分割することで、検索時の関連性が向上します。
- コスト最適化: 不要なテキストをLLMに渡さないことで、API利用コストを削減できます。
推奨パラメータ:
調査データに基づき、バランスの取れたチャンキングパラメータとして、chunkSize=500、overlap=100が推奨されます。これは、一般的なドキュメントにおいて、十分なコンテキストを保ちつつ、重複によって文脈の連続性を確保するための値です。
chunkSize: 各チャンクの最大文字数。500文字は、多くのLLMの入力に適したサイズであり、意味的なまとまりを保ちやすい長さです。chunkOverlap: チャンク間の重複文字数。100文字の重複は、チャンクの境界で文脈が途切れることを防ぎ、次のチャンクとの連続性を高めます。
Code Node内でのJavaScript実装例:
PDF Parseノードの後にCode Nodeを追加し、以下のJavaScriptコードを貼り付けます。このコードは、langchain/text_splitterモジュールを利用してテキストをチャンキングします。docker-compose.ymlでlangchainモジュールの利用を許可し、EXTRA_NODE_MODULESでインストールされていることを確認してください。
// Code Node #1: PDF抽出 + セマンティックチャンキング
const { RecursiveCharacterTextSplitter } = require('langchain/text_splitter');
try {
// PDF Parse ノードから抽出済みのテキストを取得
const inputData = this.getInputData()[0];
const extractedText = inputData.json.text; // Community PDF Parse Nodeの出力
if (!extractedText || extractedText.length < 10) {
return [{
error: 'PDF extraction failed or text is too short',
textLength: extractedText?.length || 0
}];
}
// メタデータ抽出(PDF Parseノードから取得できる情報があれば利用)
// Community PDF Parse Nodeは詳細なメタデータを提供しないため、ここでは簡易的なものに留める
const metadata = {
originalFileName: inputData.json.fileName || 'unknown',
extractedAt: new Date().toISOString()
};
// セマンティックチャンキング
const splitter = new RecursiveCharacterTextSplitter({
separators: ["\n\n", "\n", ". ", " ", ""], // 分割優先順位
chunkSize: 500, // 500字
chunkOverlap: 100 // 100字オーバーラップ
});
const chunks = await splitter.splitText(extractedText);
// チャンクに詳細情報を付与
const chunksWithMetadata = chunks.map((chunk, idx) => ({
content: chunk,
chunkIndex: idx,
totalChunks: chunks.length,
wordCount: chunk.split(/\s+/).length,
charCount: chunk.length,
...metadata // 簡易メタデータを各チャンクに付与
}));
// 出力
return [{
json: {
chunks: chunksWithMetadata,
chunkCount: chunks.length,
totalTextLength: extractedText.length,
success: true,
originalMetadata: metadata // 元のファイルメタデータも出力
}
}];
} catch (error) {
return [{
json: {
error: error.message,
hint: 'Check if langchain is installed and docker-compose.yml is correctly configured for external modules.'
}
}];
}
メタデータ付与の方法:
上記のコードでは、各チャンクにchunkIndex、totalChunks、wordCount、charCountといったチャンク固有のメタデータと、originalFileName、extractedAtといったファイル全体のメタデータを付与しています。これらのメタデータは、後でSupabaseに保存され、RAGシステムで検索結果をフィルタリングしたり、ユーザーに元の情報源を提示したりする際に非常に役立ちます。例えば、特定のページ番号やセクション名、作成者などのメタデータがあれば、より精度の高い検索が可能になります。私の場合、メタデータはRAGの「文脈理解」を深めるための重要な要素だと考えています。
このCode Nodeが正しく機能すれば、PDFから抽出されたテキストが意味のあるチャンクに分割され、それぞれのチャンクがOpenAIの埋め込み生成に進む準備が整います。
Embedding生成:HTTP Requestノードを使ったOpenAI API呼び出し
チャンキングされたテキストは、そのままではLLMが理解できる形式ではありません。そこで、各チャンクを数値のベクトル(埋め込み、Embedding)に変換する必要があります。この埋め込みは、テキストの意味的な類似性を数値的に表現したもので、RAGシステムにおける検索の核となります。ここでは、openaiライブラリに依存せず、n8nのHTTP Requestノードを使ってOpenAI APIを直接呼び出し、埋め込みを生成する方法を解説します。この方法は、環境依存性が低く、最も安定しているため、私自身も強く推奨しています。
openaiライブラリ不要のHTTP Requestノード活用法:
Code Nodeでopenaiライブラリを使用するには、docker-compose.ymlでの設定やnpm installが必要となり、環境によってはトラブルの原因となることがあります。HTTP Requestノードを使えば、これらの手間を省き、標準のHTTP通信機能だけでAPIを呼び出すことができます。これは、n8nのCode NodeのVM2サンドボックスの制約を回避する最も堅実なアプローチです。
APIキー管理と認証設定:
OpenAI APIキーは、n8nのCredentials機能を使って安全に管理します。直接ノードに書き込むのではなく、環境変数として設定することで、セキュリティを向上させ、ワークフローの再利用性を高めます。
- n8nの左側メニュー「Credentials」をクリック。
- 「New Credential」をクリックし、「Bearer Token Auth」を選択。
- Nameに「
OpenAI API Key」など分かりやすい名前を付けます。 - Tokenフィールドに、OpenAIから取得したAPIキー(
sk-xxxxxxxxxxxxxxxxxxxxxxxx)を入力します。 - 「Save」をクリックして認証情報を保存します。
複数チャンク処理のためのLoopノード活用:
Code Node #1(チャンキング)の出力は、複数のチャンクを含む配列です。OpenAI APIは一度に複数の入力を受け付けますが、n8nのHTTP Requestノードは通常、1つのアイテムに対して1回実行されます。そのため、各チャンクに対して個別にAPIを呼び出すために、「Loop Over Items」ノードを使用します。これにより、各チャンクが順番にHTTP Requestノードに渡されます。
- Code Node #1の後に「Loop Over Items」ノードを追加します。
- Input Data Fieldに「
chunks」と入力します。これはCode Node #1の出力JSONのchunksプロパティを参照します。 - Loopノードの中に「HTTP Request」ノードを追加します。
HTTP Requestノードの設定(Loop内):
- Method:
POST - URL:
https://api.openai.com/v1/embeddings - Authentication (認証):
- Auth Type:
Bearer Token - Token: 先ほど作成した「
OpenAI API Key」のCredentialを選択します。
- Auth Type:
- Headers (ヘッダー):
Key: Content-Type,Value: application/json
- Body (JSON):
- Send Body Type:
JSON - 以下のJSON構造を入力します。
inputフィールドには、現在のループアイテム(チャンク)のcontentプロパティを参照する式を使用します。
- Send Body Type:
{
"model": "text-embedding-3-small",
"input": "{{ $item().json.content }}",
"encoding_format": "float"
}
$item().json.contentは、Loopノードが現在のチャンクのcontentプロパティをHTTP Requestノードに渡すことを意味します。
レスポンスのパースと次ノードへの受け渡し:
HTTP Requestノードは、OpenAI APIからのレスポンスを自動的にパースし、その結果を次のノードに渡します。レスポンスのJSON構造は以下のようになります。
{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [0.123, -0.456, ...] // これが目的のベクトル
}
],
"model": "text-embedding-3-small",
"usage": {
"prompt_tokens": 10,
"total_tokens": 10
}
}
Loopノードの後に「Aggregation」ノードを追加し、Input Data Fieldに「response」と設定することで、各HTTP Requestノードの出力を1つの配列にまとめることができます。これにより、すべてのチャンクの埋め込みが次のSupabaseノードに一括で渡せるようになります。このHTTP RequestノードとLoopノードの組み合わせは、大量のデータに対して外部APIを呼び出す際のベストプラクティスであり、私自身も様々な自動化で活用しています。
Supabaseへのデータ保存:pgvectorテーブルへのインサート設定
RAGパイプラインの最終段階は、生成された埋め込みベクトルと関連するメタデータをSupabaseのpgvector拡張機能を利用してデータベースに保存することです。これにより、後続のRAGチャットボットが類似度検索を行い、関連性の高い情報を効率的に取得できるようになります。このデータベース設計は、RAGシステムのパフォーマンスとスケーラビリティに直結するため、非常に重要です。私自身、Supabaseのpgvectorの使いやすさには驚かされました。
Supabaseプロジェクトとpgvector拡張の有効化:
Supabaseプロジェクトをまだ作成していない場合は、Supabaseのウェブサイトで新しいプロジェクトを作成してください。その後、pgvector拡張を有効化する必要があります。
- Supabaseプロジェクトのダッシュボードにログインします。
- 左側のメニューから「Database」→「Extensions」を選択します。
- 検索バーで「
vector」と入力し、「pgvector」を見つけます。 - 「Enable extension」をクリックして有効化します。
テーブルスキーマとインデックス設定:
次に、埋め込みとメタデータを保存するためのテーブルを作成します。SupabaseのSQL Editorで以下のSQLスクリプトを実行してください。
-- pgvector拡張を有効化(既に有効化済みであればスキップ)
create extension if not exists vector;
-- ドキュメントテーブル作成
create table documents (
id bigserial primary key,
file_id text, -- Google DriveのファイルID
file_name text, -- Google Driveのファイル名
content text, -- チャンクのテキスト内容
metadata jsonb, -- チャンクのメタデータ(ページ番号、ワード数など)
embedding vector(1536), -- OpenAI text-embedding-3-smallの次元数
created_at timestamp with time zone default now()
);
-- ベクトル検索用インデックス(IVFFlatインデックス)
-- 大規模なデータセットで高速な類似度検索を可能にする
create index on documents using ivfflat (embedding vector_cosine_ops);
-- 類似度検索用RPC関数(RAGチャットボットから呼び出す用)
create or replace function match_documents(
query_embedding vector,
match_threshold float default 0.7,
match_count int default 10,
file_id_filter text default null -- 特定のファイルでフィルタリングするオプション
)
returns table(id bigint, content text, similarity float, metadata jsonb)
language sql as $$
select
documents.id,
documents.content,
1 - (documents.embedding <> query_embedding) as similarity,
documents.metadata
from documents
where 1 - (documents.embedding <> query_embedding) > match_threshold
and (file_id_filter is null or documents.file_id = file_id_filter)
order by documents.embedding <> query_embedding
limit match_count;
$$;
embedding vector(1536): OpenAIのtext-embedding-3-smallモデルは1536次元のベクトルを生成します。この次元数を正確に指定することが重要です。ivfflat (embedding vector_cosine_ops):pgvectorで類似度検索を高速化するためのインデックスです。vector_cosine_opsはコサイン類似度検索に最適化されています。match_documents関数: これは、RAGチャットボットがユーザーの質問の埋め込みを受け取り、データベースから最も類似したドキュメントチャンクを検索するために使用するSQL関数です。match_thresholdで類似度の閾値を、match_countで取得するチャンク数を制御できます。
Supabase Insertノードのバッチ処理設定:
n8nワークフローの最後に「Supabase」ノードを追加します。
- Credentials (認証情報):
- SupabaseプロジェクトのAPIキーとURLを使用して新しいCredentialを作成します。Supabaseダッシュボードの「Project Settings」→「API」で取得できます。
- Resource:
Table - Operation:
Insert - Table:
documents(作成したテーブル名) - Mode:
Batch - Loop Over:
{{ $node["Aggregation"].json }}(Aggregationノードの出力を参照)
Column (カラム) マッピング:
| Column | Expression (マッピング) |
|---|---|
file_id |
{{ $node["Google Drive Trigger"].data.file_id }} |
file_name |
{{ $node["Google Drive Trigger"].data.file_name }} |
content |
{{ $item().json.body.input }} (OpenAI APIに送った元のチャンクテキスト) |
embedding |
{{ $item().json.body.data[0].embedding }} |
metadata |
{{ JSON.stringify({ chunkIndex: $item().json.chunkIndex, totalChunks: $item().json.totalChunks, wordCount: $item().json.wordCount, charCount: $item().json.charCount, originalFileName: $node["Code Node #1"].json.originalMetadata.originalFileName, extractedAt: $node["Code Node #1"].json.originalMetadata.extractedAt }) }} |
メタデータのJSON保存方法:
metadataカラムはjsonb型なので、JavaScriptのJSON.stringify()関数を使ってオブジェクトをJSON文字列に変換して保存します。これにより、後でSupabaseから柔軟にメタデータをクエリできるようになります。私は、メタデータに可能な限り多くの情報を詰め込むことで、RAGの検索精度と柔軟性が格段に向上すると考えています。
これで、Google DriveにアップロードされたPDFファイルが、n8nを介して自動的にSupabaseのpgvectorデータベースに保存される完全なRAGパイプラインが完成しました。あとは、このデータベースを使ってRAGチャットボットを構築するだけです。
セクション4: トラブルシューティングと運用のポイント
よくあるエラーとその対処法
RAGワークフローの構築は多岐にわたる技術要素を組み合わせるため、様々なエラーに遭遇する可能性があります。私自身も数えきれないほどのエラーに直面し、そのたびに試行錯誤を繰り返してきました。ここでは、特に遭遇しやすいエラーとその対処法について、具体的な解決策を提示します。
Cannot find module 'pdf-parse'やCannot find module 'openai'の問題- 症状: Code Nodeで
require('pdf-parse')やrequire('openai')を実行した際に、「Cannot find module '...'」というエラーが発生する。 - 原因: n8nのCode NodeはVM2サンドボックス上で動作するため、
docker-compose.ymlで明示的に外部モジュールの利用を許可し、かつEXTRA_NODE_MODULESでインストールを指示しないと、モジュールが見つかりません。また、npm installをコンテナ内で直接行っても、VM2サンドボックスのパスからは見えない場合があります。 - 解決策:
docker-compose.ymlの再確認と完全な再起動:NODE_FUNCTION_ALLOW_EXTERNAL=pdf-parse,langchain,openaiとEXTRA_NODE_MODULES=pdf-parse langchain openaiが正しく設定されているか確認し、docker-compose down→docker rmi docker.n8n.io/n8n/n8n:latest→docker-compose up -dでイメージを再ビルドし、コンテナを完全に再起動します。これが最も根本的な解決策です。- Community Node『PDF Parse』の利用:
pdf-parseに関しては、n8nのCommunity Nodeであるn8n-nodes-pdf-parseを利用するのが最も確実で簡単な方法です。n8nのGUIからインストールするだけで、Code Nodeでpdf-parseをrequireする必要がなくなります。Code Nodeではチャンキング処理のみを行います。 - OpenAI API呼び出しのHTTP Requestノード化:
openaiライブラリに関しては、Code Node内でfetchAPIを使ってOpenAIのHTTPエンドポイントを直接呼び出すか、HTTP RequestノードとLoopノードを組み合わせてAPIを呼び出すのが最も環境依存性が低く安定しています。これにより、openaiライブラリのインストール自体が不要になります。
- 症状: Code Nodeで
docker-compose.ymlの設定反映問題- 症状:
docker-compose.ymlを編集しても、n8nの動作が変わらない、またはエラーが解消されない。 - 原因: Dockerコンテナが正しく再起動されていない、または古いイメージがキャッシュされている可能性があります。
- 解決策:
docker-compose downでコンテナを停止・削除し、docker rmi docker.n8n.io/n8n/n8n:latestで古いイメージを削除してから、docker-compose up -dで新しいイメージをプルして起動します。これにより、設定が確実に反映されます。
- 症状:
- APIキーの設定ミスと認証エラー
- 症状: OpenAI API呼び出し時に「
Authentication error」や「Invalid API key」といったエラーが発生する。 - 原因: OpenAI APIキーが間違っている、またはn8nのCredentialsに正しく設定されていない可能性があります。
- 解決策: OpenAIのウェブサイトでAPIキーが有効であることを確認し、n8nのCredentialsでBearer Token AuthとしてAPIキーを正確に設定します。また、HTTP RequestノードやCode NodeでCredentialsが正しく参照されているか確認してください。
- 症状: OpenAI API呼び出し時に「
- PDF抽出時のエラー(スキャンPDFなど)
- 症状: PDF ParseノードやCode NodeでのPDF抽出時に、テキストが全く取得できない、または文字化けする。
- 原因: スキャンされた画像ベースのPDFは、通常のテキスト抽出ライブラリではテキストを認識できません。また、PDFの構造が複雑な場合も抽出に失敗することがあります。
- 解決策: スキャンPDFの場合は、OCR(光学文字認識)機能を持つツール(例: Unstructured.io、AWS Textractなど)を別途利用する必要があります。本記事の範囲外ですが、これらのサービスをn8nのHTTP Requestノードで呼び出すことも可能です。テキストベースのPDFであることを確認してください。
これらのトラブルシューティングは、私がRAGパイプラインを構築する上で実際に遭遇し、解決してきた経験に基づいています。焦らず、一つずつ原因を特定し、対処していくことが成功への鍵となります。
運用時のコスト管理とパフォーマンス最適化
RAGワークフローを構築し、運用する上で、コストとパフォーマンスは常に考慮すべき重要な要素です。特にOpenAI APIやSupabaseのようなクラウドサービスを利用する場合、予期せぬコスト発生やパフォーマンス低下は避けたいものです。私自身も、コストと精度のバランスをどう取るか、常に頭を悩ませています。
OpenAI Embeddingコストの目安:
OpenAIの埋め込みモデルtext-embedding-3-smallは、100万トークンあたり$0.02と非常にコスト効率が良いです。例えば、100MBのPDFファイルを処理する場合、平均的なトークン数(1MBあたり約1000トークンと仮定)から試算すると、約10万トークンとなり、埋め込みコストはわずか$0.002程度です。しかし、大量のドキュメントを継続的に処理する場合、累積コストは無視できません。例えば、月に1000個のPDF(各10MB)を処理する場合、約1000万トークンとなり、月額$0.20程度のコストが発生します。これは非常に安価ですが、モデルをtext-embedding-3-large(100万トークンあたり$0.13)に変更すると、コストは6.5倍になります。
Supabaseストレージコスト:
Supabaseのpgvectorに保存されるデータは、テキストコンテンツ、メタデータ、そして埋め込みベクトルです。無料プランでは500MBのデータベースストレージが提供されます。1536次元の埋め込みベクトルは、1つあたり約6KBのストレージを消費します。例えば、100MBのPDFが500字チャンクで1000チャンクに分割された場合、埋め込みだけで約6MBのストレージを消費します。これにテキストとメタデータが加わりますが、通常は無料プランの範囲内で十分運用可能です。ただし、大量のドキュメント(数千〜数万ファイル)を扱う場合は、有料プランへの移行を検討する必要があります。Supabaseの有料プランはStorage 100GBで月額$25から提供されています。
チャンキングサイズ調整によるコスト・精度バランス:
チャンキングサイズは、コストとRAGの精度に直接影響します。
- チャンクサイズを小さくする:
- メリット: 検索時に、より関連性の高いピンポイントなチャンクを見つけやすくなります。LLMへの入力トークン数が減り、コストが削減される可能性があります。
- デメリット: 文脈が失われやすくなります。例えば、重要な情報が複数のチャンクに分断され、LLMが全体像を把握しにくくなることがあります。
- チャンクサイズを大きくする:
- メリット: 文脈が保たれやすくなり、LLMがより包括的な回答を生成できます。
- デメリット: 検索時にノイズが増え、関連性の低い情報も含まれる可能性があります。LLMへの入力トークン数が増え、コストが増加します。
推奨されるchunkSize=500、overlap=100は、多くのユースケースでバランスの取れた設定ですが、特定のドメインやドキュメントタイプに合わせて調整することが重要です。例えば、非常に専門的な技術文書や法務文書では、より小さいチャンクサイズで精密な検索を、物語性の高い文書では、より大きいチャンクサイズで文脈を重視するといった調整が考えられます。私の場合、まずは推奨値で試行し、RAGの回答精度を見ながら調整するようにしています。
レート制限回避のためのLoopノード活用:
OpenAI APIにはレート制限(一定時間内に呼び出せる回数)があります。特に大量のチャンクを一度に処理しようとすると、この制限に引っかかる可能性があります。n8nのLoopノードは、デフォルトでアイテムを逐次処理するため、ある程度のレート制限対策になります。しかし、非常に大量のチャンク(例えば500チャンク以上)を処理する場合や、APIの応答が遅い場合は、Loopノード内に「Delay」ノードを追加して、意図的に処理を遅延させることができます。
Loop
├─ HTTP Request ノード
└─ Delay ノード(1秒待機)← 500個以上のチャンクを処理する場合に推奨
これにより、APIへのリクエスト頻度を調整し、レート制限によるエラーを回避できます。運用開始後は、n8nの実行ログやOpenAIのAPI使用状況を定期的に確認し、必要に応じてこれらの最適化策を適用することが、安定したRAGパイプライン運用には不可欠です。
セクション5: まとめと今後の展望
今回の構築で得られるメリットの総括
今回のガイドを通じて、Google DriveのPDFをn8nで抽出し、チャンキング、OpenAIの埋め込み生成を経てSupabaseのpgvectorに保存する、完全なRAGパイプラインの構築方法を詳細に解説しました。この自動化されたワークフローは、多くのメリットを私たちにもたらします。私自身、この一連のプロセスを構築できた時の達成感はひとしおでした。
まず、Google Driveからの完全自動化パイプラインの利便性は計り知れません。一度設定してしまえば、あとはGoogle DriveにPDFファイルをアップロードするだけで、自動的にRAGデータベースが更新されていきます。手動でのデータ入力や変換作業は一切不要となり、情報管理の効率が飛躍的に向上します。これにより、私たちはより本質的な業務に集中できるようになります。
次に、n8nとSupabaseの組み合わせによる拡張性です。n8nはノーコード・ローコードツールでありながら、Code Nodeを活用することで高度なカスタマイズが可能です。これにより、PDF抽出やチャンキングのロジックを柔軟に調整できます。一方、SupabaseはPostgreSQLをベースとした強力なデータベースであり、pgvector拡張によりベクトルデータベースとしての機能も兼ね備えています。スケーラブルな設計のため、データ量の増加にも柔軟に対応でき、将来的な機能拡張にも対応しやすいのが特徴です。この組み合わせは、開発のスピードと運用の安定性を両立させる理想的な選択だと私は考えています。
そして、OpenAI APIの活用による高精度RAG実装です。OpenAIの最先端の埋め込みモデルを使用することで、テキストの意味的な類似性を非常に高い精度で捉えることができます。これにより、ユーザーの質問に対して、Google Drive内の膨大なPDFの中から最も関連性の高い情報を効率的に検索し、LLMがより正確で質の高い回答を生成するための基盤が確立されます。この高精度なRAGは、企業内の知識活用や顧客サポートなど、多岐にわたるユースケースでその真価を発揮するでしょう。
今後の応用例と発展的な活用方法
今回構築したRAGパイプラインは、単なる情報のデータベース化に留まらず、様々な応用と発展の可能性を秘めています。私自身、この基盤を元にどのような新しい価値を生み出せるか、常にワクワクしています。
最も直接的な応用例としては、RAGチャットボットへの応用が挙げられます。Supabaseに保存された埋め込みデータとmatch_documents関数を活用すれば、ユーザーからの質問を埋め込みに変換し、データベースから類似するチャンクを検索、そのチャンクをコンテキストとしてLLMに渡すことで、質問応答システムを構築できます。これは、社内ヘルプデスク、顧客サポート、製品情報検索など、多岐にわたるシーンで活用できるでしょう。n8nのWebhooksノードをトリガーとして、ユーザーインターフェース(例: Streamlit, Next.js)と連携させれば、リアルタイムなRAGチャットボットが実現可能です。
また、Edge FunctionやLangChainとの連携可能性も広がります。SupabaseのEdge Function(Denoベースのサーバーレス関数)を利用すれば、RAGの検索ロジックやLLM呼び出しをデータベースの近くで実行でき、レイテンシの短縮やスケーラビリティの向上が期待できます。例えば、documentsテーブルへのINSERTトリガーとしてEdge Functionを設定し、リアルタイムで埋め込みを生成するといった使い方も考えられます。さらに、LangChainのようなフレームワークをEdge Functionや外部アプリケーションで利用することで、より複雑なRAG戦略(例: 複数ステップの検索、情報の要約と再構成)や、異なるLLMとの連携、エージェント機能の実装など、高度なRAGシステムへと発展させることができます。
多言語対応やOCR対応の拡張案も今後の重要なテーマです。現在のパイプラインは主にテキストベースのPDFを想定していますが、スキャンされた画像PDFや多言語のドキュメントを扱う場合、Unstructured.ioのような外部APIや、Google Cloud Vision API、AWS TextractなどのOCRサービスをn8nのHTTP Requestノードで連携させることで、対応範囲を広げることが可能です。これにより、より多様な形式のドキュメントをRAGの対象とできるようになります。私たちが構築したこの基盤は、まさに未来の情報活用に向けた無限の可能性を秘めているのです。
【参考資料・出典】