OllamaにRedisセマンティックキャッシュを導入する方法|同一質問の重複LLM呼び出しを削減してチームサーバーのレスポンスを高速化する手順

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)ローカルLLM > OllamaにRedisセマンティックキャッシュを導入する方法|同一質問の重複LLM呼び出しを削減してチームサーバーのレスポンスを高速化する手順
「チームで同じ質問を何度もOllamaに投げて、GPUが無駄に占有されている」
「複数人でローカルLLMを共有すると、繰り返し質問でレスポンスが遅くなる」
そんな悩みを抱えるインフラエンジニアやシステム管理者は多いはずです。この記事では、OllamaチームサーバーにRedisセマンティックキャッシュを組み込み、意味的に類似した質問に対してLLM推論を再実行せずキャッシュ応答を返す仕組みを構築します。RedisStackのインストールから、Pythonによるキャッシュ層の実装、ヒット率測定まで、実務で即使える手順を丸ごと解説します。

この記事のポイント

・RedisStackのHNSWベクターインデックスと nomic-embed-text でセマンティックキャッシュを実現できる
・類似度閾値0.92(コサイン距離0.08)を起点に調整すれば、意味が同じ質問を自動でキャッシュヒットに振り分けられる
・Pythonの redis-py 17行のクラスで既存Ollamaコードへの差し込みが可能
・TTL・DIM不一致・メモリ上限が本番でよく引っかかるポイントで、それぞれ具体的な対処がある


OllamaにRedisセマンティックキャッシュを導入する方法|同一質問の重複LLM呼び出しを削減してチームサーバーのレスポンスを高速化する手順

「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

セマンティックキャッシュがOllamaチームサーバーで解決する問題

チームでOllamaを共有運用するようになると、ある時点で「同じような質問が繰り返し飛んでくる」という現象が起きます。
たとえば「Linuxでディスク容量を確認するコマンドは?」と「ディスク使用量を調べるLinuxコマンドを教えて」は、文字列としては別ですが意味はほぼ同一です。
通常のテキストキャッシュ(Redisのキー=質問文字列)ではこれを同一視できないため、GPU推論が2回走ります。 セマンティックキャッシュは、質問をベクトルに変換して「意味の近さ」で判定します。コサイン類似度が閾値を超えれば、過去の応答をそのまま返すのでGPUは使いません。
チーム規模が5名以上になると、同カテゴリの質問が日に何十回も来るケースがあります。現場で聞いた話では、セマンティックキャッシュ導入後にOllamaのGPU使用率が30~40%台から20%台に落ちたという事例もあります。
GPU1枚しかない環境では、推論待ちキューが詰まらなくなるだけで体感速度が大幅に改善します。 なお、Ollamaサーバーの初期構築については Ubuntu ServerでローカルLLMを構築する方法 を先に参照してください。

全体構成:RedisStack + nomic-embed-text + Ollamaの連携アーキテクチャ

本記事で構築するシステムは3層構造です。 1. **クライアント** がPythonキャッシュ層にリクエストを送る 2. **Pythonキャッシュ層** が質問テキストをOllamaの nomic-embed-text でベクトル化し、RedisStackのHNSWインデックスを検索する 3. キャッシュヒットなら即応答、ミスなら **OllamaのLLM** (例: llama3.3:70b-instruct-q4_0)を呼び出してその結果をキャッシュに書き込む RedisStackを選ぶ理由は、標準Redisに追加せず1パッケージでベクターサーチ(RediSearch)とJSON機能が使えるためです。
SCANでキーを総当たりするナイーブな実装と異なり、HNSWインデックスはキャッシュエントリが数千件に増えても検索時間はほぼ一定です。
埋め込みモデルは nomic-embed-text(768次元)を使います。軽量でOllama上で動き、英語・日本語どちらの質問にも対応できます。

RedisStackをUbuntu Serverにインストールする手順

1. リポジトリの登録

# Redisの公式GPGキーを取得してリポジトリを追加する curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor \ -o /usr/share/keyrings/redis-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] \ https://packages.redis.io/deb $(lsb_release -cs) main" \ | sudo tee /etc/apt/sources.list.d/redis.list sudo apt update

Ubuntu 22.04(Jammy)と24.04(Noble)の両方でリポジトリが提供されています。lsb_release -cs でディストリビューション名が自動判定されます。

2. RedisStackのインストールと起動確認

sudo apt install -y redis-stack-server sudo systemctl enable redis-stack-server sudo systemctl start redis-stack-server # 起動確認 redis-cli ping # 期待する出力: PONG # RediSearchモジュールがロードされているか確認 redis-cli MODULE LIST # redis-search が表示されればOK

標準の redis-server がすでに起動している場合は、ポート競合を避けるために停止してから起動します。

