TurboVecで作る完全無料・完全ローカルRAGチャットボット
はじめに
「RAGを使いたいけど、クラウドAPIにデータを送りたくない」
「コストをかけずにローカルだけで動かしたい」
製造業や医療など、機密データを扱う現場ではこういった要件が多いと思います。
本記事では、2026年にICLR採択されたGoogle Research/NYUの研究チームによるアルゴリズム TurboQuant(論文:arXiv:2504.19874)を、ベクトル検索インデックス向けに応用したライブラリ TurboVec と、ローカルLLMランタイム Ollama を組み合わせて、完全無料・完全ローカル で動作するRAGチャットボットを構築します。
今回は、Macで動作確認していますが、RaspberryPiなどでも動作すると思います。
RaspberryPiなどでは軽いモデルを使ったりする必要があると思われるので、モデルの切り替えも1行で完完結するようにしています。
TurboVecとは
TurboVec は、Rust製のベクトルインデックスライブラリです。Pythonバインディングが用意されており、pip install turbovec で導入できます。
何がすごいのか
最大の特徴は 16倍のメモリ圧縮 です。
たとえば OpenAI の text-embedding-3-small(1536次元)で1000万件を埋め込む場合:
| 方式 | メモリ使用量 |
|---|---|
| float32 そのまま | 約 31 GB |
| TurboVec(2bit圧縮) | 約 4 GB |
Raspberry Piのような制約の多いデバイスでも、大規模なベクトルインデックスを扱えるようになります。
なぜ学習不要なのか
従来のProduct Quantization(PQ)は「コードブック」を事前にデータから学習する必要がありました。TurboQuantは高次元ベクトルの数学的性質(ランダム回転後にガウス分布に近似する性質)を利用するため、データを見なくても最適な量子化境界を計算できます。
1. 正規化 — ベクトルの大きさと向きを分離
2. ランダム回転 — 座標をガウス分布に近い形に変換
3. 数学的量子化 — 分布が既知なので学習なしで最適境界を算出
ベクトルを追加するだけでインデックスが完成するため、コーパスが増えてもリビルドが不要です。
FAISSとの比較
| 比較項目 | FAISS | TurboVec |
|---|---|---|
| コードブック学習 | 必要 | 不要 |
| オンライン追加 | 困難 | 可能 |
| ARM(ラズパイ・Apple Silicon)速度 | 基準 | 多くの構成で12〜20% 高速 (※2ビットMTの一部構成を除く) |
| メモリ効率 | 普通 | 最大16倍 |
| ライセンス | MIT | MIT |
※ベンチマーク条件:100Kベクトル、k=64、Apple M3 Max / Intel Sapphire Rapids。TurboVec公式READMEより。
ARM NEON SIMDカーネルで最適化されているため、Raspberry PiやApple Silicon Macでは特に効果を発揮します。
FAISSとは
大量のベクトルの中から「似ているもの」を高速に探すためのライブラリで、RAGシステムの検索エンジンとして広く使われています。名前は Facebook AI Similarity Search の略です。
システム構成
質問入力
│
▼
SentenceTransformer(all-MiniLM-L6-v2)
│ テキスト → 384次元ベクトル
▼
TurboQuantIndex.search()
│ ARM NEON SIMD で高速 ANN 検索
▼
上位3件のドキュメントを取得
│
▼
Ollama(gemma3:1b または gemma3:4b)
│ 完全ローカル LLM で回答生成
▼
回答出力(外部通信なし)
モデル選択の目安
| モデル | 必要RAM | 推奨環境 | 品質 | 速度 |
|---|---|---|---|---|
gemma3:1b |
2GB〜 | Raspberry Pi 4(4GB) | △ 実用的 | ◎ |
gemma3:4b |
6GB〜 | Raspberry Pi 5(8GB)/ Mac | ○ 高品質 | ○ |
環境構築
1. Ollama のインストール
curl -fsSL https://ollama.com/install.sh | sh
ollama serve &
Macの場合は、brewコマンドでインストールしているとバージョンが古く使えない可能性があります。
2. LLMモデルのダウンロード
ollama pull gemma3:1b
ollama pull gemma3:4b
[GIN] 2026/06/08 - 07:29:02 | 200 | 278.709µs | 127.0.0.1 | HEAD "/"
pulling manifest ⠹ time=2026-06-08T07:29:03.537+09:00 level=INFO source=download.go:179 msg="downloading aeda25e63ebd in 16 208 MB part(s)"
pulling manifest
pulling aeda25e63ebd: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████▏ 3.3 GB tpulling manifest
pulling aeda25e63ebd: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████▏ 3.3 GB
pulling manifest
pulling aeda25e63ebd: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████▏ 3.3 GB
pulling manifest
pulling aeda25e63ebd: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████▏ 3.3 GB
pulling manifest
pulling manifest
pulling aeda25e63ebd: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████▏ 3.3 GB
pulling e0a42594d802: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████▏ 358 B
pulling dd084c7d92a3: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████▏ 8.4 KB
pulling 3116c5225075: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████▏ 77 B
pulling b6ae5839783f: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████▏ 489 B
verifying sha256 digest
writing manifest
success
ollama list
[GIN] 2026/06/08 - 07:31:24 | 200 | 42.75µs | 127.0.0.1 | HEAD "/"
[GIN] 2026/06/08 - 07:31:24 | 200 | 821.125µs | 127.0.0.1 | GET "/api/tags"
NAME ID SIZE MODIFIED
gemma3:4b a2af6cc3eb7f 3.3 GB About a minute ago
3. Rust のインストール(turbovecのビルドに必要)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
4. Python環境のセットアップ
mkdir turbovec-rag-chatbot
cd turbovec-rag-chatbot
python3 -m venv .venv
source .venv/bin/activate
まずは、requirements.txt: を作ります。
turbovec>=0.2.0
ollama>=0.3.0
sentence-transformers>=3.0.0
numpy>=1.26.0
torch>=2.2.0
transformers>=4.40.0
pip install -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
Raspberry Piで pip install torch だけを先に実行してから pip install -r requirements.txt とすると、依存関係の解決が再度走り、重い標準版バイナリをダウンロードしようとする場合があります。上記のように --extra-index-url をまとめて指定するのが確実です。
実装コード
ファイル構成
turbovec-rag-chatbot/
├── chatbot.py # メインスクリプト
├── seed_docs.py # サンプルドキュメント投入
├── requirements.txt # 依存パッケージ
├── docs.json # ドキュメントストア(自動生成)
└── index.tv # TurboVec インデックス(自動生成)
docs.jsonとindex.tvは、自動生成されます。
chatbot.py
#!/usr/bin/env python3
"""
TurboVec RAG Chatbot — Ollama 対応版(完全無料・完全ローカル)
Ollama でローカル LLM を動かし、TurboVec で高速ベクトル検索を行う RAG チャットボット
"""
import json
import ollama
import turbovec
import numpy as np
from sentence_transformers import SentenceTransformer
from pathlib import Path
# ── 設定 ──────────────────────────────────────────────────────────────────
# モデルを切り替えたいときはここを変更するだけ
# "gemma3:1b" → RAM 2GB〜、Raspberry Pi 4 (4GB) でも動作
# "gemma3:4b" → RAM 6GB〜、高品質・Raspberry Pi 5 (8GB) 推奨
OLLAMA_MODEL = "gemma3:4b"
EMBED_MODEL = "all-MiniLM-L6-v2" # 軽量埋め込みモデル(384次元)
EMBED_DIM = 384
BIT_WIDTH = 4 # 2=高圧縮, 4=バランス, 8=高精度
INDEX_PATH = "index.tv"
DOCS_PATH = "docs.json"
TOP_K = 3
MAX_TOKENS = 512 # ローカルLLMはトークン数を絞ると高速
SYSTEM_PROMPT = """あなたは日本語で回答するRAGチャットボットです。
提供されたコンテキストのみを根拠に回答してください。
コンテキストに情報がない場合は「この質問に関する情報がドキュメントにありません」と答えてください。
回答は簡潔・正確にしてください。"""
# ── ドキュメントストア ────────────────────────────────────────────────────
class DocStore:
def __init__(self, path: str = DOCS_PATH):
self.path = Path(path)
self.docs: list[dict] = []
if self.path.exists():
self.docs = json.loads(self.path.read_text(encoding="utf-8"))
def save(self):
self.path.write_text(
json.dumps(self.docs, ensure_ascii=False, indent=2), encoding="utf-8"
)
def add(self, title: str, body: str) -> int:
doc_id = len(self.docs)
self.docs.append({"id": doc_id, "title": title, "body": body})
self.save()
return doc_id
def get_texts(self) -> list[str]:
return [f"{d['title']} {d['body']}" for d in self.docs]
def __len__(self):
return len(self.docs)
# ── TurboVec インデックス管理 ─────────────────────────────────────────────
class VectorIndex:
def __init__(self, dim: int = EMBED_DIM, bit_width: int = BIT_WIDTH):
self.dim = dim
self.bit_width = bit_width
self.index = turbovec.TurboQuantIndex(dim, bit_width)
def build(self, embeddings: np.ndarray):
# add() には shape [n, dim] の float32 2次元配列をそのまま渡す
vecs = np.ascontiguousarray(embeddings, dtype=np.float32)
self.index.add(vecs)
print(f" → {len(embeddings)} 件をインデックス化しました "
f"({self.bit_width}bit, 次元={self.dim})")
def search(self, query_vec: np.ndarray, k: int = TOP_K) -> list[int]:
# search() は shape [1, dim] の2次元配列を受け取り (scores, indices) を返す
q = np.ascontiguousarray(query_vec, dtype=np.float32)
if q.ndim == 1:
q = q.reshape(1, -1)
_scores, indices = self.index.search(q, k)
return indices[0].tolist()
def save(self, path: str = INDEX_PATH):
self.index.write(path)
print(f" → インデックスを保存しました: {path}")
def load(self, path: str = INDEX_PATH):
self.index = turbovec.TurboQuantIndex.load(path)
print(f" → インデックスを読み込みました: {path}")
# ── RAG パイプライン ──────────────────────────────────────────────────────
class RAGChatbot:
def __init__(self):
self.store = DocStore()
self.vi = VectorIndex()
self.embedder = None
print("📦 TurboVec RAG Chatbot (Ollama Edition) を起動中...")
self._check_ollama()
self._load_embedder()
self._load_or_build_index()
# ── 初期化 ──────────────────────────────────────────────────────────
def _check_ollama(self):
"""Ollama の起動確認とモデルの存在チェック"""
try:
models = [m.model for m in ollama.list().models]
if OLLAMA_MODEL not in models:
print(f"⚠️ モデル '{OLLAMA_MODEL}' が見つかりません。ダウンロードします...")
print(f" ollama pull {OLLAMA_MODEL}")
ollama.pull(OLLAMA_MODEL)
print(f" ✓ ダウンロード完了")
else:
print(f"✓ Ollama モデル確認: {OLLAMA_MODEL}")
except Exception as e:
raise RuntimeError(
f"Ollama に接続できません。起動しているか確認してください。\n"
f" macOS/Linux: ollama serve\n"
f" エラー詳細: {e}"
)
def _load_embedder(self):
print(f"🔤 埋め込みモデルをロード中: {EMBED_MODEL}")
self.embedder = SentenceTransformer(EMBED_MODEL)
print(" ✓ 完了")
def _load_or_build_index(self):
if Path(INDEX_PATH).exists() and len(self.store) > 0:
print(f"📂 既存のインデックスを読み込みます: {INDEX_PATH}")
self.vi.load()
elif len(self.store) > 0:
print("🔨 インデックスを新規構築します...")
self._rebuild_index()
else:
print("ℹ️ ドキュメントが未登録です。'add' コマンドで追加してください。")
def _rebuild_index(self):
if len(self.store) == 0:
print("⚠️ ドキュメントが空のためインデックスを構築できません。")
return
texts = self.store.get_texts()
print(f" エンベディング生成中 ({len(texts)} 件)...")
embeddings = self.embedder.encode(texts, show_progress_bar=True)
self.vi = VectorIndex(dim=embeddings.shape[1])
self.vi.build(embeddings)
self.vi.save()
# ── ドキュメント管理 ─────────────────────────────────────────────────
def add_document(self, title: str, body: str):
doc_id = self.store.add(title, body)
print(f" ✓ ドキュメント #{doc_id} '{title}' を追加しました。インデックスを再構築します...")
self._rebuild_index()
def list_documents(self):
if not self.store.docs:
print(" (ドキュメントなし)")
return
for d in self.store.docs:
preview = d["body"][:60].replace("\n", " ")
print(f" #{d['id']:3d} [{d['title']}] {preview}...")
# ── 検索・回答生成 ───────────────────────────────────────────────────
def retrieve(self, query: str) -> list[dict]:
if len(self.store) == 0:
return []
q_vec = self.embedder.encode([query])
indices = self.vi.search(q_vec, k=min(TOP_K, len(self.store)))
return [self.store.docs[i] for i in indices if i < len(self.store.docs)]
def answer(self, query: str) -> str:
top_docs = self.retrieve(query)
if not top_docs:
context = "(関連ドキュメントなし)"
else:
context = "\n\n".join(
f"【{d['title']}】\n{d['body']}" for d in top_docs
)
titles = ", ".join(f"「{d['title']}」" for d in top_docs)
print(f"\n 🔍 参照ドキュメント: {titles}")
response = ollama.chat(
model=OLLAMA_MODEL,
options={"num_predict": MAX_TOKENS},
messages=[
{"role": "system", "content": SYSTEM_PROMPT + f"\n\nコンテキスト:\n{context}"},
{"role": "user", "content": query},
],
)
return response["message"]["content"]
# ── REPL ────────────────────────────────────────────────────────────
def run(self):
print("\n" + "=" * 58)
print(f" 🤖 TurboVec RAG Chatbot — Ollama Edition")
print(f" 📡 使用モデル: {OLLAMA_MODEL}")
print("=" * 58)
print(" コマンド一覧:")
print(" /add — ドキュメントを追加")
print(" /list — ドキュメント一覧を表示")
print(" /rebuild — インデックスを再構築")
print(" /model — 現在のモデルを確認")
print(" /quit — 終了")
print("=" * 58 + "\n")
while True:
try:
user_input = input("あなた > ").strip()
except (EOFError, KeyboardInterrupt):
print("\n👋 終了します。")
break
if not user_input:
continue
if user_input == "/quit":
print("👋 終了します。")
break
elif user_input == "/list":
self.list_documents()
elif user_input == "/rebuild":
self._rebuild_index()
elif user_input == "/model":
print(f" 現在のモデル: {OLLAMA_MODEL}")
print(f" 変更するには chatbot.py の OLLAMA_MODEL を書き換えてください。")
elif user_input == "/add":
title = input(" タイトル: ").strip()
print(" 本文 (空行で確定):")
lines = []
while True:
line = input()
if line == "":
break
lines.append(line)
body = "\n".join(lines)
if title and body:
self.add_document(title, body)
else:
print(" ⚠️ タイトルと本文を入力してください。")
else:
print("Bot > ", end="", flush=True)
reply = self.answer(user_input)
print(reply)
print()
# ── エントリポイント ──────────────────────────────────────────────────────
if __name__ == "__main__":
bot = RAGChatbot()
bot.run()
seed_docs.py
#!/usr/bin/env python3
"""
サンプルドキュメントを一括登録するスクリプト
初回セットアップ時に実行してください。
"""
import json
from pathlib import Path
DOCS_PATH = "docs.json"
SAMPLE_DOCS = [
{
"title": "TurboVecとは",
"body": (
"TurboVecはRust製のベクトルインデックスライブラリです。"
"Pythonバインディングもあり、pip install turbovecでインストールできます。"
"Google ResearchのTurboQuantアルゴリズムを実装しています。"
"ICLR 2026で採択された研究論文がベースです。"
),
},
{
"title": "圧縮効率",
"body": (
"TurboVecは1000万件のベクトルを31GBから4GBに圧縮します。"
"2bitモードで最大16倍の圧縮率を達成します。"
"float32をそのまま使うと31GBになりますが、TurboVecなら4GBに収まり"
"RAGシステムをローカルで動かせます。"
),
},
{
"title": "FAISSとの比較",
"body": (
"TurboVecはARMアーキテクチャでFAISSよりも12〜20%高速です。"
"x86でもFAISS以上の速度を出します。"
"FAISS IndexPQFastScanを上回るベンチマーク結果が報告されています。"
"NEON(ARM)とAVX-512BW(x86)のSIMDカーネルで最適化されています。"
),
},
{
"title": "学習不要の仕組み",
"body": (
"TurboVecはコードブックの事前学習が不要です。"
"従来のPQはデータからコードブックを学習しますが、"
"TurboQuantは高次元ベクトルの数学的性質を利用するため学習フェーズがありません。"
"ベクトルを追加するだけでインデックスが完成します。"
),
},
{
"title": "Pythonでの使い方",
"body": (
"from turbovec import TurboQuantIndex でインポートします。"
"index = TurboQuantIndex(384, 4) で384次元4bitインデックスを作成します。"
"index.add(vectors)でベクトルを追加し、"
"index.search(query, k=10)で近傍検索ができます。"
"本チャットボットではall-MiniLM-L6-v2(384次元)を使用しています。"
),
},
{
"title": "ローカルRAGへの活用",
"body": (
"TurboVecを使えばAPIコールなしの完全ローカルRAGパイプラインを構築できます。"
"データが外部に出ないため、製造業や医療など機密性の高い分野で特に有用です。"
"OllamaやGemmaと組み合わせてプライベートAIシステムを構築できます。"
),
},
{
"title": "Raspberry Pi での注意点",
"body": (
"Raspberry Pi 4以降を推奨します(RAM 4GB以上)。"
"Rustツールチェーンをインストール後、pip install turbovecを実行します。"
"埋め込みモデルはall-MiniLM-L6-v2(384次元)が軽量でラズパイ向きです。"
"初回の埋め込み生成はCPUのみで数分かかる場合があります。"
),
},
]
def main():
path = Path(DOCS_PATH)
if path.exists():
existing = json.loads(path.read_text(encoding="utf-8"))
print(f"既存ドキュメント {len(existing)} 件を検出しました。")
ans = input("上書きしますか? (y/N): ").strip().lower()
if ans != "y":
print("キャンセルしました。")
return
docs = [{"id": i, **d} for i, d in enumerate(SAMPLE_DOCS)]
path.write_text(json.dumps(docs, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"✓ {len(docs)} 件のサンプルドキュメントを {DOCS_PATH} に保存しました。")
print("次に以下を実行してください:")
print(" python chatbot.py")
if __name__ == "__main__":
main()
実行手順
1. サンプルドキュメントのインデックス化
初回は必ず先に seed_docs.py を実行して docs.json と index.tv を生成してください。これを省略するとチャットボットがドキュメントを参照できません。
python seed_docs.py
✓ 7 件のサンプルドキュメントを docs.json に保存しました。
次に以下を実行してください:
python chatbot.py
2. チャットボットの起動
python chatbot.py
今回は、Macで実行しているので、gemma3:4bを使っています。
📦 TurboVec RAG Chatbot (Ollama Edition) を起動中...
[GIN] 2026/06/08 - 07:55:50 | 200 | 488.125µs | 127.0.0.1 | GET "/api/tags"
✓ Ollama モデル確認: gemma3:4b
🔤 埋め込みモデルをロード中: all-MiniLM-L6-v2
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
Loading weights: 100%|█████████████████████████████████████████████████████████████████████████████| 103/103 [00:00<00:00, 5755.19it/s]
✓ 完了
🔨 インデックスを新規構築します...
エンベディング生成中 (7 件)...
Batches: 100%|███████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 2.86it/s]
→ 7 件をインデックス化しました (4bit, 次元=384)
→ インデックスを保存しました: index.tv
==========================================================
🤖 TurboVec RAG Chatbot — Ollama Edition
📡 使用モデル: gemma3:4b
==========================================================
コマンド一覧:
/add — ドキュメントを追加
/list — ドキュメント一覧を表示
/rebuild — インデックスを再構築
/model — 現在のモデルを確認
/quit — 終了
==========================================================
あなた >
あなた > TurboVecの圧縮率はどれくらいですか?
Bot >
🔍 参照ドキュメント: 「TurboVecとは」, 「圧縮効率」, 「FAISSとの比較」
TurboVecは、2bitモードで最大16倍の圧縮率を達成します。
モデルの切り替え方
方法1: chatbot.py を1行書き換え(シンプル)
OLLAMA_MODEL = "gemma3:1b" # 4b → 1b に変更
方法2: 環境変数で切り替え
まず chatbot.py の該当行を以下に変更します:
import os
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "gemma3:4b")
あとは起動時に指定するだけです:
python chatbot.py
OLLAMA_MODEL=gemma3:1b python chatbot.py
ドキュメントの追加
/add コマンドで実行中にドキュメントを追加できます。追加と同時にインデックスが再構築されます。
あなた > /add
タイトル: 製造ライン異常検知
本文 (空行で確定):
工場の製造ラインでは OPC UA でセンサーデータを収集し、
TurboVec でベクトル化して異常パターンを近傍検索します。
✓ ドキュメント #7 '製造ライン異常検知' を追加しました。インデックスを再構築します...
エンベディング生成中 (8 件)...
Batches: 100%|███████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 1.44it/s]
→ 8 件をインデックス化しました (4bit, 次元=384)
→ インデックスを保存しました: index.tv
あなた > 製造ラインでは、何を使ってセンサーデータを収集しますか?
Bot >
🔍 参照ドキュメント: 「製造ライン異常検知」, 「ローカルRAGへの活用」, 「学習不要の仕組み」
OPC UA でセンサーデータを収集します。
Claude API版との比較
「完全無料にこだわらない」「回答品質を優先したい」場合は、Ollama部分をClaude Haiku APIに差し替えることもできます。
| 項目 | Ollama版(本記事) | Claude Haiku API版 |
|---|---|---|
| コスト | 完全無料 | 有料 |
| インターネット | 不要 | 必要 |
| データの外部送信 | なし | あり |
| 回答品質 | △〜○ | ◎ |
| 応答速度(CPU) | △ | ◎ |
| 閉じたネットワーク | 対応 | 非対応 |
製造現場や医療など機密性が求められる環境では Ollama版が特に有効です。
トラブルシューティング
Ollamaに接続できない
ollama serve
python chatbot.py
turbovecのビルドが失敗する
rustup update stable
pip install --force-reinstall turbovec
Raspberry Piでメモリが足りない
sudo dphys-swapfile swapoff
sudo sed -i 's/CONF_SWAPSIZE=.*/CONF_SWAPSIZE=2048/' /etc/dphys-swapfile
sudo dphys-swapfile setup && sudo dphys-swapfile swapon
回答が遅い
-
gemma3:4b→gemma3:1bに変更 -
MAX_TOKENS = 256に下げる -
BIT_WIDTH = 2でインデックス検索を高速化
まとめ
TurboVec × Ollama の組み合わせで、以下を実現できました:
- ✅ 完全無料(APIキー不要)
- ✅ 完全ローカル(データが外に出ない)
- ✅ Raspberry Pi 4で動作(ARM NEON最適化の恩恵)
- ✅ モデル切り替えが1行(1b / 4b を用途に応じて選択)
- ✅ コードブック学習不要(ドキュメント追加だけでインデックスが育つ)
製造DXの現場でオンプレRAGを構築する際の参考になれば幸いです。
使用したソースコード群
参考リンク