はじめに
税理士事務所で、関与先の大量の書類(PDF・Excel・Word)を横断検索したい——という動機で、半年ほどローカルRAGを自作・運用していました。
結論から言うと、最終的に全部捨てて、macOS標準の mdfind(Spotlight)+ Vision(OCR)に戻しました。 ディスクも約10GB空きました。
ただし、これは「RAGは不要」という話ではありません。自分が解きたい検索問題の種類を見誤っていた、という話です。詳しくは後半で仕分けします。
解決したかった課題:
- 関与先ごとの契約書・決算書・申告書・請求書…を「中身」で検索したい
- ファイル名は覚えてないが「あの覚書どこ?」を一発で出したい
- スキャンしただけの画像PDFも引っかけたい
- 関与先の機密情報なので、ChatGPT等の外部クラウドには上げられない(だから「ローカル」RAG)
この記事で得られるもの:
- ローカルRAGの自作で踏んだ地雷(関与先混入・wedge・運用コスト)
- 「grep vs RAG」の実測比較と、検索タスクの仕分け方
- grepはPDF/Officeの中身を読めない。でもSpotlightは最初から索引しているという事実
- スキャンPDFだけApple Vision OCRで埋める構成
- AI検索を作る前に確認すべきチェックリスト
やったこと:ローカルRAGの自作
最初に組んだのはこんな構成です。
- open-notebook(ローカルのNotebookLM的OSS)
- SurrealDB(ベクトルインデックス)
- ollama(埋め込み生成)
- 全部 Docker(Colima) で運用
関与先の機密情報を外部に出せない以上、クラウドのAI検索は最初から選択肢になし。「完全にPC内で完結するローカルRAG」を組んで、関与先のPDFを片っ端から投入しました。我ながらモダンやな、と。そのまま、まじめに運用を続けました。
検索精度より怖かったのは「関与先の混入」だった
この記事でいちばん伝えたいのは、速度でも精度でもなく、士業特有の失敗モードです。
全関与先が1つの共有ベクトルインデックスに乗ります。ここで問題が起きました。源泉徴収票・給与明細・決算書のような汎用フォーマットは、関与先をまたいでベクトル的にほぼ同一になる。結果、
社名をクエリに入れても、別の関与先の書類が普通に混ざって返ってくる(実測)
検索結果が少しズレるだけなら笑い話です。でも税理士業務で「A社を探しているときにB社の資料が混ざる」のは、単なる検索ミスではなく情報漏洩リスクに直結する業務事故です。
この時点で、検索システムに求める第一条件は「賢さ」ではなく**「隔離」**だと気づきました。
RAGは作るより、運用し続ける方が重い
ローカルRAGは、最初に動いた瞬間はかなり気持ちいいです。でも実務で必要なのは「一度動くこと」ではなく、**「明日も、来月も、繁忙期にも、黙って正しく動くこと」**です。
その観点で重かったもの:
- wedge:OCRや埋め込みの重い処理がingestワーカーを塞ぎ、投入が止まる
- 再投入地獄:埋め込みの欠落が出るたびに対象を洗い出して再投入
- 常駐コスト:SurrealDBもollamaもメモリを食う。止まれば検索も止まる
- 約10GBのディスク:ベクトルDB+Dockerイメージ
「検索したいだけ」なのに、インフラのお守りで時間が溶けていく。OS標準のインデックス機能に乗る方が、圧倒的に楽でした。
転機:「grepの方が速くね?」
ある日ふと思いました。普通に grep した方が速いし簡単なのでは?
同じ関与先フォルダに対して、同じクエリで「RAG vs grep」を実測しました(RAGはウォーム状態)。
| クエリ | 種別 | grep | RAG(ベクトル) |
|---|---|---|---|
| 覚書 | 既知語 | 33ms / 正解2件 | 1624ms / 0件 |
| 定款 | 既知語 | 34ms / 正解 | 1801ms / 正解(grepの53倍遅い) |
| 会社設立の手続き | 言い換え | 0件 | 正解 |
「覚書」のようにファイルにそのまま書いてある2文字を、RAGは閾値を下げても0件で取りこぼす(ベクトルは短い語に弱い)。一方、「会社設立の手続き」のような言い換え・概念検索ではRAGが勝ちました。
RAGが悪かったのではなく、検索したいものを見誤っていた
ここが本質です。失敗の原因は、RAGを使ったことではなく、自分の検索問題の種類を取り違えていたことでした。
検索タスクを仕分けると、こうなります。
| 探したいもの | 向いている方法 |
|---|---|
| 社名・金額・書類名・覚書・定款(既知語) |
mdfind / grep |
| PDF / Word / Excel の本文 | Spotlight(mdfind) |
| スキャンPDF(画像) | Vision OCR + grep |
| 「会社設立の手続き」のような概念検索 | RAGが有利 |
| 関与先ごとの厳密な隔離 | フォルダスコープ検索が安全 |
私の実務で多かったのは、社名・書類名・金額・日付・覚書・定款のような、すでに頭の中にある語で探す検索でした。この用途では、意味検索よりも全文検索の方が速く・正確で・説明可能です。
つまり、RAGの問題ではなく、**「既知語中心の業務検索にローカルRAGは過剰だった」**というだけの話でした。
でも、grepはPDF/Officeの「中身」を読めない
仕分けはできた。が、壁があります。
grep "売買契約" 契約書.pdf
# → 0件(PDFは圧縮バイナリ。テキスト層があっても語として読めない)
PDFもWord(.docx)もExcel(.xlsx)も、中身はZIP圧縮されたXMLやバイナリ。grep/ripgrepはそれを「ただのバイナリ」としか見られません。
じゃあRAGに戻るしかないのか?——いいえ。手元のOSが、とっくに解決していました。
発見:Spotlightが最初から「中身」を索引していた
macOSの mdfind(Spotlightのコマンド版)を、フォルダを絞って叩いてみます。
# 関与先フォルダ内を「中身」で全文検索
mdfind -onlyin "/path/to/関与先" "売買契約"
# → 0売買契約書.pdf がヒット(ファイル名ではなく本文に反応)
# 本文限定で検索したいとき
mdfind -onlyin "/path/to/関与先" 'kMDItemTextContent == "*覚書*"cd'
検証してわかったこと:
-
テキスト層のあるPDF・Word・Excelは、中身(Excelはセル値まで)を最初からSpotlightが索引済み。
mdfindで本文ヒットする。 -
-onlyinでフォルダスコープ=関与先の混入ゼロ。最重要だった「隔離」が、標準機能でそのまま満たせる。 - これを macOSは2005年(Tiger)からバックグラウンドで黙ってやり続けていた。
半年かけて、OSが標準でやっていることを、Docker+ベクトルDB+ollamaで再発明していたわけです。
唯一の穴:スキャンPDF → これもOS標準のVision OCRで埋める
Spotlightにも読めないものが1つだけあります。テキスト層のない、画像としてスキャンされただけのPDFです。中身が画像なので索引のしようがない。
ここだけは「テキスト化」が要りますが、これもOS標準で済みました——macOSの Vision フレームワーク(Apple Vision OCR) です。
- スキャンPDF → Vision OCR でテキスト抽出
- 抽出テキストを、原本と同じフォルダの隠しサブフォルダ
.pdftext/にサイドカー.txtとして保存(原本PDFには一切触らない=更新日時もそのまま) - あとは
.txtを grep するだけ
Vision OCRは回転不変です。
| 回転 | 認識文字数 |
|---|---|
| 0度 | 746字 |
| 90度 | 743字 |
| 180度(逆さ) | 754字 |
| 270度 | 735字 |
横向きでも逆さでも、向きを自動判定してほぼ同じ精度で読みます(日本語OCRは macOS 13 Ventura 以降が必要)。ScanSnapの傾きスキャンもそのまま通りました。
※ Spotlightは隠しフォルダ(
.pdftext/)を索引しないので、スキャン分だけは薄いラッパー(os.walkで.txtを読むgrep)を使っています。テキスト系はmdfind、スキャンだけこのラッパー、という二段構えです。
結果:RAGを全撤去して約10GB解放
最終的に捨てたもの:
- SurrealDBのベクトルDB(投入済みデータ)… 約7.8GB
- Dockerイメージ(open-notebook + surrealdb)… 約2.55GB
- Colima VM ごと撤去
- 合計 約10GB+常駐プロセス2つ
検索体制は、
- テキスト系(PDF/Word/Excel)→
mdfind(Spotlight) - スキャン画像PDF → Vision OCR + grep
の2本だけに。Docker も ollama も要らなくなり、お守りに使っていた時間が本来の業務に戻ってきました。
まとめ:重いものを作る前に、まず検索問題を仕分ける
今回の反省は、「RAGは不要」という話ではありません。
言い換えや概念で探したい場合、単純な全文検索では届かない資料をRAGが引けることもあります。RAGが向いている検索は確かにあります。
ただ、私の税理士業務で多かったのは、社名・書類名・金額・日付・覚書・定款のような、すでに頭の中にある語で探す検索でした。この用途では、意味検索よりも全文検索の方が速く・正確で・説明可能です。さらに mdfind -onlyin で関与先フォルダに範囲を絞れば、別関与先の混入も防げる。
スキャンPDFだけは別処理が要りましたが、ここも Vision OCR でサイドカー .txt を作る構成で足りました。
少なくとも今回の業務検索の大半は、macOS標準の Spotlight + Vision OCR で十分でした。
新しい技術を入れる前に、まず自分が解きたい検索問題を仕分ける。そして、OS標準の全文検索・インデックス・OCR機能でどこまでできるかを確認する(これはWindows Search でも同じ発想が使えます)。これだけで、運用コストもディスクもかなり減らせます。
半年、返してほしい。でも、いい勉強でした。
AI検索を作る前に確認した方がいいこと
最後に、判断基準としてチェックリストを置いておきます。
- 探したいものは「概念」か「既知語」か
- ファイル名ではなく本文検索が本当に必要か
- OS標準検索(Spotlight / Windows Search)でPDF / Office本文まで拾えないか
- 検索対象をフォルダ単位で安全に分離できるか
- スキャンPDFだけ別処理にすれば済まないか
- 0件だったときに「本当に無い」と断定してよい仕組みか
- そのシステムを半年後も自分で保守できるか
「概念検索が主」「全社横断で意味的に引きたい」なら、RAGは正解です。逆に上の多くが「既知語・本文・フォルダ隔離」に寄るなら、まずOS標準を試す価値があります。
【おまけ】ハマり集
1. Spotlightの索引は「古く」なる(stale)
イベント駆動(ファイル変更で数分以内に索引更新)で、定期フル再走査はありません。なので、
- 同期サービス(Dropbox等)が他デバイスから書いたファイル(mtimeは原作成時刻のまま)
- オンラインのみ(スマートシンク)ファイル
- 外付けボリューム
このあたりは索引が遅れ/欠けることがあります。「mdfind で0件=資料なし」と断定するのは危険。強制再索引は:
mdimport "/path/to/file_or_dir"
私は「過去N日に変更されたファイルだけ毎朝 mdimport」する軽量なlaunchdを仕込みました(ctime基準なので同期で入ったファイルも拾える)。
2. brew uninstall ollama で python ごと消えて青ざめた
ollamaを消そうとしたら、依存が芋づる式に:
ollama → mlx-c → mlx → python@3.14
brew uninstall ollama が、不要になった依存(mlx / mlx-c / python@3.14)まで巻き込んで削除しました。「ollamaがpythonに依存してるなんて思わない」——依存ツリーは意外です。
# 消す前に依存ツリーを見る
brew deps --tree ollama
brew uses --installed mlx
幸い、別用途のPythonツールは独立したvenv(pyenv製)だったので無傷でした。消す前に依存と、常駐プロセスの実行環境(launchd plistの実行パス)を確認すべき、という教訓。
3. スキャン判定に pymupdf4llm を使うと遅い
PDFのテキスト抽出に pymupdf4llm を使うと、スキャンPDFで内部的にTesseract/RapidOCRが走って激遅でした。スキャン判定は pdftotext(内部OCRなし)で高速にゲートし、スキャンだけApple Visionへ流すと正確かつ速い。
4. mdls kMDItemTextContent は当てにならない
索引状態を確認しようと mdls -name kMDItemTextContent を見ると、索引済みファイルでも (null) を返します(この属性は表示用に展開されないだけ)。索引の有無は、実際に mdfind で引くか、mdimport -t -d2 <file>(importerのドライ実行)で確かめましょう。