sudo systemctl stop redis-server sudo systemctl disable redis-server sudo systemctl start redis-stack-server

3. ベクターインデックスの事前作成

Pythonコードから動的に作ることもできますが、本番では事前に作成しておくほうが安定します。

# ベクターインデックスをredis-cliから直接作成する例 redis-cli FT.CREATE llm-cache ON HASH PREFIX 1 "cache:" \ SCHEMA \ prompt TEXT \ response TEXT \ embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 768 DISTANCE_METRIC COSINE # 作成確認 redis-cli FT.INFO llm-cache | grep num_docs

DIM 768nomic-embed-text の出力次元数です。別の埋め込みモデルに変える場合はここを合わせて変更します。mxbai-embed-large なら1024になります。

Pythonでセマンティックキャッシュ層を実装する手順

1. 依存パッケージのインストール

pip install redis numpy requests

2. キャッシュクラスの実装

以下のコードを /opt/ollama-cache/semantic_cache.py として保存します。

import json, struct, hashlib import numpy as np import requests from redis import Redis from redis.commands.search.query import Query OLLAMA_URL = "http://localhost:11434" EMBED_MODEL = "nomic-embed-text" LLM_MODEL = "llama3.3:70b-instruct-q4_0" CACHE_TTL = 3600 # キャッシュの有効期限(秒) DISTANCE_THR = 0.08 # コサイン距離の閾値(1-0.92=0.08 → 92%以上の類似度) INDEX_NAME = "llm-cache" class SemanticCache: def __init__(self): self.r = Redis(host="localhost", port=6379) def _embed(self, text: str) -> list: res = requests.post(f"{OLLAMA_URL}/api/embeddings", json={"model": EMBED_MODEL, "prompt": text}) return res.json()["embedding"] def _vec_bytes(self, vec: list) -> bytes: return np.array(vec, dtype=np.float32).tobytes() def _search(self, query_vec: list) -> str | None: q = (Query("*=>[KNN 1 @embedding $vec AS dist]") .return_fields("response", "dist") .sort_by("dist") .dialect(2)) results = self.r.ft(INDEX_NAME).search( q, query_params={"vec": self._vec_bytes(query_vec)}) if results.docs and float(results.docs[0].dist) < DISTANCE_THR: return results.docs[0].response return None def ask(self, prompt: str) -> tuple[str, bool]: vec = self._embed(prompt) cached = self._search(vec) if cached: return cached, True # (応答テキスト, キャッシュヒット) res = requests.post(f"{OLLAMA_URL}/api/generate", json={"model": LLM_MODEL, "prompt": prompt, "stream": False}) response = res.json()["response"] key = f"cache:{hashlib.sha256(prompt.encode()).hexdigest()[:16]}" self.r.hset(key, mapping={ "prompt": prompt, "response": response, "embedding": self._vec_bytes(vec), }) self.r.expire(key, CACHE_TTL) return response, False if __name__ == "__main__": cache = SemanticCache() test_prompts = [ "Linuxでディスク容量を確認するコマンドは?", "ディスク使用量を調べるLinuxコマンドを教えて", # セマンティックに近い "Linuxのメモリ使用量を確認する方法は?", ] for p in test_prompts: ans, hit = cache.ask(p) print(f"[{'HIT ' if hit else 'MISS'}] {p[:30]}... => {ans[:60]}")

ask()(応答テキスト, キャッシュヒット) のタプルを返します。既存コードへの差し込みは client.ask(prompt) の1行で済みます。
Ollamaのモデル指定や閾値は定数として上部に集めているため、設定変更が容易です。

3. 動作確認

# 動作確認スクリプトを実行する cd /opt/ollama-cache python3 semantic_cache.py # 期待する出力例: # [MISS] Linuxでディスク容量を確認する... => dfコマンドを使います。... # [HIT ] ディスク使用量を調べるLinux... => dfコマンドを使います。... # [MISS] Linuxのメモリ使用量を確認する... => freeコマンドまたはtopコマン...

1回目はMISSでOllama呼び出しが発生し、2問目は意味が類似しているのでHITになります。3問目はメモリに関する別の質問なのでMISSになります。

キャッシュのヒット率とパフォーマンスを測定する方法

1. Redisコマンドでキャッシュ状態を確認する

# キャッシュエントリ数を確認する redis-cli FT.INFO llm-cache | grep num_docs # 例: num_docs 47 → 47件がインデックス済み # Redisのキャッシュヒット・ミス統計 redis-cli INFO stats | grep -E "keyspace_hits|keyspace_misses" # keyspace_hits:1284 # keyspace_misses:312

