プログラミング

ローカルRAGが遅いのはLLMじゃなく埋め込みだった話

ローカルでRAGを組んだときの「遅い・効かない」の原因が、生成LLMではなく埋め込みモデルの側にあった記録です。CPUでの `bge-m3` が1チャンク30〜60秒かかって大きな文書が入らなかった件、英語中心の埋め込みモデルで日本語検索が無言で0件になった件、そしてOllamaが2つ動いてポートを奪い合っていた件をまとめました。

目次

はじめに

手元の文書をAIに検索させて答えさせる仕組み(RAG)を、ローカルで自前に組んでみたことのある方はいませんか。
やってみると、たいてい最初にぶつかるのが「遅い」という壁です。

私もローカルRAGを組んでいて遅さに悩んだのですが、てっきり「生成するLLMが重いせいだ」と思い込んでいました。
実際に計ってみたら、それは見当違いでした。

結論を先に書きます。
ローカルRAGの律速は、多くの場合「生成」ではなく「埋め込み(embeddings)」です。
さらに、日本語RAGの質も、生成モデルではなく埋め込みモデルで露骨に変わります。
RAGをチューニングするとき、まず生成側を疑いがちですが、見るべき順番が違った、という話を共有します。

この記事は、OSS版NotebookLMを自前で動かす検証の中で、埋め込みまわりで実際に詰まった記録に基づいています。

まず用語:「埋め込み」がRAGの検索の心臓

RAGは大きく2つの処理でできています。

  • 埋め込み(embeddings):文書を「意味の近さで検索できる数字の列」に変換する処理。これを事前にやっておくことで、質問が来たときに「意味が近い文書」を探せるようになります。
  • 生成:検索で見つかった文書を渡して、AIに答えを書かせる処理。

「AIが答える」というと2の生成ばかり意識しがちですが、検索の精度も速度も、実は1の埋め込みが握っています。
今回ハマったのは、まさにこの1の方でした。

真犯人①:埋め込みが遅すぎて、大きな文書が入らない

ローカルの埋め込みには、Ollamaで動かす bge-m3 というモデルを使っていました。
日本語に強い良いモデルなのですが、CPUで動かすと、1チャンク(文書を区切った断片)の埋め込みに30〜60秒かかりました。
モデルは常駐させていて、起動待ちではなくこの速度です。

小さな文書なら数チャンクなので問題は表面化しません。
ところが、Wikipediaの記事(抽出すると441チャンク)を入れようとした瞬間に破綻しました。
埋め込みが終わる前に処理がタイムアウトし、リトライで最初からやり直しになり、それがまた終わらずにリトライ……という無限ループに陥って、いつまで経っても完了しないのです。
しかもセッションが長引くとCPUとメモリが逼迫して、序盤は数秒だったチャンクがさらに遅くなる、という悪循環まで起きました。

ここで腹落ちしたのが、「生成(claude側)は普通に動いているのに、埋め込みのせいでそもそも文書が入らない」という構図です。
RAGが遅い・進まないとき、生成LLMを疑う前に、まず埋め込みの所要時間を計るべきでした。

対処の方向性としては、次のようなものになります。

  • 軽量な埋め込みモデルに変える(nomic-embed-text など)。品質と引き換えに速度を取る。
  • GPUを使う。CPU埋め込みは大量・大型の文書だと現実的でない。
  • 入れる文書を小さく刻む。大きなWeb記事を丸ごと入れない。

bge-m3 は品質は高いのですが、CPUでは大きい文書に向かない、というのが実測の結論でした。

真犯人②:埋め込みモデルが日本語に弱いと、検索が「無言で」0件になる

もう1つ、別のツール(AnythingLLM)では、速度ではなく精度の方で埋め込みに泣かされました。
こちらは内蔵の all-MiniLM-L6-v2 という埋め込みモデルを使っていたのですが、日本語の文書を入れて質問すると「分かりません」しか返ってこないのです。

おかしいので、データベース(SQLite)を直接覗いて切り分けました。
すると、次のような状態でした。

  • 文書はワークスペースに紐付いている
  • 埋め込みも成功している(チャンクはちゃんと格納されている)
  • なのに検索結果は0件

埋め込めているのに、検索が無言で0件になる。
これが一番気味の悪いパターンです。

真因は、all-MiniLM-L6-v2 が英語中心のモデルで日本語が弱いことでした。
日本語の質問と日本語の文書の「意味の近さ」のスコアが低く出てしまい、既定のしきい値(0.25)を全チャンクが下回って、まるごとふるい落とされていたのです。
エラーも警告も出ないので、原因にたどり着くまで時間がかかりました。

応急処置は、しきい値を No restriction(0)にして、ふるい落としを止めることです。
本筋の対処は、埋め込みモデルを日本語対応のもの(bge-m3multilingual-e5 など)に替えることです。
実際、bge-m3 を使ったOpen Notebook側では、しきい値を一切いじらなくても日本語検索がちゃんと当たりました。
同じ文書・同じ質問でも、埋め込みモデルを替えるだけで結果が変わるのを目の当たりにして、埋め込みの効きの大きさを実感しました。

余談:2つのOllamaがポートを奪い合っていた

埋め込みまわりでもう1つ、Windowsならではの落とし穴を踏んだので書いておきます。
「モデルが見つからない」「埋め込みの挙動がおかしい」とき、Ollamaが2つ動いていてポートを奪い合っていることがありました。

具体的には、自分で入れたスタンドアロンの ollama.exe と、AnythingLLMが内部で同梱している llm.exe が、両方とも同じポート(:11434)を使おうとしていたのです。
どちらが先に握っているかで、見えるモデルが変わってしまいます。

こういうときは、ポートを誰が握っているかを直接確認するのが早いです。

Get-NetTCPConnection -State Listen -LocalPort 11434 | Select-Object OwningProcess

出てきたプロセスIDをタスクマネージャーで照合すれば、どちらのOllamaが :11434 を握っているか一目で分かります。
原因不明の「モデルが見えない」に遭遇したら、まずここを疑うと早いです。

まとめ

いかがでしたか。
今回は、ローカルRAGを組んだときに「遅い・効かない」原因が、思っていた生成LLMではなく埋め込みの方にあった、という話をまとめました。

要点を振り返ると、次の3つです。

  1. RAGが遅い・進まないとき、まず疑うべきは生成ではなく埋め込みです。CPUの bge-m3 は1チャンク30〜60秒かかり、大きな文書はタイムアウトのループで入りませんでした。
  2. 大量・大型を扱うなら、軽量な埋め込みモデルかGPUが要ります。品質の高いモデルほどCPUでは重いです。
  3. 日本語RAGの精度は埋め込みモデルで決まります。英語中心のモデルは「埋め込めているのに検索0件」を無言で起こします。しきい値の緩和は応急処置で、本筋は日本語対応モデルへの差し替えです。

RAGのチューニングというと、つい生成モデルやプロンプトに目が行きがちです。
でも、ローカルで自前に組むなら、まず埋め込みの速度と日本語耐性を見る。
これだけで原因究明の順番がだいぶ変わると思います。

埋め込みモデルの選定やしきい値の決め方は、自分の文書の傾向によっても変わるので、ClaudeやGeminiに「日本語のローカルRAGに向く埋め込みモデルは?」と相談しながら詰めるのもいいと思います。
同じところで時間を溶かす人が少なくなればうれしいです。

この記事が誰かの役に立てばうれしいです。