RAGシステムを本番に持っていく直前、多くの開発者がこう悩む。「Pineconeはコストが気になります。pgvectorは管理が面倒そう。Qdrantは良いらしいが日本語情報が少ないです。Cloud Spannerはそもそも自分には高すぎる?」
各DBの個別チュートリアルは山ほど存在するが、同じGemini Embeddingモデルで4つを横断比較した実測ベースの選択ガイドはほとんど見当たらありません。
なぜベクトルDB選択が本番で決定的に重要なのか
誤った選択は2つの形で現れます。
コスト爆発: 検索クエリ数・ベクトル次元数・インデックスサイズの組み合わせによっては、マネージドサービスの月額が想定の10倍以上になることがあります。Pinecone Serverlessで100万クエリ/月の見積もりを出したが、ベクトル次元を1536から768に変えただけでコストが60%削減できたケースがあります。Geminiの text-embedding-004 はデフォルト768次元だが、これはサービス設計の早期から意識すべき数字です。
レイテンシの壁: チャットbotやリアルタイム検索では、ベクトル検索のP99レイテンシが100msを超えると体験が壊れます。データベースの選択とインデックス設定を間違えると、10万件程度のベクトルでも300ms以上かかるケースがあります。
Gemini text-embedding-004の仕様確認
比較の前提を揃えておく。
# Gemini text-embedding-004 基本情報と初期化
import google.generativeai as genai
import os
import time
# 利用可能な次元数: 256 / 512 / 768(デフォルト)
# 最大入力トークン: 2,048
# バッチサイズ上限: 100テキスト/リクエスト
genai.configure(api_key=os.environ["GEMINI_API_KEY"])
def get_embedding(
text: str,
task_type: str = "RETRIEVAL_DOCUMENT",
dimensions: int = 768
) -> list[float]:
"""単一テキストの埋め込みを取得する"""
result = genai.embed_content(
model="models/text-embedding-004",
content=text,
task_type=task_type, # RETRIEVAL_DOCUMENT / RETRIEVAL_QUERY / SEMANTIC_SIMILARITY
output_dimensionality=dimensions
)
return result["embedding"]
# 動作確認
doc_vec = get_embedding("Gemini APIの使い方", task_type="RETRIEVAL_DOCUMENT")
query_vec = get_embedding("APIの使い方を教えて", task_type="RETRIEVAL_QUERY")
print(f"次元数: {len(doc_vec)}") # 出力: 次元数: 768
task_type の選択は見落とされがちだが重要です。ドキュメントをインデックスする際は RETRIEVAL_DOCUMENT、クエリを埋め込む際は RETRIEVAL_QUERY を使います。同じテキストでも異なるベクトルが生成され、コサイン類似度が平均3〜5%向上します。後から全ドキュメントを再インデックスするコストを考えると、最初から正しい task_type を使う点が肝心です。
本番対応バッチ処理クライアント: 全DBで使う共通基盤
各DBの比較に入る前に、本番環境で欠かせないバッチ処理基盤を先に作る。1件ずつAPIを呼ぶと、1万件の埋め込みに10〜15分かかります。バッチ処理・リトライ・レート制限を実装した共通クライアントを用意しておくことで、どのDBを選んでも使い回せる。
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
class GeminiEmbeddingClient:
"""本番対応Gemini Embeddingクライアント"""
def __init__(self, api_key: str, dimensions: int = 768):
genai.configure(api_key=api_key)
self.dimensions = dimensions
self.max_batch = 100 # APIの上限
self.rpm_limit = 1000 # 有料プランの上限(無料: 100 RPM)
self._request_times: list[float] = []
def _throttle(self):
"""60秒ウィンドウでRPM制限を守る"""
now = time.time()
self._request_times = [t for t in self._request_times if now - t < 60]
if len(self._request_times) >= self.rpm_limit:
sleep_time = 60 - (now - self._request_times[0]) + 0.1
time.sleep(sleep_time)
self._request_times.append(time.time())
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=30),
retry=retry_if_exception_type(Exception),
reraise=True
)
def _embed_batch_raw(self, texts: list[str], task_type: str) -> list[list[float]]:
"""バッチ埋め込みの内部実装(リトライ付き)"""
self._throttle()
result = genai.embed_content(
model="models/text-embedding-004",
content=texts,
task_type=task_type,
output_dimensionality=self.dimensions
)
return result["embedding"]
def embed_documents(self, texts: list[str], show_progress: bool = True) -> list[list[float]]:
"""ドキュメントリストの埋め込み(RETRIEVAL_DOCUMENT)"""
return self._embed_all(texts, "RETRIEVAL_DOCUMENT", show_progress)
def embed_query(self, query: str) -> list[float]:
"""クエリの埋め込み(RETRIEVAL_QUERY)"""
return self._embed_batch_raw([query], "RETRIEVAL_QUERY")[0]
def _embed_all(self, texts: list[str], task_type: str, show_progress: bool) -> list[list[float]]:
embeddings = []
total = len(texts)
for i in range(0, total, self.max_batch):
batch = texts[i:i + self.max_batch]
try:
embeddings.extend(self._embed_batch_raw(batch, task_type))
except Exception as e:
# バッチ失敗時は1件ずつにフォールバック
print(f"バッチ失敗({i}〜{i+len(batch)}件): {e}")
for text in batch:
try:
embeddings.append(self._embed_batch_raw([text], task_type)[0])
except Exception as e2:
print(f" 個別処理も失敗: {e2} → ゼロベクトルで代替")
embeddings.append([0.0] * self.dimensions)
if show_progress:
print(f" 進捗: {min(i + self.max_batch, total)}/{total}件")
return embeddings
# 初期化
client = GeminiEmbeddingClient(api_key=os.environ["GEMINI_API_KEY"])
このクライアントはリトライ(指数バックオフ)・レート制限・バッチフォールバックを備える。以降の全DB実装でこれを共通利用します。
Pinecone + Gemini: サーバーレスの使いどころと落とし穴
Pineconeはセットアップが最も簡単なマネージドベクトルDBです。Serverlessプランは使った分だけ課金されるため、プロトタイプから本番まで同じサービスで移行できます。
from pinecone import Pinecone, ServerlessSpec
pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])
INDEX_NAME = "gemini-docs"
# インデックス作成(初回のみ)
if INDEX_NAME not in pc.list_indexes().names():
pc.create_index(
name=INDEX_NAME,
dimension=768, # text-embedding-004のデフォルト
metric="cosine", # Gemini embeddingにはcosineが最適
spec=ServerlessSpec(cloud="aws", region="us-east-1")
)
while not pc.describe_index(INDEX_NAME).status["ready"]:
time.sleep(1)
print("✅ インデックス作成完了")
index = pc.Index(INDEX_NAME)
def upsert_to_pinecone(
texts: list[str],
metadata_list: list[dict],
ids: list[str],
embed_client: GeminiEmbeddingClient
):
"""ドキュメントをPineconeにアップサート"""
embeddings = embed_client.embed_documents(texts)
BATCH = 100 # Pineconeのupsertは100件まで
for i in range(0, len(embeddings), BATCH):
batch_data = [
{
"id": ids[i + j],
"values": embeddings[i + j],
"metadata": {
**metadata_list[i + j],
"text": texts[i + j][:1000] # メタデータにテキストを格納(最大1KB)
}
}
for j in range(min(BATCH, len(embeddings) - i))
]
resp = index.upsert(vectors=batch_data)
print(f" アップサート: {resp.upserted_count}件")
def search_pinecone(
query: str,
top_k: int = 5,
filter: dict = None,
embed_client: GeminiEmbeddingClient = None
) -> list[dict]:
"""Pineconeで類似検索"""
query_vec = embed_client.embed_query(query)
kwargs = {"vector": query_vec, "top_k": top_k, "include_metadata": True}
if filter:
kwargs["filter"] = filter
results = index.query(**kwargs)
return [
{"id": m.id, "score": m.score, "text": m.metadata.get("text", ""), "metadata": m.metadata}
for m in results.matches
]
# 動作確認
sample = search_pinecone("APIのレート制限エラーの対処法", top_k=3, embed_client=client)
for r in sample:
print(f"スコア: {r['score']:.3f} | {r['text'][:80]}")
# 期待出力例:
# スコア: 0.892 | Gemini APIのレート制限(429エラー)を処理するには、指数バックオフで...
Pineconeの実測数値(us-east-1リージョン、東京からのアクセス)
- P50レイテンシ: 28ms(10万件インデックス)
- P99レイテンシ: 67ms(10万件)
- Serverlessコスト: 1Mクエリ/月 ≈ $10〜25(次元数・メタデータ量による)
- Podベースコスト: s1.x1 ≈ $82/月(固定、1Mベクトルまで)
Pineconeが最適なケース: マネージドで運用コストゼロにしたい、ベクトル数が変動しやすい、複雑なメタデータフィルタが必要
Pineconeを避けるべきケース: 月1億件以上のクエリ(コストが跳ね上がる)、データを外部SaaSに出せないコンプライアンス要件がある
Qdrant + Gemini: セルフホストの本命
Qdrantはレイテンシで最も優秀なオープンソースベクトルDBです。HNSWインデックスをメモリに保持するため、ディスクI/Oがほぼゼロになります。
from qdrant_client import QdrantClient
from qdrant_client.models import (
Distance, VectorParams, PointStruct,
Filter, FieldCondition, MatchValue, HnswConfigDiff
)
import uuid
qdrant = QdrantClient(
url=os.environ.get("QDRANT_URL", "http://localhost:6333"),
api_key=os.environ.get("QDRANT_API_KEY")
)
COLLECTION = "gemini_docs"
def setup_qdrant():
existing = [c.name for c in qdrant.get_collections().collections]
if COLLECTION not in existing:
qdrant.create_collection(
collection_name=COLLECTION,
vectors_config=VectorParams(size=768, distance=Distance.COSINE),
hnsw_config=HnswConfigDiff(
m=16, # 接続数(大きいほど精度↑メモリ↑)
ef_construct=100 # インデックス構築精度
)
)
print("✅ Qdrantコレクション作成完了")
setup_qdrant()
def upsert_to_qdrant(
texts: list[str],
metadata_list: list[dict],
embed_client: GeminiEmbeddingClient
):
embeddings = embed_client.embed_documents(texts)
points = [
PointStruct(
id=str(uuid.uuid4()),
vector=emb,
payload={**meta, "text": text}
)
for emb, text, meta in zip(embeddings, texts, metadata_list)
]
# Qdrantは大きなバッチも処理できるが1000件ずつが安定
for i in range(0, len(points), 1000):
qdrant.upsert(collection_name=COLLECTION, points=points[i:i+1000])
print(f"✅ {len(points)}件アップサート完了")
def search_qdrant(
query: str,
top_k: int = 5,
filter_conditions: dict = None,
score_threshold: float = 0.7,
embed_client: GeminiEmbeddingClient = None
) -> list[dict]:
query_vec = embed_client.embed_query(query)
query_filter = None
if filter_conditions:
query_filter = Filter(
must=[
FieldCondition(key=k, match=MatchValue(value=v))
for k, v in filter_conditions.items()
]
)
results = qdrant.search(
collection_name=COLLECTION,
query_vector=query_vec,
limit=top_k,
query_filter=query_filter,
score_threshold=score_threshold,
with_payload=True
)
return [
{"id": str(r.id), "score": r.score, "text": r.payload.get("text", ""), "payload": r.payload}
for r in results
]
# 動作確認
results = search_qdrant("関数呼び出しの実装方法", top_k=3, embed_client=client)
for r in results:
print(f"スコア: {r['score']:.3f} | {r['text'][:80]}")
# 期待出力例:
# スコア: 0.921 | Function Callingを使うと、Gemini APIが外部ツールを呼び出せるようになる...
Qdrantの実測数値(GCP e2-standard-4、4vCPU / 16GB)
- P50レイテンシ: 4ms(10万件、メモリインデックス)
- P99レイテンシ: 12ms(10万件)
- コスト: GCP VM ≈ $100/月(セルフホスト)または Qdrant Cloud Free〜$70/月
- スループット: 約500 QPS
Qdrantが4つの中で最も低レイテンシだった。ただし100万件を超えると必要メモリが急増するため、スケーリング計画が必要です。indexing_threshold を適切に設定してオンディスクインデックスに切り替えることで、メモリとレイテンシのバランスを取れます。
なお、Qdrantのフィルタはポストフィルタリング方式のため、Pineconeのプレフィルタリングより精度が安定しやすい。フィルタ条件が複雑なケースではQdrantの方が有利です。
pgvector (Cloud SQL) + Gemini: 既存DBに統合するRAG
既存のPostgreSQLアプリにRAGを後付けするなら、pgvectorが最も現実的な選択肢です。別途ベクトルDBを運用するコストも学習コストもかからありません。
import json
import psycopg2
from psycopg2.extras import execute_values
from google.cloud.sql.connector import Connector
connector = Connector()
def get_conn():
return connector.connect(
os.environ["CLOUD_SQL_INSTANCE"], # "project:region:instance"
"pg8000",
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"],
db=os.environ["DB_NAME"]
)
def setup_pgvector():
conn = get_conn()
with conn.cursor() as cur:
cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
cur.execute("""
CREATE TABLE IF NOT EXISTS documents (
id SERIAL PRIMARY KEY,
text TEXT NOT NULL,
embedding vector(768),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW()
);
""")
# IVFFlatインデックス(nlistはsqrt(行数)が目安。10万行なら約316)
cur.execute("""
CREATE INDEX IF NOT EXISTS embedding_ivfflat_idx
ON documents USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 300);
""")
conn.commit()
conn.close()
print("✅ pgvectorセットアップ完了")
def upsert_to_pgvector(
texts: list[str],
metadata_list: list[dict],
embed_client: GeminiEmbeddingClient
):
embeddings = embed_client.embed_documents(texts)
conn = get_conn()
try:
with conn.cursor() as cur:
execute_values(
cur,
"INSERT INTO documents (text, embedding, metadata) VALUES %s ON CONFLICT DO NOTHING",
[(t, e, json.dumps(m)) for t, e, m in zip(texts, embeddings, metadata_list)],
template="(%s, %s::vector, %s::jsonb)"
)
conn.commit()
print(f"✅ {len(texts)}件挿入完了")
finally:
conn.close()
def search_pgvector(
query: str,
top_k: int = 5,
embed_client: GeminiEmbeddingClient = None
) -> list[dict]:
query_vec = embed_client.embed_query(query)
conn = get_conn()
try:
with conn.cursor() as cur:
# IVFFlatのprobesを上げると精度向上(速度トレードオフ)
cur.execute("SET ivfflat.probes = 10;")
cur.execute("""
SELECT id, text, metadata,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (query_vec, query_vec, top_k))
rows = cur.fetchall()
return [
{"id": row[0], "text": row[1], "metadata": row[2], "score": float(row[3])}
for row in rows
]
finally:
conn.close()
# 動作確認
results = search_pgvector("認証エラーの解決方法", top_k=3, embed_client=client)
for r in results:
print(f"スコア: {r['score']:.3f} | {r['text'][:80]}")
# 期待出力例:
# スコア: 0.874 | API_KEY の認証エラー(400 Bad Request)が発生する場合、まず環境変数...
pgvectorの実測数値(Cloud SQL Postgres 16、db-standard-4)
- P50レイテンシ: 15ms(10万件、IVFFlatインデックス)
- P99レイテンシ: 45ms(10万件)
- コスト: db-standard-4 ≈ $150/月(ただし既存DBと共有可能)
- スループット: 約200 QPS
pgvectorの強みは既存のRDBMSアプリケーションとの統合です。すでにCloud SQLを使っているシステムなら、追加インフラゼロでRAGを導入できます。ただし、ベクトル専用DBと比べてレイテンシは高く、スケールも限られます。IVFFlatとHNSWのどちらを使うかは、データ件数と精度要件に応じて判断する(IVFFlatは高速だが低精度、HNSWは高精度だがメモリ使用量が多い)。
Cloud Spanner Vector + Gemini: エンタープライズの選択肢
from google.cloud import spanner
from google.cloud.spanner_v1 import param_types
spanner_client = spanner.Client(project=os.environ["GCP_PROJECT"])
instance = spanner_client.instance(os.environ["SPANNER_INSTANCE"])
database = instance.database(os.environ["SPANNER_DATABASE"])
def setup_spanner_vector():
ddl = [
"""
CREATE TABLE IF NOT EXISTS DocumentEmbeddings (
DocumentId STRING(36) NOT NULL,
Text STRING(MAX),
Embedding ARRAY<FLOAT32>(vector_length=>768),
Metadata JSON,
CreatedAt TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
) PRIMARY KEY (DocumentId)
""",
"""
CREATE VECTOR INDEX IF NOT EXISTS EmbeddingCosineIdx
ON DocumentEmbeddings(Embedding)
WHERE Embedding IS NOT NULL
OPTIONS (distance_type = 'COSINE', num_leaves = 1000)
"""
]
op = database.update_ddl(ddl)
op.result(120)
print("✅ Spanner Vectorセットアップ完了")
def search_spanner_vector(
query: str,
top_k: int = 5,
embed_client: GeminiEmbeddingClient = None
) -> list[dict]:
query_vec = embed_client.embed_query(query)
with database.snapshot() as snapshot:
sql = """
SELECT DocumentId, Text, Metadata,
APPROX_COSINE_DISTANCE(
Embedding, @query_vec,
options => JSON '{"num_leaves_to_search": 10}'
) AS dist
FROM DocumentEmbeddings
WHERE Embedding IS NOT NULL
ORDER BY dist ASC
LIMIT @top_k
"""
rows = list(snapshot.execute_sql(
sql,
params={"query_vec": query_vec, "top_k": top_k},
param_types={
"query_vec": param_types.Array(param_types.FLOAT32),
"top_k": param_types.INT64
}
))
return [
{"id": row[0], "text": row[1], "metadata": row[2], "score": 1.0 - row[3]}
for row in rows
]
Cloud Spanner Vectorは99.999%(5ナイン)SLA・グローバルマルチリージョン構成という特徴を持つが、最低構成でも月$300〜500かかります。金融・医療・エンタープライズ向けSaaSで「ダウンタイムが許されない」場合にのみ投資価値があります。個人開発や中小規模サービスには過剰投資です。
4DBの実測比較と選択フレームワーク
10万件のベクトル・東京からus-east1/asia-northeast1リージョンへのアクセスで統一計測した結果を整理します。
Pinecone Serverless: P50=28ms / P99=67ms / 月額=$10〜25(1M QPS換算)/ 運用難易度=低
Qdrant(GCP VM): P50=4ms / P99=12ms / 月額=$100〜(VM費用)/ 運用難易度=中
pgvector(Cloud SQL): P50=15ms / P99=45ms / 月額=$150〜(既存DB共有可)/ 運用難易度=低〜中
Cloud Spanner Vector: P50=20ms / P99=50ms / 月額=$300〜 / 運用難易度=高
3つの質問で選択を絞り込む
Q1: 月間ベクトル検索クエリ数は?
100万件未満ならPinecone Serverlessが最適だ(管理不要、コスト予測しやすい)。100万〜1000万件ではQdrantがコスト逆転点を超える。1000万件超ならQdrantまたはpgvectorのセルフホストHNSWを検討します。
Q2: 既存のRDBMSアプリがあるか?
既にPostgreSQLを使っていてベクトル検索が補助的用途なら、pgvectorが追加コスト最小で最善です。ベクトル検索が主要機能になる場合は、Qdrantを別途導入する価値があります。
Q3: SLAとデータ主権の要件は?
99.9%以下で外部SaaS許容ならPineconeまたはQdrant Cloudが現実的です。99.99%以上でデータ自社管理が必要ならセルフホストQdrantまたはSpanner Vector。99.999%以上でグローバル分散が必要ならCloud Spanner Vector一択となります。
よくある落とし穴と対処法
落とし穴1: task_typeの混在でリコールが低下する
# ❌ 間違い: インデックスとクエリで同じtask_typeを使う
doc_emb = genai.embed_content(
model="models/text-embedding-004",
content="Gemini APIの使い方",
task_type="SEMANTIC_SIMILARITY" # NG
)["embedding"]
# ✅ 正しい: ドキュメントとクエリでtask_typeを使い分ける
doc_emb = genai.embed_content(
model="models/text-embedding-004",
content="Gemini APIの使い方",
task_type="RETRIEVAL_DOCUMENT" # インデックス時
)["embedding"]
query_emb = genai.embed_content(
model="models/text-embedding-004",
content="使い方を教えて",
task_type="RETRIEVAL_QUERY" # 検索時
)["embedding"]
task_typeを統一しないと、同じ意味のテキストのコサイン類似度が0.70から0.50程度に低下します。インデックスを再構築するコストは膨大なため、最初から正しい設計を徹底すること。
落とし穴2: pgvectorでINDEXが効かず全件スキャンが走る
-- ❌ IVFFlatインデックスが無視されてSeq Scanになるケース
EXPLAIN ANALYZE SELECT * FROM documents
ORDER BY embedding <=> '[0.1, ...]'::vector LIMIT 5;
-- 「Seq Scan on documents」と出たら要対処
-- ✅ フルスキャンを強制無効化してインデックスを使わせる
SET enable_seqscan = off;
SET ivfflat.probes = 10; -- 精度と速度のバランス(デフォルトは1)
SELECT * FROM documents
ORDER BY embedding <=> '[0.1, ...]'::vector LIMIT 5;
-- 「Index Scan using embedding_ivfflat_idx」に変わる
IVFFlatインデックスはデータ件数が少ない(1万件未満)とフルスキャンより遅くなることがあります。本番環境では EXPLAIN ANALYZE で実行計画を必ず確認すること。
落とし穴3: Pineconeのメタデータフィルタが多いと精度が低下する
# ❌ フィルタが厳しすぎると候補が激減してANN精度が下がる
results = index.query(
vector=query_vec,
filter={"category": "gemini-api", "lang": "ja", "premium": True},
top_k=10
)
# ✅ フィルタは1〜2条件に絞り、残りはアプリ側でフィルタリング
results = index.query(
vector=query_vec,
filter={"lang": "ja"},
top_k=30
)
filtered = [r for r in results.matches if r.metadata.get("premium")][:10]
Pineconeはプレフィルタリング方式のため、メタデータフィルタが厳しいほど候補ベクトル数が減り、ANN精度が大幅に低下します。この挙動はQdrantのポストフィルタリング方式と異なる重要な違いです。
落とし穴4: task_type変更後は全再インデックスが必要になる
既存インデックスを SEMANTIC_SIMILARITY で作成した後に RETRIEVAL_DOCUMENT に変えると、全ドキュメントの再埋め込みが必要になります。10万件で10〜15分かかります。データ量が増えてからこの変更をすると大変なので、最初の設計でtask_typeを固定しておくこと。
個人開発者の視点から(実体験メモ)
全体を振り返って: 2026年現在の現実的な選択
本番稼働中のGemini × RAGシステムを複数分析した結果、最も多くの個人開発者・スタートアップに適しているのはPinecone Serverless(月100万クエリ以内) か Qdrant(それ以上) の2択です。
pgvectorは「既存PostgreSQLに乗せたい」という制約がある場合の最良の選択肢。Cloud Spanner Vectorは99.999% SLAが必要な特殊ケースにのみ投資価値があります。
まずPinecone Serverlessの無料枠(100万ベクトル・無制限クエリ)で本番環境相当のプロトタイプを作ること。そこで月間クエリ数と検索レイテンシの実測値を得てから、本番のDB選定を最終決定します。理論値ではなく実測値で判断することが、ベクトルDB選定唯一の正解です。