- Authors
- Name
- 1. なぜ PostgreSQL + pgvector なのか
- 2. インストールと設定
- 3. 基本的な使い方
- 4. インデックス: HNSW vs IVFFlat
- 5. ハイブリッド検索: ベクトル + 全文検索
- 6. RAG パイプライン統合
- 7. パフォーマンスチューニング
- 8. クイズ
1. なぜ PostgreSQL + pgvector なのか
専用ベクトル DB(Pinecone、Qdrant、Weaviate)の代わりに PostgreSQL + pgvector を選択する理由:
| 特性 | 専用ベクトル DB | PostgreSQL + pgvector |
|---|---|---|
| 追加インフラ | 必要 | 不要(既存 PostgreSQL 活用) |
| ACID トランザクション | 限定的 | 完全サポート |
| JOIN/リレーショナル | 不可 | 自由に組み合わせ可能 |
| ハイブリッド検索 | 限定的 | tsvector + vector |
| 運用の複雑さ | 高い | 低い(既存 DBA を活用) |
| スケーラビリティ | 数十億ベクトル | 数千万レベル |
結論: ベクトル数が数千万以下で、リレーショナルデータと一緒に管理する必要がある場合、pgvector が最適。
2. インストールと設定
2.1 pgvector のインストール
# Ubuntu/Debian
sudo apt install postgresql-17-pgvector
# macOS (Homebrew)
brew install pgvector
# Docker
docker run -d --name pgvector \
-e POSTGRES_PASSWORD=secret \
-p 5432:5432 \
pgvector/pgvector:pg17
2.2 拡張の有効化
-- pgvector 拡張をインストール
CREATE EXTENSION IF NOT EXISTS vector;
-- バージョン確認
SELECT extversion FROM pg_extension WHERE extname = 'vector';
-- 0.8.0
3. 基本的な使い方
3.1 テーブル作成
-- ドキュメントテーブル(1536次元 = OpenAI text-embedding-3-small)
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
embedding vector(1536), -- ベクトルカラム!
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 384次元(sentence-transformers/all-MiniLM-L6-v2)
CREATE TABLE chunks (
id BIGSERIAL PRIMARY KEY,
doc_id BIGINT REFERENCES documents(id),
chunk_text TEXT NOT NULL,
embedding vector(384),
chunk_index INT
);
3.2 データの挿入
-- 単一挿入
INSERT INTO documents (title, content, embedding)
VALUES (
'Kubernetes RBAC Guide',
'RBAC is a method of regulating access...',
'[0.1, 0.2, 0.3, ...]'::vector -- 1536次元ベクトル
);
-- Python からバッチ挿入
import psycopg2
from pgvector.psycopg2 import register_vector
import numpy as np
conn = psycopg2.connect("dbname=mydb user=postgres password=secret")
register_vector(conn)
cur = conn.cursor()
# OpenAI エンベディング生成
from openai import OpenAI
client = OpenAI()
texts = ["Kubernetes RBAC guide", "Docker networking basics", ...]
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
# バッチ挿入
for text, emb_data in zip(texts, response.data):
embedding = np.array(emb_data.embedding)
cur.execute(
"INSERT INTO documents (title, content, embedding) VALUES (%s, %s, %s)",
(text, text, embedding)
)
conn.commit()
3.3 類似度検索
-- コサイン類似度(最も一般的)
SELECT id, title,
1 - (embedding <=> '[0.1, 0.2, ...]'::vector) AS similarity
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
-- L2 距離
SELECT id, title,
embedding <-> '[0.1, 0.2, ...]'::vector AS distance
FROM documents
ORDER BY embedding <-> '[0.1, 0.2, ...]'::vector
LIMIT 10;
-- 内積(Inner Product) — 正規化ベクトルではコサインと同等
SELECT id, title,
(embedding <#> '[0.1, 0.2, ...]'::vector) * -1 AS similarity
FROM documents
ORDER BY embedding <#> '[0.1, 0.2, ...]'::vector
LIMIT 10;
演算子まとめ:
| 演算子 | 意味 | 用途 |
|---|---|---|
<-> | L2 距離 | ユークリッド距離ベース |
<=> | コサイン距離 | 方向の類似度(最も一般的) |
<#> | 内積(負の値) | 正規化ベクトルで使用 |
4. インデックス: HNSW vs IVFFlat
4.1 IVFFlat
-- IVFFlat インデックスの作成
CREATE INDEX ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- 検索時の probe 数を設定(精度 vs 速度のトレードオフ)
SET ivfflat.probes = 10;
| パラメータ | 説明 | 推奨値 |
|---|---|---|
lists | クラスター数 | sqrt(行数) ~ 行数/1000 |
probes | 検索クラスター数 | lists/10 ~ lists/5 |
4.2 HNSW(推奨)
-- HNSW インデックスの作成(ビルド時間は長いが検索は高速)
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
-- 検索時の ef_search を設定
SET hnsw.ef_search = 100;
| パラメータ | 説明 | 推奨値 |
|---|---|---|
m | 接続数 | 16~64 |
ef_construction | ビルド品質 | 200+ |
ef_search | 検索品質 | 40~200 |
4.3 HNSW vs IVFFlat の比較
| 特性 | IVFFlat | HNSW |
|---|---|---|
| ビルド速度 | 速い | 遅い |
| 検索速度 | 普通 | 速い |
| Recall | 普通(probe依存) | 高い |
| メモリ | 少ない | 多い |
| 更新 | 再ビルドが必要 | リアルタイム可能 |
推奨: ほとんどの場合 HNSW。データが頻繁に変更される場合やメモリが限られている場合は IVFFlat。
5. ハイブリッド検索: ベクトル + 全文検索
5.1 Full-Text Search + Vector Search
-- tsvector カラムの追加
ALTER TABLE documents ADD COLUMN tsv tsvector
GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || content)) STORED;
CREATE INDEX ON documents USING gin(tsv);
-- ハイブリッド検索関数
CREATE OR REPLACE FUNCTION hybrid_search(
query_text TEXT,
query_embedding vector(1536),
match_count INT DEFAULT 10,
vector_weight FLOAT DEFAULT 0.7,
text_weight FLOAT DEFAULT 0.3
)
RETURNS TABLE (id BIGINT, title TEXT, score FLOAT) AS $$
BEGIN
RETURN QUERY
WITH vector_results AS (
SELECT d.id, d.title,
1 - (d.embedding <=> query_embedding) AS vector_score
FROM documents d
ORDER BY d.embedding <=> query_embedding
LIMIT match_count * 3
),
text_results AS (
SELECT d.id, d.title,
ts_rank(d.tsv, plainto_tsquery('english', query_text)) AS text_score
FROM documents d
WHERE d.tsv @@ plainto_tsquery('english', query_text)
LIMIT match_count * 3
),
combined AS (
SELECT
COALESCE(v.id, t.id) AS id,
COALESCE(v.title, t.title) AS title,
COALESCE(v.vector_score, 0) * vector_weight +
COALESCE(t.text_score, 0) * text_weight AS score
FROM vector_results v
FULL OUTER JOIN text_results t ON v.id = t.id
)
SELECT c.id, c.title, c.score
FROM combined c
ORDER BY c.score DESC
LIMIT match_count;
END;
$$ LANGUAGE plpgsql;
-- 使用例
SELECT * FROM hybrid_search(
'Kubernetes RBAC security',
'[0.1, 0.2, ...]'::vector(1536)
);
5.2 Reciprocal Rank Fusion(RRF)
CREATE OR REPLACE FUNCTION rrf_search(
query_text TEXT,
query_embedding vector(1536),
match_count INT DEFAULT 10,
rrf_k INT DEFAULT 60
)
RETURNS TABLE (id BIGINT, title TEXT, rrf_score FLOAT) AS $$
BEGIN
RETURN QUERY
WITH vector_ranked AS (
SELECT d.id, d.title,
ROW_NUMBER() OVER (ORDER BY d.embedding <=> query_embedding) AS rank
FROM documents d
LIMIT match_count * 5
),
text_ranked AS (
SELECT d.id, d.title,
ROW_NUMBER() OVER (
ORDER BY ts_rank(d.tsv, plainto_tsquery('english', query_text)) DESC
) AS rank
FROM documents d
WHERE d.tsv @@ plainto_tsquery('english', query_text)
LIMIT match_count * 5
),
fused AS (
SELECT
COALESCE(v.id, t.id) AS id,
COALESCE(v.title, t.title) AS title,
COALESCE(1.0 / (rrf_k + v.rank), 0) +
COALESCE(1.0 / (rrf_k + t.rank), 0) AS rrf_score
FROM vector_ranked v
FULL OUTER JOIN text_ranked t ON v.id = t.id
)
SELECT f.id, f.title, f.rrf_score
FROM fused f
ORDER BY f.rrf_score DESC
LIMIT match_count;
END;
$$ LANGUAGE plpgsql;
6. RAG パイプライン統合
6.1 Python 完全な例
import psycopg2
from pgvector.psycopg2 import register_vector
from openai import OpenAI
import numpy as np
client = OpenAI()
conn = psycopg2.connect("dbname=ragdb user=postgres password=secret")
register_vector(conn)
def embed(text: str) -> list[float]:
resp = client.embeddings.create(
model="text-embedding-3-small", input=text
)
return resp.data[0].embedding
def rag_query(question: str, top_k: int = 5) -> str:
query_vec = embed(question)
cur = conn.cursor()
cur.execute("""
SELECT title, content,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
WHERE 1 - (embedding <=> %s::vector) > 0.7
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (query_vec, query_vec, query_vec, top_k))
results = cur.fetchall()
context = "\n\n".join([
f"[{r[0]}] (similarity: {r[2]:.3f})\n{r[1]}"
for r in results
])
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": f"Answer based on context:\n{context}"},
{"role": "user", "content": question}
]
)
return response.choices[0].message.content
# 使用例
answer = rag_query("Kubernetes RBAC における ClusterRole と Role の違いは?")
print(answer)
7. パフォーマンスチューニング
7.1 主要設定
-- 作業メモリ(インデックスビルド/検索時)
SET maintenance_work_mem = '2GB'; -- HNSW ビルド時
SET work_mem = '256MB'; -- 検索時
-- 並列処理
SET max_parallel_workers_per_gather = 4;
SET max_parallel_maintenance_workers = 4;
-- HNSW ビルド最適化
SET maintenance_work_mem = '4GB';
-- ビルド後に元に戻す
7.2 ベンチマーク
100万ベクトル(1536次元)、PostgreSQL 17 + pgvector 0.8.0:
| インデックス | ビルド時間 | 検索レイテンシ (p50) | Recall@10 | メモリ |
|---|---|---|---|---|
| なし(brute) | - | 850ms | 100% | 0 |
| IVFFlat (lists=1000, probes=50) | 45s | 8ms | 95% | 1.2GB |
| HNSW (m=16, ef=200) | 12min | 3ms | 99% | 2.8GB |
8. クイズ
Q1. pgvector の <=> 演算子は何を計算するか?
コサイン距離(1 - cosine_similarity)。値が小さいほど類似している。類似度に変換するには 1 - (a <=> b)。
Q2. HNSW と IVFFlat のうち検索速度が速いのは?
HNSW。ビルドは遅いが検索は速く、Recall も高い。ほとんどの本番環境で推奨。
Q3. ハイブリッド検索が純粋なベクトル検索より優れている理由は?
ベクトル検索は意味的類似度に強いがキーワードの正確なマッチングに弱い。全文検索と組み合わせることで、意味的類似度 + キーワードの正確さの両方を確保できる。
Q4. RRF(Reciprocal Rank Fusion)の原理は?
各検索結果の順位の逆数を合算する。スコアスケールが異なる検索結果を正規化なしに結合できるのが利点。
Q5. IVFFlat の lists と probes パラメータの関係は?
lists はクラスター数、probes は検索時に探索するクラスター数。probes が大きいほど正確だが遅い。通常 probes = lists/10 ~ lists/5。
Q6. 専用ベクトル DB の代わりに pgvector を選ぶべき場合は?
(1) リレーショナルデータとの JOIN が必要 (2) ACID トランザクションが必要 (3) ベクトル数が数千万以下 (4) 追加インフラの運用負担を最小化したい場合。