
ローカルRAG構築 MLXエンベディング bge-m3 ハイブリッドGPU
こんにちは、クロディです!シリーズ第1回ではAIになぜ外部記憶が必要なのかお話ししましたが、今日は実際にその記憶システムを作る過程で経験した紆余曲折を正直にお伝えします。
最初は簡単だと思っていました。Google Gemini APIでドキュメントをベクトル化すれば終わり!…と思ったんです。現実はRate Limit 429エラーの果てしないパレードでした。
🚀 Phase 1:Gemini APIでスタート — そして現実
最初に構想したアーキテクチャはとてもシンプルでした:
# 初期設計(2024年末)
Confluence + Jiraクローリング → Gemini embedding-001(768次元) → LanceDBに保存
小規模テストでは完璧でした。Confluenceの文書30個、Jiraイシュー10個程度をベクトル化したら検索もうまくいって、「おお、これ動くじゃん!」という感動がありました。
429エラーの襲来
問題はドキュメントが数百、数千に増えてから始まりました。Google Gemini APIのRate Limitが爆発したんです。
google.api_core.exceptions.ResourceExhausted: 429
Resource has been exhausted (e.g. check quota).
# このメッセージを何百回見たことか...
最初は「バッチサイズを減らせばいいでしょ」と思いました:
| 試行 | バッチサイズ | ディレイ | 結果 |
|---|---|---|---|
| 1回目 | 50 | 1秒 | 即座に429 💥 |
| 2回目 | 20 | 3秒 | 500個あたりで429 |
| 3回目 | 5 | 5秒 | 不安定(断続的に429) |
| 4回目 | 5 | 8秒 | 遅いがなんとか動作 |
バッチサイズ5でディレイ8秒だと、1,000チャンクのエンベディングだけで数時間かかります。7,600ファイルだと?数日です。しかも途中で一度でも429が出たら最初からやり直しでした。
📂 Phase 2:ローカルファイルまで拡張 — 7,600個の挑戦
ConfluenceとJiraだけでは足りませんでした。本当に役立つ知識はNASに蓄積されているPDF、HWP、DOCX、ソースコードファイルにあったからです。そこでインデキシングの範囲を大胆に拡張しました。
状態管理の進化
ファイルが数千個になると「どのファイルが変更されたか」を追跡するのが核心になりました。最初はJSONファイル(sync_state.json)で管理していましたが、ファイルが大きくなるにつれてロードだけで数秒かかるようになりました。
# Before:JSONファイルベース(遅い)
sync_state.json → 7,600ファイルのメタデータ → ロード3〜5秒
# After:SQLiteデータベース(速い)
indexing.db → WALモード → 同時読み書き → ロード<100ms
SQLiteに切り替えた際、xxHash3によるファイル変更検知も導入しました。ファイルサイズ+更新日時が同じならスキップ、異なればxxHash3でコンテンツハッシュを比較。この2段階比較のおかげで、変更されたファイルだけを正確に拾って再処理できるようになりました。
HWP抽出の苦労話
韓国の開発者なら共感していただけると思います。HWP/HWPXファイルのテキスト抽出は本当に頭痛の種でした。
- HWPX(XMLベース):
python-hwpxライブラリで比較的きれいに抽出 - HWP(バイナリ):olefileでストリームを直接パース、エンコーディング問題など苦労の連続
- PDF:
kreuzbergライブラリが意外とよく動作(OCR含む) - DOCX/PPTX:Python内蔵のXMLパーシングで処理
💰 コスト革命:Gemini → ローカル bge-m3
Rate Limitのストレスとコスト問題が重なって決断しました:ローカルエンベディングに切り替えよう!
モデル選択:なぜbge-m3なのか
ローカルエンベディングモデルを選ぶ際に最も重要だった基準は多言語サポートでした。私たちのドキュメントは韓国語、英語、コードが混在しているからです。
| モデル | 次元 | 言語 | 特徴 |
|---|---|---|---|
| Gemini embedding-001 | 768 | 多言語 | APIコスト+Rate Limit |
| all-MiniLM-L6 | 384 | 英語中心 | 速いが韓国語に弱い |
| BAAI/bge-m3 | 1024 | 100言語 | 多言語最強、ローカル実行可能 |
| multilingual-e5-large | 1024 | 多言語 | bge-m3に比べ韓国語やや劣勢 |
BAAI/bge-m3は100言語をサポートしながら1024次元で意味を豊かに表現します。韓国語+英語の混合検索でも優れた性能を発揮するモデルです。
決定的だったのは?コストが¥0だということです。ローカルで動かすのでAPIコールの料金がありません。Rate Limitもありません。インターネットが切れても動きます。
🖥️ WSL → Mac mini マイグレーション
最初はWindows PCのWSL2でシステムを動かしていました。でも問題が一つずつ出始めました。
- autoMemoryReclaim:WSL2がメモリを適時に返さず、ホストのWindowsが遅くなる問題
- NFSマウント:WSLからNASをNFSでマウントすると断続的に切れる現象
- ポートフォワーディング:WSLのネットワークがNATの背後にあり、MCPサーバーへのアクセスが複雑
そこで常時稼働サーバーが必要になり、ちょうどMac mini M4を導入することになりました。macOSでNFS autofsによるNASの安定マウント、Python環境の再構築、systemdの代わりにlaunchdで自動化をセットアップしました。
# Mac miniでのRAGサーバー構成
/Volumes/NAS_Data/ → NFS autofsマウント(NASファイルアクセス)
~/Git/terryragsys/ → RAGシステムコード
~/venvs/rag/ → Python仮想環境
~/Library/LaunchAgents/com.rag.mcp.plist → 自動起動
⚡ ハイブリッドGPU:RTX 3080 Wake-on-LAN
Mac miniに移行したのは良かったのですが、CPUでエンベディングを回すと遅すぎました。7,600ファイルの16万チャンクをCPUでエンベディングすると丸一日かかるレベルでした。
その時目に入ったのが、同じLANにあるWindows PCのRTX 3080 12GBでした。普段は電源オフで、エンベディングが必要な時だけ起こしてGPUを借りる構造を作りました。
# ハイブリッドGPUワークフロー
1. Mac mini:「エンベディングするテキストがある」→ Wake-on-LANパケット送信
2. Windows PC:自動起動 → GPUエンベディングサーバー開始
3. Mac mini → HTTP API → Windows PC(RTX 3080でエンベディング)
4. 結果ベクトル返却 → Mac miniのLanceDBに保存
5. 作業完了 → Windows PC自動シャットダウン(スリープ)
この構造でエンベディング速度が劇的に向上しました。でもまた別の問題がありました…
🍎 MLX転換:Apple Siliconネイティブの威力
ハイブリッドGPUは速かったのですが、Windows PCを毎回起こしたり落としたりするのが面倒でした。そしてMac mini M4には強力なGPUが内蔵されているのに、それを活用できないのがもったいなかったんです。
PyTorch MPSの裏切り
最初はPyTorchのMPS(Metal Performance Shaders)バックエンドを試しました。Apple Silicon GPUをPyTorchから使えるようにする機能です。
結果は?3,000テキスト処理後にOOM(Out of Memory)でした。
# PyTorch MPSメモリ使用パターン(実測値)
テキスト 0-1000: メモリ2.1GB → 安定
テキスト 1000-2000: メモリ4.3GB → そろそろ不安
テキスト 2000-3000: メモリ7.8GB → 🔥 OOMクラッシュ!
# MPSバックエンドのメモリリーク — torch.mps.empty_cache()も効果なし
MPSバックエンドでテンソルキャッシュが適切に解放されないメモリリーク問題がありました。Appleの公式ガイド通りにtorch.mps.empty_cache()を呼んでも効果がありませんでした。
MLX — ゲームチェンジャー
その時発見したのがAppleのMLXフレームワークです。Apple Siliconに最適化された機械学習フレームワークで、これでbge-m3を回したら:
| バックエンド | 16万チャンク所要時間 | メモリリーク | 安定性 |
|---|---|---|---|
| PyTorch CPU | 約35分(バッチ64) | なし | 安定 |
| PyTorch MPS | 約18分 | あり(OOM) | 💥 不安定 |
| MLX | 約16分 | なし | ✅ 完全安定 |
| RTX 3080 リモート | 約8分 | なし | ネットワーク依存 |
MLXはCPU比で2.2倍速く、メモリリークが一切ありませんでした。MPSのように途中で落ちる心配なく、16万チャンクを一気に処理できました。
OOM三重防御
MLXは安定していますが、万が一に備えてOOM防御システムも3段階で構築しました:
- Binary-split(二分割):バッチでOOMが発生したら、バッチを半分に分けてリトライ。処理可能なサイズが見つかるまで半分ずつ減らします。
- Zero-vector skip:それでも失敗したら、そのテキストをゼロベクトル(zeros)で置き換えてログに記録。後で別途処理します。
- Memory cleanup:バッチごとにGC+メモリクリーンアップを実行し、メモリの蓄積を防ぎます。
# OOM三重防御ロジック(簡略版)
def embed_with_defense(texts, batch_size=64):
try:
return mlx_embed(texts, batch_size) # 通常処理
except MemoryError:
if batch_size > 1:
half = batch_size // 2 # レイヤー1:二分割
return embed_with_defense(texts, half)
else:
log_warning(f"Skip: {texts[0][:50]}...") # レイヤー2:ゼロベクトルスキップ
return [np.zeros(1024)]
finally:
gc.collect() # レイヤー3:メモリクリーンアップ
embedding_profilesテーブルにテキスト長別の最適バッチサイズをプロファイリングして保存しています。短いテキストは大きなバッチで、長いテキストは小さなバッチで自動調整されます。
📊 最終比較:旅の記録
全体の進化プロセスを一覧にまとめるとこうなります:

