OllamaのローカルAPIを使えば、文書の埋め込み、類似検索、LLMによる回答生成までを自分のPC内でつなげられます。ただし、Ollamaにはクラウドモデル・クラウド機能もあるため、「インストールしただけで完全オフライン」とは限りません。この記事では、ローカルモデルだけを使う最小RAGをNode.jsで実装し、ループバック接続での実行結果と、外部通信を遮断して確認する手順を分けて示します。
対象は、RAGの仕組みを理解しながら小さな社内資料や個人ノートで試したい人です。Web収集、OCR、大規模ベクトルデータベース、認証付き社内公開、モデル比較は扱いません。本番化するときは、この記事の最小構成に権限管理、監査、バックアップ、評価を追加してください。
情報確認日:2026年8月17日(日本時間)
完全ローカルのRAGは7段階で確認する
最小構成
- 固定:Ollamaをループバックアドレスへバインドする
- 無効化:Ollamaのクラウド機能を明示的に止める
- 準備:ネットワーク接続中にローカルモデルをダウンロードする
- 索引:文書をチャンクに分け、ローカル埋め込みを作る
- 検索:質問ベクトルと近いチャンクをcosine similarityで選ぶ
- 生成:選んだ根拠だけをローカルのチャットモデルへ渡す
- 検証:外向き通信を遮断して同じ処理が完了するか記録する
| 処理 | この記事の実装 | 外部サービス |
|---|---|---|
| 埋め込み | 127.0.0.1:11434/api/embed |
使わない |
| ベクトル保存 | プロセスメモリ上の配列 | 使わない |
| 類似検索 | Node.jsのcosine計算 | 使わない |
| 回答生成 | 127.0.0.1:11434/api/chat |
使わない |
| 出典表示 | アプリケーション側の文書IDとURL | 使わない |
ローカルLLMのRAGはどんな構成になる?
local documents
↓ chunk
POST /api/embed ← local embedding model
↓ vectors + document metadata
query → POST /api/embed → cosine similarity → top 3 chunks
↓
POST /api/chat
↓
answer + verified sources
RAGは、質問に近い根拠を検索し、その根拠と質問をLLMへ渡して回答させる構成です。モデルが知識を永続的に学習するファインチューニングとは異なります。RAGそのものの仕組みと向き不向きはRAGの基本、基本用語はLLMとは何か、ベクトル検索の考え方はベクトルデータベース入門で確認できます。
このサンプルではベクトルデータベースを使わず、少数チャンクをメモリ上で総当たりします。仕組みを追いやすい一方、文書量が増えると検索時間、起動時の再インデックス、更新管理が問題になります。
フレームワーク非依存のNode.js RAG全体、段階別テスト、永続ベクトルストアへの接続はNode.jsでRAGを実装する方法へ譲ります。本記事の固有範囲は、Ollamaのローカルモデル、ループバック固定、クラウド無効化、外向き通信の検証です。
「Ollamaならオフライン」と言い切れない理由
Ollamaの公式資料は、ローカルAPIとクラウドAPIを区別しています。取得済みのローカルモデルをlocalhostで実行する処理はローカルにできますが、モデル ダウンロードにはネットワークが必要です。また、現行Ollamaにはクラウドモデル・クラウド機能があり、名前に-cloudを含むモデルを選べばプロンプトはローカルモデルだけでは処理されません。
ローカル限定の条件:ローカルモデル名を指定し、接続先URLをhttp://127.0.0.1:11434へ固定し、クラウド機能を無効化します。そのうえで外向き通信を遮断した再現テストを行います。「localhostと書いた」だけでは通信停止の証明になりません。
必要なものと完成条件
- Ollamaが動作するmacOS、Windows、またはLinux環境
- Node.js 18以上。この記事のサンプルはbuilt-in
fetchを使う - ローカル埋め込みモデル
embeddinggemma - ローカル生成モデル。検証済みサンプルの既定値は
qwen3:1.7b - 出典ID・title・URLを持つ小さな文書セット
完成条件は、ネットワーク接続中と外向き通信遮断中の両方で、同じ質問に対して検索と生成が完了し、Ollamaがループバックアドレスだけで待ち受けしていることです。回答文が同一になる必要はありません。
手順1:ローカルモデルを先にダウンロードする
モデルファイルは先に取得します。生成モデルはPCのメモリ、利用言語、ライセンスに合うローカルモデルに置き換えてください。モデル名の末尾が-cloudのものは、この手順の対象外です。Ollamaの導入やモデル取得の基本操作が初めてなら、Ollamaの使い方を先に確認してください。
ollama pull embeddinggemma
ollama pull qwen3:1.7b
ollama ls
ollama lsに両方が表示されることを確認します。モデルは更新され得るため、検証記録には名前だけでなく、実行日、size、表示された更新時刻も残します。重要用途ではモデルファイルの供給元、ライセンス、バージョン管理方針も別途定めてください。
手順2:Ollamaのクラウド機能を無効化する
公式FAQでは、利用者設定ファイルのdisable_ollama_cloudまたは環境変数OLLAMA_NO_CLOUD=1でクラウド機能を無効化できます。継続して使う端末では、設定ファイルのほうが起動シェルに依存しにくくなります。
{
"disable_ollama_cloud": true
}
上記をOllamaの利用者設定ディレクトリにあるserver.jsonに設定し、Ollamaを完全に再起動します。一時的に環境変数を使う場合は、Ollamaサーバーを起動するプロセスへ値を渡します。
OLLAMA_NO_CLOUD=1 ollama serve
すでにデスクトップアプリ側のサーバーが動いている状態で別のollama serveを起動するとポート競合になる場合があります。実行方法は一つに統一し、起動ログにOllama cloud disabled: trueがあることを確認します。
手順3:バインドアドレスを127.0.0.1に保つ
Ollamaは既定で127.0.0.1:11434へバインドします。ローカルAPIは認証不要なので、RAGを同じPC内で動かすだけならOLLAMA_HOST=0.0.0.0へ変更しません。
避ける設定:0.0.0.0へのバインドやルーターのポート転送は、ローカル限定の境界を変えます。別PCから使う要件がある場合は、リバースプロキシ、TLS、認証、ファイアウォール、利用者制限を含む別設計としてレビューしてください。
macOS・Linuxではlsofなど、WindowsではGet-NetTCPConnectionなどOSのソケット確認ツールを使い、11434番ポートのローカルアドレスを記録します。
手順4:文書をID付きチャンクへ分ける
検索結果から元文書へ戻れるように、チャンクに一意なID、文書title、正規URL、本文を持たせます。サンプルは固定配列ですが、実運用では取得日時、改訂日時、公開状態、アクセス権限も保持します。
| フィールド | 役割 | 例 |
|---|---|---|
id |
回答と根拠を結び直す | support-hours:0 |
title |
人が根拠を識別する | サポート受付時間 |
url |
検証可能な参照先 | 公式ページURL |
text |
検索と回答に使う本文 | 営業時間と例外条件 |
チャンクのサイズを唯一の正解として固定しません。見出し、段落、表、例外条件を壊さないことを優先し、質問に必要な情報が一つのチャンクに収まるかを評価します。
手順5:localhostだけを呼ぶ最小RAGを作る
次のlocal-rag.mjsは、パッケージを追加せずに動く学習用サンプルです。3文書を分割し、/api/embedでインデックスとクエリをベクトル化し、近い3チャンクを/api/chatへ渡します。
const OLLAMA_URL = process.env.OLLAMA_URL ?? "http://127.0.0.1:11434";
const EMBED_MODEL = process.env.EMBED_MODEL ?? "embeddinggemma";
const CHAT_MODEL = process.env.CHAT_MODEL ?? "qwen3:1.7b";
if (new URL(OLLAMA_URL).hostname !== "127.0.0.1") {
throw new Error("OLLAMA_URL must use 127.0.0.1 in this local-only sample");
}
const documents = [
{
id: "support-hours",
title: "サポート受付時間",
url: "https://example.com/support/hours",
text: "平日の受付は10時から17時です。土日祝日は休業です。緊急窓口はありません。"
},
{
id: "returns",
title: "返品条件",
url: "https://example.com/policy/returns",
text: "未使用品は到着後7日以内に申請してください。開封済み商品は対象外です。"
},
{
id: "shipping",
title: "発送の目安",
url: "https://example.com/guide/shipping",
text: "在庫品は決済確認後2営業日以内に発送します。予約品は商品ページの予定日を優先します。"
}
];
function chunkText(document, size = 180, overlap = 30) {
const chunks = [];
for (let start = 0, number = 0; start < document.text.length; number += 1) {
const text = document.text.slice(start, start + size);
chunks.push({
id: `${document.id}:${number}`,
title: document.title,
url: document.url,
text
});
if (start + size >= document.text.length) break;
start += size - overlap;
}
return chunks;
}
async function embed(input) {
const response = await fetch(`${OLLAMA_URL}/api/embed`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ model: EMBED_MODEL, input })
});
if (!response.ok) throw new Error(`embed failed: ${response.status}`);
return (await response.json()).embeddings;
}
function cosine(a, b) {
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i += 1) {
dot += a[i] * b[i];
normA += a[i] ** 2;
normB += b[i] ** 2;
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
const chunks = documents.flatMap((document) => chunkText(document));
const vectors = await embed(chunks.map((chunk) => chunk.text));
const index = chunks.map((chunk, i) => ({ ...chunk, vector: vectors[i] }));
const question = process.argv.slice(2).join(" ") || "土曜日に問い合わせできますか?";
const [queryVector] = await embed(question);
const hits = index
.map((chunk) => ({ ...chunk, score: cosine(queryVector, chunk.vector) }))
.sort((a, b) => b.score - a.score)
.slice(0, 3);
const context = hits
.map((hit) => `[${hit.id}] ${hit.title}\n${hit.text}`)
.join("\n\n");
const response = await fetch(`${OLLAMA_URL}/api/chat`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
model: CHAT_MODEL,
stream: false,
keep_alive: "5m",
messages: [
{
role: "system",
content: "根拠だけで日本語回答してください。不明なら不明と答え、各文末に根拠IDを角括弧で示してください。"
},
{ role: "user", content: `質問: ${question}\n\n根拠:\n${context}` }
]
})
});
if (!response.ok) throw new Error(`chat failed: ${response.status}`);
const answer = (await response.json()).message.content;
const usedIds = [...answer.matchAll(/\[([a-z0-9-]+:\d+)\]/gi)].map((match) => match[1]);
const sources = [...new Map(
hits.filter((hit) => usedIds.includes(hit.id)).map((hit) => [hit.url, hit])
).values()];
console.log(answer);
console.log("\n検証用source:");
for (const source of sources) console.log(`- ${source.title}: ${source.url}`);
手順6:サンプルを実行して検索結果を見る
node local-rag.mjs "土曜日に問い合わせできますか?"
node local-rag.mjs "予約商品の発送日はいつですか?"
最初の質問では、平日10時から17時、土日祝日は休業という根拠が上位に入り、土曜日は通常窓口を利用できない旨が返るのが期待値です。二つ目では、予約品は各商品ページの予定日を優先する根拠が必要です。
文章の自然さだけで合格にしません。ターミナルへ表示された出典URLを人が開き、回答の各主張が対応チャンクに書かれているかを確認します。サンプルのexample.comは説明用なので、自分が管理・利用許可を持つ正規URLへ置き換えてください。
掲載サンプルの実測結果
2026年8月17日04:04 JSTに、Ollama 0.32.5、Node.js v24.13.0、embeddinggemma:latest(621 MB、ID 85462619ee72)、qwen3:1.7b(1.4 GB、ID 8f68893c685c)で掲載サンプルを実行しました。lsofではOllamaが127.0.0.1:11434だけで待ち受けしていることを確認しました。
$ node local-rag.mjs "土曜日に問い合わせできますか?"
土曜日に問い合わせることはできません。
根拠: [support-hours:0]
検証用source:
- サポート受付時間: https://example.com/support/hours
$ node local-rag.mjs "予約商品の発送日はいつですか?"
予約商品の発送日は、商品ページに記載された予定日を優先して行われます。
根拠ID: [shipping:0]
検証用source:
- 発送の目安: https://example.com/guide/shipping
接続先の対照試験としてOLLAMA_URL=https://example.comを指定すると、HTTPリクエストの前にOLLAMA_URL must use 127.0.0.1で停止しました。実行用ファイルはnode --checkを通過し、SHA-256は74ad62b312dc8f0dcf49e0f583809a6e2cfa11ea215b149e39eecda0a47104a1です。
外部通信を遮断した状態でも2問を完走
04:20:10〜04:20:36 JSTには、一時的なOllamaサーバーとNode.jsのサンプルの両方をmacOSのプロセスのサンドボックス内で実行しました。ポリシーは全ネットワークを拒否した後、localhostの受信 / 送信だけを許可する構成です。OllamaはOLLAMA_NO_CLOUD=1、127.0.0.1:11435で起動し、ログにOllama cloud disabled: trueとListening on 127.0.0.1:11435が出ました。
localhost:11435/api/version → {"version":"0.32.5"}
https://example.com → Could not resolve host
Case 1 embed / chat → HTTP 200、support-hours:0を引用
Case 2 embed / chat → HTTP 200、shipping:0を引用
一時サーバーは04:20:45 JSTまでに停止し、プロセスポリシーも終了時に解除されました。永続的なファイアウォールルールは作成していません。詳細なコマンド、時間、出力、対象範囲は同じ記事バンドルのverification-log.mdに残しています。
実測の限界:この試験はOllamaサーバーとRAGクライアントという対象プロセスの外向き通信を遮断した結果です。OS全体のファイアウォールやネットワークインターフェースは変更していないため、同時に動く無関係なプロセスまで「外部通信なし」とする証明ではありません。本番では運用単位全体を隔離ネットワークまたは既定拒否のファイアウォールへ置き、監視も同じ境界で確認してください。
同じ埋め込みモデルを使う理由
インデックス作成時と質問時で別の埋め込みモデルを使うと、ベクトルの次元や意味空間が一致せず、距離を比較できません。公式資料もインデックスとクエリで同じモデルを使うよう案内しています。
/api/embedが返すベクトルはL2-normalizedされています。正規化済みベクトルでは内積とcosine similarityの順位は一致しますが、サンプルは処理の意味が分かるようにcosineを明示計算しています。
出典URLをLLMに作らせない
LLMへURLを自由生成させると、存在しないパスや似た別ページを出す恐れがあります。サンプルはLLMへチャンクIDだけを書かせ、アプリケーション側が検索結果のメタデータからURLを引き直します。
生成結果に未知のIDがあっても出典一覧に追加しません。本番では、回答の主張と出典の対応、無効ID、引用漏れを検査してください。引用設計を深める場合はRAGで出典を正しく表示する方法を参照できます。
外部通信なしをどう検証する?
「APIキーを設定していない」だけでは不十分です。モデルを取得した後、OS ファイアウォール、隔離ネットワーク、または検証用コンテナ・VMで外向き通信を止め、RAG処理が完了するかを再実行します。業務端末のネットワーク設定を直接変える場合は、管理者の手順に従ってください。
| 確認項目 | 記録する値 | 合格例 |
|---|---|---|
| Ollamaのバージョン | 検証時のバージョン | 再現可能な値がある |
| モデル一覧 | 名前、サイズ、更新時刻 | ローカルモデルだけを指定 |
| クラウド設定 | 起動ログ | cloud disabled: true |
| 待ち受けアドレス | プロセス、アドレス、ポート | 127.0.0.1:11434 |
| 外向き通信 | 遮断方法、開始・終了時刻 | 既定拒否またはネットワーク切断 |
| 埋め込み | HTTPステータス、モデル、件数 | localhostで成功 |
| chat | HTTPステータス、モデル、回答ID | localhostで成功 |
| 外部到達テスト | 対象、結果 | 遮断中は失敗 |
検証用ログにプロンプト本文や機密文書を丸ごと残す必要はありません。テストケースID、ハッシュ、件数、ステータス、経過時間など、再現に必要な最小メタデータへ絞ります。
「ローカルなのに通信した」場合の切り分け
OLLAMA_URLがクラウドホストやLANアドレスになっていないか確認する- 指定モデル名に
-cloudが含まれないか確認する - クラウド無効化後にOllamaプロセスを再起動したか確認する
- RAGアプリケーション内のアクセス解析、エラー追跡、フォント、CDN、更新確認を確認する
- 文書loaderやOCRが外部APIを呼んでいないか確認する
- OSのプロセス別ネットワークログで通信元を特定する
Ollamaだけをローカル化しても、アプリケーションの可観測性や文書処理が外部サービスを使えばシステム全体は完全ローカルではありません。検証範囲を「Ollamaプロセス」ではなく「RAGを動かす一連のプロセス」にします。
回答が不正確なときの戻り先
回答が不正確
├─ 正しいchunkがtop 3にある?
│ ├─ No → 文書・chunk・embedding・検索を見直す
│ └─ Yes
├─ promptに必要な条件が全部ある?
│ ├─ No → top-k・重複除去・複数sourceを見直す
│ └─ Yes
├─ 根拠にないclaimがある?
│ ├─ Yes → instruction・model・拒否条件を見直す
│ └─ No
└─ source IDとURLが一致する?
├─ No → metadata mappingを修正する
└─ Yes → test caseへ追加する
設定変更後は、期待した一問だけでなく、通常質問、言い換え、複数文書、答えなし、更新済み文書を含むテストセットを再実行します。評価の作り方はRAG評価のやり方で具体化しています。
この最小構成を本番へ持ち込めない理由
- 再起動するとメモリ上のインデックスが消える
- 文書の追加・更新・削除を自動反映しない
- 全チャンクを総当たりするため規模に比例して遅くなる。候補が増えたら一次検索の後段にリランキングを置く設計も検討対象になる
- 利用者ごとの閲覧権限を検索前にフィルターしない
- プロンプトインジェクションや悪意ある文書を検査しない
- 同時実行数、タイムアウト、メモリ不足を制御しない
- バックアップ、監査ログ、削除証跡がない
次の段階では、永続ベクトルストア、文書バージョン、アクセス制御、入力・出力制限、監視、バックアップを追加します。機密データを扱う場合は、ローカル実行だけをセキュリティ対策の完成と考えないでください。共有や公開の前に確認する項目はローカルLLMのセキュリティ対策で30項目に整理しています。
モデルをメモリから外す方法
Ollamaはkeep_aliveでモデルを一定時間メモリに保持できます。共有PCや検証終了後に早く解放したい場合は、APIでkeep_alive: 0を指定するか、CLIで停止します。
ollama ps
ollama stop qwen3:1.7b
ollama stop embeddinggemma
メモリから解放してもモデルファイルはストレージに残ります。ストレージ上の削除、バックアップ、端末廃棄は別のデータのライフサイクルとして管理します。
公開前チェックリスト
- ローカル埋め込みモデルとローカル生成モデルをダウンロードした
-cloudモデルを指定していない- クラウド機能を無効化し、Ollamaを再起動した
- 接続先URLを
127.0.0.1:11434に固定した - 11434番ポートがループバックアドレスだけで待ち受けしている
- インデックスとクエリに同じ埋め込みモデルを使った
- チャンクにID、title、正規URLを保存した
- URLをLLMに生成させずメタデータから解決した
- 外向き通信を遮断した状態でembedとchatが成功した
- 回答の各主張を出典本文と人が照合した
- 検証日時、バージョン、モデル、遮断方法、結果を記録した
よくある質問
Ollamaをインストールすれば、入力は絶対に外へ出ませんか?
インストールだけでは保証できません。ローカルモデルとlocalhost APIを選び、クラウド機能を無効化し、RAGアプリケーションを含む全プロセスの外向き通信を検証してください。
ベクトルデータベースは必須ですか?
少数チャンクの学習用サンプルでは必須ではありません。文書量、更新頻度、同時利用者が増えたら、永続化、近似近傍検索、メタデータフィルター、バックアップを備えたストアを検討します。
埋め込みと回答生成に同じモデルを使いますか?
役割は別です。文書とクエリは同じ埋め込みモデルでベクトル化し、回答生成はchat用のローカルモデルを使います。
localhost APIにAPIキーは必要ですか?
OllamaのローカルAPIは既定で認証不要です。そのためループバックだけにバインドし、LANやInternetへ露出しないことが重要です。クラウドAPIには認証が必要です。
外部通信を切れば安全なRAGになりますか?
データ流出経路の一部は減らせますが、安全性の完成ではありません。端末権限、malware、バックアップ、ログ、閲覧権限、プロンプトインジェクション、モデルの供給網も対策が必要です。
完全ローカルRAGはデータ経路まで確認する
OllamaでローカルRAGを作るときは、/api/embedで同じ埋め込みモデルを使って文書と質問をベクトル化し、近いチャンクだけを/api/chatへ渡します。出典はLLMにURLを作らせず、チャンクIDからアプリケーション側のメタデータへ戻します。
完全ローカルを名乗る条件は、localhostを指定したコードだけではありません。クラウド機能を無効化し、ローカルモデルを使い、ループバックバインドを確認し、外向き通信を遮断して処理が完了することを記録してください。この最小構成で流れを理解した後、永続化、権限、security、評価を一つずつ追加します。