「ローカルLLMでRAGを動かしたいが、Ollama で動かした Gemma 4 と、別途用意したベクトルデータベースをどう繋ぐか分からない」——この悩みは、オープンソースLLMを本番環境に持ち込む際に必ずぶつかる壁です。
ここではGemma 4 と ChromaDB または pgvector を組み合わせた RAG システムの実装を、アーキテクチャ設計から本番最適化まで段階的に解説します。Apache 2.0 ライセンスのコンポーネントで固めているため、商用サービスへの組み込みに制約がありません。
RAG と Gemma 4 の組み合わせが強い理由
従来の RAG 実装では「コンテキスト窓が短い→チャンクを細かく切る→文脈が失われる」というトレードオフが常につきまとっていた。Gemma 4 の 31B Dense と 26B MoE は 256K トークンのコンテキスト窓を持ち、E2B/E4B でも 128K あります。これは、RAG の設計思想そのものを変え得るスペックです。
具体的には、検索でヒットしたチャンクを複数まとめて1回のプロンプトに含める「Long-Context RAG」が現実的な選択肢になります。100件のチャンク(各1,000トークン)を一度に渡しても、256K 窓には余裕があります。モデルが文書間の関係を直接参照しながら回答を生成できるため、チャンクの境界で文脈が切れる問題が大幅に軽減されます。
もう一つの強みが Apache 2.0 ライセンスです。Gemma 3 では商用利用に制約のあるカスタムライセンスが使われていたが、Gemma 4 は完全な Apache 2.0 に移行しました。これは SaaS への組み込み、OEM 提供、派生モデルの商業販売がすべて自由になることを意味します。
システムアーキテクチャの全体設計
本番 RAG システムの構成要素は次の5層からなります。
┌─────────────────────────────────────────┐
│ 1. ドキュメント処理層 │
│ PDF/HTML/Markdown → テキスト抽出 │
│ → チャンキング → 埋め込み生成 │
├─────────────────────────────────────────┤
│ 2. ベクトルストア層 │
│ ChromaDB(開発・中規模) │
│ pgvector(PostgreSQL統合・大規模) │
├─────────────────────────────────────────┤
│ 3. 検索層 │
│ セマンティック検索 + キーワード検索 │
│ ハイブリッドランキング(RRF) │
├─────────────────────────────────────────┤
│ 4. 生成層 │
│ Gemma 4(Ollama / Gemini API) │
│ Long-Context or Chunked RAG │
├─────────────────────────────────────────┤
│ 5. キャッシュ・最適化層 │
│ Redis(クエリキャッシュ) │
│ バッチ埋め込み生成 │
└─────────────────────────────────────────┘
Step 1: 埋め込みモデルの選択とセットアップ
Gemma 4 自体は汎用生成モデルであり、直接埋め込みベクトルを出力する設計ではありません。RAG では専用の埋め込みモデルと組み合わせて使う。
Gemma 4 との相性が良い埋め込みモデルの選択肢:
# オプション1: multilingual-e5-large(多言語対応・ローカル実行可能)
from sentence_transformers import SentenceTransformer
embedding_model = SentenceTransformer("intfloat/multilingual-e5-large")
# 1024次元、50以上の言語に対応、Apache 2.0
# オプション2: text-embedding-3-small(Google Gemini API)
import google.generativeai as genai
genai.configure(api_key="YOUR_GEMINI_API_KEY")
# Google の埋め込みAPIを使う場合(Gemma 4 との同一エコシステム)
# オプション3: nomic-embed-text(Ollama経由でローカル実行)
import requests
def embed_with_ollama(text: str) -> list:
resp = requests.post(
"http://localhost:11434/api/embeddings",
json={"model": "nomic-embed-text", "prompt": text}
)
return resp.json()["embedding"]プロダクションでは multilingual-e5-large と Ollama の nomic-embed-text を比較検証することを勧める。前者は品質が高く、後者はローカル実行で外部依存がありません。
Step 2: ChromaDB との統合
ChromaDB は開発環境や中規模用途(数百万件程度のドキュメント)に最適なベクトルストアです。Python ネイティブな API と永続化機能を持ちます。
import chromadb
from sentence_transformers import SentenceTransformer
from chromadb.utils import embedding_functions
import uuid
# ChromaDB セットアップ
client = chromadb.PersistentClient(path="./chroma_db")
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="intfloat/multilingual-e5-large"
)
collection = client.get_or_create_collection(
name="documents",
embedding_function=embedding_fn,
metadata={"hnsw:space": "cosine"}
)
def add_documents(texts: list, metadatas: list = None):
ids = [str(uuid.uuid4()) for _ in texts]
collection.add(
documents=texts,
metadatas=metadatas or [{}] * len(texts),
ids=ids
)
return ids
def search_documents(query: str, n_results: int = 10, filter_meta: dict = None):
results = collection.query(
query_texts=[query],
n_results=n_results,
where=filter_meta
)
return [
{"text": doc, "metadata": meta, "distance": dist}
for doc, meta, dist in zip(
results["documents"][0],
results["metadatas"][0],
results["distances"][0]
)
]
# 使用例
add_documents(
texts=["Gemma 4は2026年4月にGoogleが発表したオープンソースLLMです。"],
metadatas=[{"source": "gemma4_overview.pdf", "page": 1}]
)
results = search_documents("Gemma 4の発表時期は?")Step 3: pgvector との統合(大規模・PostgreSQL統合)
既存の PostgreSQL インフラに AI 機能を追加したい場合は pgvector が最適です。既存の RDB との JOIN も可能なため、ユーザーデータと知識ベースを組み合わせた高度なクエリが書ける。
import psycopg2
import numpy as np
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("intfloat/multilingual-e5-large")
class PgVectorRAG:
def __init__(self, conn_string: str):
self.conn = psycopg2.connect(conn_string)
self._setup_schema()
def _setup_schema(self):
with self.conn.cursor() as cur:
cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
cur.execute("""
CREATE TABLE IF NOT EXISTS documents (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding vector(1024),
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
""")
# IVFFlat インデックス(大規模データ向け)
cur.execute("""
CREATE INDEX IF NOT EXISTS documents_embedding_idx
ON documents USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
""")
self.conn.commit()
def add_document(self, content: str, metadata: dict = None):
embedding = model.encode(content).tolist()
with self.conn.cursor() as cur:
cur.execute(
"INSERT INTO documents (content, embedding, metadata) VALUES (%s, %s, %s)",
(content, embedding, psycopg2.extras.Json(metadata or {}))
)
self.conn.commit()
def search(self, query: str, k: int = 10, min_similarity: float = 0.7) -> list:
query_embedding = model.encode(query).tolist()
with self.conn.cursor() as cur:
cur.execute("""
SELECT content, metadata,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
WHERE 1 - (embedding <=> %s::vector) > %s
ORDER BY embedding <=> %s::vector
LIMIT %s;
""", (query_embedding, query_embedding, min_similarity, query_embedding, k))
return [
{"content": row[0], "metadata": row[1], "similarity": float(row[2])}
for row in cur.fetchall()
]
# 使用例
rag = PgVectorRAG("postgresql://user:password@localhost:5432/ragdb")
rag.add_document("Gemma 4のMoEモデルは推論時に3.8Bパラメータがアクティブになる。")
results = rag.search("Gemma 4 MoEのアクティブパラメータ数")Step 4: Gemma 4 と組み合わせた生成
検索結果を Gemma 4 に渡して回答を生成します。256K コンテキストを活かした Long-Context RAG では、複数の検索結果を1回のプロンプトにまとめて送れます。
import requests
def generate_answer_gemma4(
query: str,
context_chunks: list,
model: str = "gemma4:27b",
use_long_context: bool = True
) -> str:
if use_long_context:
# Long-Context RAG: チャンクをすべて1プロンプトに
context_text = "
---
".join([
f"[出典: {c.get("metadata", {}).get("source", "不明")}]
{c["text"]}"
for c in context_chunks
])
prompt = f"""以下のドキュメント群を参照して質問に答えてください。
# 参照ドキュメント
{context_text}
# 質問
{query}
# 回答(参照した出典を明示してください)"""
else:
# 通常RAG: 上位チャンクのみ
top_context = "
".join([c["text"] for c in context_chunks[:3]])
prompt = f"コンテキスト:
{top_context}
質問: {query}
回答:"
response = requests.post(
"http://localhost:11434/api/generate",
json={
"model": model,
"prompt": prompt,
"stream": False,
"options": {
"temperature": 0.1, # RAGでは低温度で事実に基づく回答を促す
"top_p": 0.9
}
}
)
return response.json()["response"]
def rag_pipeline(query: str, vector_store, k: int = 20) -> str:
# 検索
results = vector_store.search(query, k=k)
# 生成
answer = generate_answer_gemma4(query, results)
return answerStep 5: ハイブリッド検索とクエリ拡張
セマンティック検索だけでは漏れが生じます。キーワード検索と組み合わせた「ハイブリッド検索」と、クエリを言い換えて検索範囲を広げる「クエリ拡張」で精度を上げる。
from rank_bm25 import BM25Okapi
import re
class HybridSearchRAG:
def __init__(self, vector_rag, documents: list, metadatas: list):
self.vector_rag = vector_rag
self.documents = documents
# BM25 インデックス構築
tokenized = [re.findall(r"\w+", doc.lower()) for doc in documents]
self.bm25 = BM25Okapi(tokenized)
self.metadatas = metadatas
def search(self, query: str, k: int = 10, alpha: float = 0.5) -> list:
# セマンティック検索結果
semantic_results = {
r["text"]: r["similarity"]
for r in self.vector_rag.search(query, k=k*2)
}
# BM25 検索結果
query_tokens = re.findall(r"\w+", query.lower())
bm25_scores = self.bm25.get_scores(query_tokens)
max_bm25 = max(bm25_scores) if max(bm25_scores) > 0 else 1.0
bm25_results = {
self.documents[i]: bm25_scores[i] / max_bm25
for i in bm25_scores.argsort()[-k*2:][::-1]
}
# Reciprocal Rank Fusion (RRF) でスコアを統合
all_docs = set(semantic_results.keys()) | set(bm25_results.keys())
fused_scores = {
doc: alpha * semantic_results.get(doc, 0) + (1 - alpha) * bm25_results.get(doc, 0)
for doc in all_docs
}
top_k = sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)[:k]
return [{"text": doc, "score": score} for doc, score in top_k]プロダクション最適化:バッチ埋め込みとキャッシュ
大量のドキュメントを処理する場合、埋め込み生成をバッチ化することで処理時間が大幅に短縮されます。
import redis
import hashlib
import json
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("intfloat/multilingual-e5-large")
redis_client = redis.Redis(host="localhost", port=6379, db=0)
def get_embeddings_cached(texts: list, cache_ttl: int = 86400) -> list:
embeddings = []
texts_to_encode = []
cache_keys = []
# キャッシュ確認
for text in texts:
key = f"emb:{hashlib.md5(text.encode()).hexdigest()}"
cached = redis_client.get(key)
if cached:
embeddings.append(json.loads(cached))
texts_to_encode.append(None)
else:
embeddings.append(None)
texts_to_encode.append(text)
cache_keys.append(key)
# キャッシュミスをバッチエンコード
uncached_texts = [t for t in texts_to_encode if t is not None]
if uncached_texts:
# batch_size=64でGPUメモリを効率利用
new_embeddings = model.encode(uncached_texts, batch_size=64).tolist()
idx = 0
for i, text in enumerate(texts_to_encode):
if text is not None:
embeddings[i] = new_embeddings[idx]
redis_client.setex(cache_keys[i], cache_ttl, json.dumps(new_embeddings[idx]))
idx += 1
return embeddingsどのユースケースにこの構成が最適か
Gemma 4 + ChromaDB/pgvector の RAG 構成が最も効果を発揮するのは次のような場面です。
社内ドキュメント検索システム——外部 API に送れない機密文書(法的書類、財務データ、医療記録)を社内サーバーで完全オフライン処理できます。Apache 2.0 ライセンスなので商用利用に追加費用がかからありません。
多言語ナレッジベース——Gemma 4 の140言語サポートと多言語埋め込みモデルを組み合わせることで、日英混在の技術ドキュメントや多言語カスタマーサポートに対応できます。
コードベース検索——256K コンテキストを活用して、大規模なコードベース全体を一度に参照しながら実装方針を問い合わせられます。チャンキングが不要なため、関数をまたいだ依存関係も正しく理解されます。
まず ollama pull gemma4:27b でモデルを取得し、ChromaDB のローカル環境で検証してから pgvector 移行を検討する順序が現実的です。