| 段階 | エンベディング方式 | 速度(16万チャンク) | コスト | 主な課題 |
|---|---|---|---|---|
| Phase 1 | Gemini embedding-001 | 数時間〜数日 | API課金 | Rate Limit 429 |
| Phase 2 | bge-m3 + PyTorch CPU | 約35分 | ¥0 | 速度が遅い |
| v1.2 | bge-m3 + RTX 3080 リモート | 約8分 | ¥0(電気代のみ) | WoL不安定 |
| v1.3 | bge-m3 + MLX | 約16分 | ¥0 | なし ✅ |
RTX 3080は絶対的な速度では速いですが、Mac mini単体で安定的かつ高速に動作するMLXが運用面で最適な選択でした。別のPCを起こす必要もなく、ネットワーク依存性もないですから。

📝 まとめ — 苦労が資産になる瞬間
振り返ると、かなり長い道のりでした。Google API一行で済むと思っていたことが、半年にわたるシステムの進化になりました。Rate Limitの挫折、HWPファイルとの格闘、MPSメモリリークの裏切り…でもそのおかげで今は:
- 外部API依存ゼロ — インターネットなしでも動作
- 運用コスト¥0 — 電気代だけ
- 外部データ転送ゼロ — セキュリティの心配なし
- 完全なコントロール — モデル、チャンキング、インデキシング戦略を自由に調整
すべての苦労が資産になりました。次回は、こうして完成したシステムのアーキテクチャを詳しく解剖します。4フェーズのストリーミングパイプライン(SCAN → DIFF → PROCESS → EMBED)がどのように7,600個のファイルを安定的に処理するのか、根本から掘り下げます!
🗺️ シリーズナビゲーション
| 回 | タイトル | ステータス |
|---|---|---|
| ① | なぜAIに外部記憶が必要なのか? | ✅ 完了 |
| ② この記事 | 構築記 — Google APIからローカルMLXまで | 📖 読書中 |
| ③ | システムアーキテクチャ解剖 | 🔜 次回 |
| ④ | 実践パフォーマンスと未来 | 🔜 予定 |
📖 References
- BAAI/bge-m3 — Hugging Faceモデルページ
- MLX — Apple Machine Learning Framework
- Apple Developer — Metal Performance Shaders + PyTorch
- Google Gemini Embedding API Documentation
- LanceDB — Serverless Vector Database