はじめに
Mac miniでそれなりに大きいローカルLLMモデルが動くことが確認できたので、いろんなローカルLLMの使い方があるなあと考えていたが、いくつかのアイディアの中でも以前からどうしても試してみたかったことがRAGだった。学生時代の教科書や技術書・論文など自炊も含めてPDFが15年で1500件ぐらいたまっていて、正直うまく使えていない実感がある。全文検索のしくみでは記憶に頼ったワード検索になるのでたどり着けない時もあるし、技術書を基にした自分好みの設計レビューなどもできるんではと考えると、RAG試してみるしかないなというところである。
この大AI時代、「知識に関する質問はフロンティアモデルに聞けばいいじゃん」と考える人も居そうだが、汎用的な言語モデル由来の情報より、"一度読んで理解した自分のために集めた知識に限定して検索して得た情報"の方が、自分には価値があるのかなと思っている。RAGなのでAIで慣れきったあいまい検索もできるようになる。
RAGの対象とするファイルの準備
RAGの対象とするのはPDFに限定することにした。私は整理が下手なので、ローカルドライブやOneDriveの中は無法地帯でいろんなファイルが放置されている。ドキュメント、PDFも写真も、あらゆる情報を整理せずがぐちゃぐちゃに保管されている。PDFを使える形にする以前に、全てのファイルのうち大部分を占める雑多な写真ファイルを整理したうえで、対象とするPDFを抽出することが必要だった。
TreeSizeで可視化すると、画像・写真がほとんどのファイルサイズを占めていた。何でもかんでも残しておく癖が災いして、写真・スクリーンショットなど15年分で40GB程度のデータに膨らんでいた。
最近M365のプランを契約するようになって、OneDriveの1TBが利用できるようになったので、いろんなクラウドストレージに分散していたものをOneDriveに一括集約していた。いろんなストレージを乗り換えながら今の形に至るので、重複ファイルもあった。重複写真はCodexに作ってもらった重複レビュー器で捌いた。
こだわりもないので写真と画像はEXIFなどを元に機械的に日付ルールで整理した。

RAGで対象とするPDFファイルの処理
PDFのOCRのVLMモデルとして、Mac miniのOllama環境から試せるDeepseek-OCRとglm-ocrを試した。glm-ocrでは、日本語のスキャンが弱く、中国語簡体字や「バッテリー」が「バッチリー」など揺らぎスキャンがたびたび発生したので、Deepseek-OCRをOCRエンジンの主軸とした。1500PDFファイルに対して、夏休みからコツコツOCRをしていたが、OCRは1週間かけてもおわらなかった。15年のツケは重い。9月に入ってからもLLMを24hずっと稼働させて3週間ぐらいかかった。
LLMが動くMac miniに対して、Windowsからジョブ注入をしており、WindowsUpdateやPCの異常シャットダウンがたびたびおき、処理の手戻りが何度か発生した。ローカルでSQLiteのように簡単に使えてランタイムも不要でpowershellから叩けるLiteDBを使って、作業全体のACID整合性を保った。LiteDBとの出会いがこの夏1番の良い出会いだった。
自炊した本の特徴として、傾きがゆがんでいるので補正する必要があったり、技術書ならではの図表がちりばめられれているが重要な示唆もあるのでもれなく取得する必要があるなど、単に文字をテキスト化する以上の処理が自炊データのOCRには必要になる。品質良くOCRを実施するため、一つのPDFごとに直列に手順を分割してOCRさせた。話題のOCRプロジェクトもあったが、手元にCUDAを使える環境がなかったのと、Mac mini+VLMの組み合わせでどこまでできるか確認したかったので、以下手順によりOCRを進めた。
OCRパイプライン(手順2-5はMac miniで実施)
- OpenCVにより傾き補正(CPUで簡易にできる処理を選択)
- Deepseek-OCRのGroundingで図表の存在を取得 (dpi:300px)
- Deepseek-OCRで文章部分をテキスト化 (dpi:600px)
- glm-ocrで表内のテキストを再取得
- qwenにテキストを投げて日本語の誤スキャン文字を訂正
- 最後にmarkdownファイルにコンポーズ
Groundingの詳細はmodel説明を参照
https://huggingface.co/deepseek-ai/DeepSeek-OCR
昨今話題の超解像+OCRをするプロジェクト
https://github.com/dnobori/DN_SuperBook_PDF_Converter
RAGの作成
PDFのOCRができたので、qwen3-embedding:8bでベクトル化をした。データベースはQuadrant、チャンク単位は800tokenに120tokenをラップさせた。RAGを作ってみるまでに分からなかったことだが、RAG検索は主にベクトル検索、ランク処理、要約の3つの処理で構成される。
| 処理 | 処理時間 | 内容 |
|---|---|---|
| ベクトル検索 | 4秒 | ベクトルの近しいチャンクを抽出(上位50件) |
| ランク処理 | 4秒 | 意味論で近しいチャンクを再ランク |
| 推論による要約生成 | 40秒~3分 | 上位数件の情報を元に問いに合うように回答 |
ベクトル処理は線形代数演算、ランク処理も300M程度の言語モデルによる処理なので数秒の処理で実現ができるが、推論はどうしてもパラメータ数の多いLLMを通すので遅くなる。ただ、推論を通すことで8B程度のLLMでも高品質な要約を返してくれて質問に対する明確な回答が得られる。この辺りは使い方次第で、ランク処理まででやめて早い回答を得たり、最後まで実施して高品質な回答を得られるようにするなど、用途によって使い分けてもいいなと感じた。
最後に
今回初めてローカルLLMを生産的な用途で使ってみた(これまではお勉強的なパターンのトライアルのみ)。自分が持っている情報に対してRAG検索ができることは、想像以上に素晴らしい。今回のワークを経て、Mac mini程度で動作するローカルLLMモデルは、OCRやチャンク化などのバッチ処理に向いているなと改めて感じた。さらに、RAG観点で言えば、ベクトル化されたデータの検索自体(ベクトル検索)にはLLMも不要であるし、数秒での応答など意外とレイテンシーがよい。例えばベクトル検索の結果をフロンティアモデルを使う際のMCPサーバーの応答として回答を返すなど、RAGだけでないうまい使い方があるなと思った。