keyspace_hitskeyspace_misses の比率がキャッシュ全体のヒット率です。ただしセマンティックキャッシュのHKV読み取りも計上されるため、実際のLLM呼び出し削減率はPythonコードの戻り値 hit フラグをアプリ側でカウントするほうが正確です。

2. 応答時間を比較するシェルスクリプト

#!/bin/bash # キャッシュあり・なしの応答時間を比較する PROMPT="Linuxのcronジョブ設定方法を教えてください" echo "--- キャッシュなし(直接Ollama呼び出し)---" time curl -s -X POST http://localhost:11434/api/generate \ -d "{\"model\":\"llama3.3:70b-instruct-q4_0\",\"prompt\":\"${PROMPT}\",\"stream\":false}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['response'][:80])" echo "" echo "--- キャッシュあり(2回目・HIT想定)---" time python3 -c " from semantic_cache import SemanticCache ans, hit = SemanticCache().ask('${PROMPT}') print(f'HIT={hit} {ans[:80]}') "

70Bモデルで直接呼び出すと20~60秒かかる応答が、キャッシュHIT時には1秒未満で返ります。埋め込み取得(nomic-embed-text)とRedis検索で0.3~0.8秒程度かかるのが内訳です。

本番運用で直面しやすいトラブルと対処法

**類似度閾値が合わない**
デフォルトの DISTANCE_THR = 0.08(コサイン類似度92%相当)は英語質問中心の環境向けです。日本語の質問は表現ゆらぎが大きいため、0.12~0.15(88~85%)に緩めると自然な言い換えもヒットしやすくなります。逆に専門的なコマンド名が変わっただけで別の質問なのにヒットする場合は0.05以下に絞ります。
運用開始後1週間は誤ヒット・未ヒットをログに記録して閾値を調整するのが現実的です。 **インデックスのDIM不一致エラー**
redis-py から FT.CREATE をコード内で動的に実行すると、すでに別次元で作成済みのインデックスとバッティングして Index already exists エラーが出る場合があります。インデックスは起動時に1回だけ作成するか、FT.INFO でDIMを確認してから起動するスクリプトを用意します。

# 既存インデックスのDIM確認 redis-cli FT.INFO llm-cache | grep -A1 "dim" # DIM が 768 であることを確認する

**Redisのメモリ上限とTTL管理**
1エントリのembeddingは768次元 × 4バイト = 約3KB、promptとresponseを含めると1エントリ10~30KBになります。キャッシュが10,000件溜まると200MB~300MBを占有します。
/etc/redis-stack.confmaxmemory 1gbmaxmemory-policy allkeys-lru を設定し、古いエントリが自動削除されるようにします。加えてTTLを適切に設定することで、内容が変わりやすい質問(最新ニュースを聞くなど)の陳腐化を防げます。 Ollamaのモデル選定で迷っている場合は ローカルLLMのモデルを比較する方法 も参照してください。また、社内へのOllama導入検討段階にある場合は 社内でChatGPTが使えないときの代替手段 で導入判断の整理ができます。

まとめ

項目設定・コマンド補足
インストール sudo apt install -y redis-stack-server RediSearchが同梱される
インデックス作成 redis-cli FT.CREATE llm-cache ON HASH ... DIM 768 nomic-embed-text用
キャッシュ検索 FT.SEARCH llm-cache "*=>[KNN 1 @embedding $vec AS dist]" HNSWでO(log n)検索
類似度閾値 DISTANCE_THR = 0.08(92%類似) 用途に合わせて0.05~0.15で調整
TTL設定 self.r.expire(key, 3600) 1時間。質問内容次第で変更
メモリ上限 maxmemory 1gb + allkeys-lru redis-stack.confに記述
状態確認 redis-cli FT.INFO llm-cache num_docsでエントリ数確認
チームでOllamaを共有する規模が大きくなるほど、セマンティックキャッシュの効果は増します。閾値チューニングを1週間かけてデータドリブンで行うのが、現場での安定稼働への近道です。

ローカルLLMのキャッシュ・チューニングを2日間のハンズオンで体験する

セマンティックキャッシュやRAGパイプラインの構築を、実機GPU環境で手を動かしながら習得したい方向けに、「ローカルAIマスターセミナー」を開催しています。
少人数(最大8名)ZOOMハンズオン形式で実施しています。

>> ローカルAIマスターセミナーの詳細を確認する
ローカルLLMの構築・運用に関する関連記事もあわせて参考にしてください。

Ubuntu ServerでローカルLLMを構築する方法|Ollamaで機密データを外に出さず業務AIを動かす完全ガイド
社内でChatGPTが使えないときの代替手段|機密データを守るローカルLLMという選択肢
ローカルLLMのモデルを比較する方法|Llama3.3・Mistral・Gemma・Phi-4をUbuntuで使い分けるポイント

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。