pgvector-semantic-search について
PostgreSQL と pgvector でセマンティック検索を構築する開発者向けの実践ガイドです。ベクトルの保存、インデックス、クエリ調整に加え、量子化やフィルター検索も扱います。
主な機能
- 01halfvec とコサイン距離の設定
- 02HNSW インデックスの作成と調整
- 03バイナリ量子化と再ランキング
- 04フィルター付きベクトル検索への対応
- 05一括ロードとインデックス保守
活用シーン
- 01テキスト埋め込みのセマンティック検索構築
- 02大規模ベクトルデータの検索調整
- 03フィルター検索の再現率向上
- 04ベクトルインデックス性能の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ timescale/ pg-aiguide --skill pgvector-semantic-searchインストール後、Claude Code で/pgvector-semantic-searchを入力して読み込まれたか確認します。
詳細
セマンティック検索のための pgvector
セマンティック検索では、完全一致するキーワードではなく、意味に基づいてコンテンツを検索します。埋め込みモデルはテキストを高次元ベクトルに変換し、意味が似ているものは近い位置に対応します。pgvector はこれらのベクトルを PostgreSQL に保存し、近似最近傍(ANN)インデックスを使って最も近い一致をすばやく検索します。データベースを離れることなく、数百万行までスケールできます。テキストとその埋め込みを一緒に保存し、検索テキストをベクトルに変換して、距離が最小の行を返すようクエリします。
このガイドでは、pgvector のセットアップとチューニングを解説します。検索品質に大きく影響する埋め込みモデルの選択やテキストのチャンク化は対象外です。すべての機能(halfvec、binary_quantize、反復スキャン)を使うには pgvector 0.8.0+ が必要です。
推奨設定(デフォルト)
特別な理由がない限り、この設定を使用してください。
- 埋め込み列のデータ型:
halfvec(N)。Nは埋め込みの次元数です(すべての箇所で一致させる必要があります)。例では 1536 を使用しています。ご使用の次元数Nに置き換えてください。 - 距離:コサイン(
<=>) - インデックス:HNSW(
m = 16、ef_construction = 64)。halfvec_cosine_opsを使用し、<=>でクエリします。 - クエリ時の再現率:
SET hnsw.ef_search = 100(公開されているベンチマークを参考にした適切な初期値です。再現率を高めるには、レイテンシーの増加を許容して値を上げてください) - クエリパターン:
ORDER BY embedding <=> $1::halfvec(N) LIMIT k
この設定は、ほとんどのテキスト埋め込みワークロードで、速度と再現率のバランスに優れています。
基本ルール
- 各データベースで拡張機能を有効にします:
CREATE EXTENSION IF NOT EXISTS vector; - デフォルトでは HNSW インデックスを使用します。速度と再現率のバランスに優れ、空のテーブルにも作成でき、トレーニング工程も不要です。書き込みの多いワークロードやメモリーに制約のあるワークロードでのみ、IVFFlat を検討してください。
- デフォルトでは
halfvecを使用します。halfvec形式で保存し、インデックスを作成すると、再現率をほとんど損なわずにストレージとインデックスのサイズを 50% 削減できます。 - 初期データを一括ロードした後にインデックスを作成すると、構築パフォーマンスが最も高くなります。
- 本番環境ではインデックスを同時作成します:
CREATE INDEX CONCURRENTLY ... - デフォルトではコサイン距離(
<=>)を使用します。正規化されていない埋め込みにはコサイン距離を使用してください。単位正規化された埋め込みでは、コサイン距離と内積の順位は同じになります。デフォルトではコサイン距離を使用してください。 - クエリ演算子をインデックスの演算子クラスに合わせます:
halfvec_cosine_opsで作成したインデックスには、クエリで<=>が必要です。halfvec_l2_opsには<->が必要です。演算子が一致しないと、インデックスは使用されません。 - クエリベクトルは必ず明示的にキャストします(
$1::halfvec(N))。これにより、準備済みステートメントでの暗黙的なキャスト失敗を防げます。 - データとクエリには必ず同じ埋め込みモデルを使用します。ベクトルを生成するモデルが同一でなければ、類似検索は機能しません。
型のルール
- 埋め込みは
halfvec(N)として保存します - クエリベクトルは
halfvec(N)にキャストします - 二値量子化されたベクトルは、生成列に
bit(N)として保存します - 明示的なキャストなしに
vector/halfvec/bitを混在させないでください ORDER BY内でテーブル列に対してbinary_quantize()を呼び出さないでください。代わりに保存してください- 次元数を一致させる必要があります:
halfvec(1536)列には、::halfvec(1536)にキャストしたクエリベクトルが必要です。
標準パターン
-- Store and index as halfvec
CREATE TABLE items (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
contents TEXT NOT NULL,
embedding halfvec(1536) NOT NULL -- NOT NULL requires embeddings generated before insert, not async
);
CREATE INDEX ON items USING hnsw (embedding halfvec_cosine_ops);
-- Query: returns 10 closest items. $1 is the embedding of your search text.
SELECT id, contents FROM items ORDER BY embedding <=> $1::halfvec(1536) LIMIT 10;
L2 距離や内積など、その他の距離演算子については、pgvector README を参照してください。
HNSW インデックス
推奨されるインデックスの種類です。速度と再現率のバランスに優れた、多層のナビゲーショングラフを作成します。空のテーブルにも作成でき(トレーニング工程は不要です)。
CREATE INDEX ON items USING hnsw (embedding halfvec_cosine_ops);
-- With tuning parameters
CREATE INDEX ON items USING hnsw (embedding halfvec_cosine_ops) WITH (m = 16, ef_construction = 64);
HNSW パラメーター
| パラメーター | デフォルト | 説明 |
|---|---|---|
m | 16 | レイヤーあたりの最大接続数です。値を上げると再現率が向上しますが、メモリー使用量も増加します |
ef_construction | 64 | 構築時の候補リストです。値を上げるとグラフの品質が向上しますが、構築に時間がかかります |
hnsw.ef_search | 40 | クエリ時の候補リストです。値を上げると再現率が向上しますが、クエリに時間がかかります。LIMIT 以上にする必要があります。 |
ef_search の調整(大まかな目安。実際の結果はデータセットによって異なります):
| ef_search | おおよその再現率 | 相対速度 |
|---|---|---|
| 40 | 低め(一部のベンチマークで約95%) | 1x(基準) |
| 100 | 高め | 約2倍遅い |
| 200 | 非常に高い | 約4倍遅い |
| 400 | ほぼ完全 | 約8倍遅い |
-- Set search parameter for session
SET hnsw.ef_search = 100;
-- Set for single query
BEGIN;
SET LOCAL hnsw.ef_search = 100;
SELECT id, contents FROM items ORDER BY embedding <=> $1::halfvec(1536) LIMIT 10;
COMMIT;
IVFFlat インデックス(一般的には非推奨)
デフォルトでは HNSW を使用してください。IVFFlat は、最高の再現率よりも HNSW の運用コストを重視する場合にのみ使用してください。
次の場合は IVFFlat を選択してください:
- 書き込みが多い、またはデータが常に変化し、インデックスを頻繁に再構築してもよい場合
- インデックスを頻繁に再構築しており、予測しやすい構築時間とメモリー使用量を求める場合
- メモリーに余裕がなく、HNSW グラフの大部分をメモリー上に保持できない場合
- データがパーティション化または階層化されており、このインデックスをアクセス頻度の低いパーティションに置く場合
次の要件がある場合は IVFFlat を避けてください:
- 低レイテンシーで最高の再現率が必要
- 調整を最小限にしたい
- 「設定したら後は放置」できるインデックスが必要
注意:
- IVFFlat では、インデックス作成前にデータが存在している必要があります。
- 再現率は
listsとivfflat.probesに依存します。probes を上げると再現率が向上しますが、クエリに時間がかかります。
初期設定:
CREATE INDEX ON items
USING ivfflat (embedding halfvec_cosine_ops)
WITH (lists = 1000);
SET ivfflat.probes = 10;
量子化戦略
- 量子化はメモリーに関する判断であり、再現率に関する判断ではありません。
- ストレージとインデックス作成には、デフォルトで
halfvecを使用してください。 - HNSW インデックスのフットプリントは、1536 次元の
halfvec(m=16)あたり約 4~6 KB と見積もってください(概算)。3072 次元では約 2 倍になり、m=32 では HNSW のリンク/グラフのオーバーヘッドがおよそ 2 倍になります。 - CPU がほとんどアイドル状態なのに p95/p99 レイテンシーが上昇する場合、HNSW インデックスがメモリー上に常駐しなくなっている可能性が高いです。
halfvecが収まらない場合は、二値量子化と再ランキングを使用してください。
1536 次元ベクトルのガイドライン
m=16、1536 次元の場合の halfvec の容量の目安(RAM の大部分をインデックスのキャッシュに利用できると仮定):
| RAM | halfvec ベクトルのおおよその最大数 |
|---|---|
| 16 GB | 約200~300万ベクトル |
| 32 GB | 約400~600万ベクトル |
| 64 GB | 約800~1200万ベクトル |
| 128 GB | 約1600~2500万ベクトル |
3072 次元の埋め込みでは、これらの数値を約 2 で割ってください。
m=32 の場合も、容量を約 2 で割ってください。
この規模でインデックスがメモリーに収まらない場合は、二値量子化を使用してください。
これらは範囲の目安であり、保証値ではありません。負荷時のキャッシュ常駐率と p95/p99 レイテンシーを監視して検証してください。
二値量子化(非常に大規模なデータセット向け)
メモリーを 32 分の 1 に削減できます。許容できる再現率を得るには、再ランキングと併用してください。
-- Table with generated column for binary quantization
CREATE TABLE items (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
contents TEXT NOT NULL,
embedding halfvec(1536) NOT NULL,
embedding_bq bit(1536) GENERATED ALWAYS AS (binary_quantize(embedding)::bit(1536)) STORED
);
CREATE INDEX ON items USING hnsw (embedding_bq bit_hamming_ops);
-- Query with re-ranking for better recall
-- ef_search must be >= inner LIMIT to retrieve enough candidates
SET hnsw.ef_search = 800;
WITH q AS (
SELECT binary_quantize($1::halfvec(1536))::bit(1536) AS qb
)
SELECT *
FROM (
SELECT i.id, i.contents, i.embedding
FROM items i, q
ORDER BY i.embedding_bq <~> q.qb -- computes binary distance using index
LIMIT 800
) candidates
ORDER BY candidates.embedding <=> $1::halfvec(1536) -- computes halfvec distance (no index), more accurate than binary
LIMIT 10;
オーバーサンプリング比 80 倍(10 件の結果に対して 800 件の候補)は、妥当な出発点です。二値量子化では精度が失われるため、再ランキング中に真の最近傍を見つけるには、より多くの候補が必要です。再現率が不十分な場合は増やし、再ランキングのレイテンシーが高すぎる場合は減らしてください。
データセット規模ごとのパフォーマンス
| 規模 | ベクトル数 | 設定 | 備考 |
|---|---|---|---|
| 小規模 | <100K | デフォルト | インデックスは任意ですが、テールレイテンシーの改善に役立ちます |
| 中規模 | 100K–5M | デフォルト | p95 レイテンシーを監視してください。一般的な本番環境の規模です |
| 大規模 | 5M+ | ef_construction=100+ | メモリー常駐が重要です |
| 非常に大規模 | 10M+ | 二値量子化+再ランキング | 可能であれば、まず RAM の増設またはパーティション化を行ってください |
再現率の調整では、まず ef_search を調整してください。m の増加は、再現率が頭打ちになり、メモリーに余裕がある場合に限って行ってください。並行処理時にインデックスがメモリーに収まらないと、テールレイテンシーが急上昇します。二値量子化は最後の手段です。まずは RAM の増設またはパーティション化を検討してください。
フィルタリングのベストプラクティス
フィルターを使用したベクトル検索には注意が必要です。フィルターの選択性やクエリの形状によって、フィルターが早期終了(行数不足や結果の欠落)を引き起こしたり、処理量(レイテンシー)を増やしたりすることがあります。
反復スキャン(フィルターの選択性が高い場合に推奨)
デフォルトでは、WHERE 句があると HNSW が早期に検索を終了することがあり、期待よりも少ない結果しか返されない場合があります。反復スキャンを有効にすると、フィルターに一致する行が十分に見つかるまで HNSW が検索を続けます。
フィルターによって結果セットが大幅に絞り込まれる場合は、反復スキャンを有効にしてください。
-- Enable iterative scans for filtered queries
SET hnsw.iterative_scan = relaxed_order;
SELECT id, contents
FROM items
WHERE category_id = 123
ORDER BY embedding <=> $1::halfvec(1536)
LIMIT 10;
それでも結果が少ない場合は、スキャンの予算を増やします。
SET hnsw.max_scan_tuples = 50000;
トレードオフ:hnsw.max_scan_tuples を増やすと再現率は向上しますが、レイテンシが大幅に増加する可能性があります。
反復スキャンが不要な場合:
- フィルターがテーブルの大部分に一致する場合(選択性が低い)
- B-tree インデックスを使用して事前フィルタリングしている場合
- 単一のパーティションまたは部分インデックスをクエリする場合
適切なフィルタリング戦略を選ぶ
選択性の高いフィルター(約 1 万行未満) フィルター列に B-tree インデックスを使用すると、Postgres が ANN の前に事前フィルタリングできます。
CREATE INDEX ON items (category_id);
カーディナリティの低いフィルター(異なる値が少ない場合) フィルター値ごとに部分 HNSW インデックスを使用します。
CREATE INDEX ON items
USING hnsw (embedding halfvec_cosine_ops)
WHERE category_id = 11;
フィルター値が多い場合、またはデータセットが大きい場合 フィルターキーでパーティション分割し、各 ANN インデックスを小さく保ちます。
CREATE TABLE items (
embedding halfvec(1536),
category_id int
) PARTITION BY LIST (category_id);
主なルール
- 一致する行が少ないフィルターには、事前フィルタリング、パーティション分割、または反復スキャンが必要です。
- 現実的な負荷のもとで p95/p99 レイテンシと訪問タプル数を測定し、フィルター付きクエリを必ず検証してください。
代替手段:ラベルベースのフィルタリングに pgvectorscale を使用する
ラベルベースのフィルターを使用する大規模なデータセットでは、pgvectorscale の StreamingDiskANN インデックスが smallint[] 列のフィルター付きインデックスをサポートします。ベクトルと併せてラベルをインデックス化するため、HNSW の後処理フィルタリングで精度が低下することなく、効率的なフィルター付き検索が可能です。セットアップの詳細については、pgvectorscale のドキュメントを参照してください。
一括読み込み
-- COPY is fastest; binary format is faster but requires proper encoding
-- Text format: '[0.1, 0.2, ...]'
COPY items (contents, embedding) FROM STDIN;
-- Binary format (if your client supports it):
COPY items (contents, embedding) FROM STDIN WITH (FORMAT BINARY);
-- Add indexes AFTER loading
SET maintenance_work_mem = '4GB';
SET max_parallel_maintenance_workers = 7;
CREATE INDEX ON items USING hnsw (embedding halfvec_cosine_ops);
メンテナンス
- 定期的に VACUUM を実行してください。更新や削除の後、古いエントリーは VACUUM が実行されるまで残る場合があります。
- パフォーマンスが低下した場合は REINDEX を実行してください。高頻度の変更後にグラフを最初から再構築します。
- 削除が頻繁に発生する書き込み負荷の高いワークロードでは、IVFFlat の使用、または hypertables を使用した時間によるパーティション分割を検討してください。
モニタリングとデバッグ
-- Check index size
SELECT pg_size_pretty(pg_relation_size('items_embedding_idx'));
-- Debug query performance
EXPLAIN (ANALYZE, BUFFERS) SELECT id, contents FROM items ORDER BY embedding <=> $1::halfvec(1536) LIMIT 10;
-- Monitor index build progress
SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%"
FROM pg_stat_progress_create_index;
-- Compare approximate vs exact recall
BEGIN;
SET LOCAL enable_indexscan = off; -- Force exact search
SELECT id, contents FROM items ORDER BY embedding <=> $1::halfvec(1536) LIMIT 10;
COMMIT;
-- Force index use for debugging
BEGIN;
SET LOCAL enable_seqscan = off;
SELECT id, contents FROM items ORDER BY embedding <=> $1::halfvec(1536) LIMIT 10;
COMMIT;
よくある問題(症状 → 対処方法)
| 症状 | 考えられる原因 | 対処方法 |
|---|---|---|
| クエリで ANN インデックスが使用されない | ORDER BY と LIMIT がない、演算子が一致しない、または暗黙の型キャストが発生している | インデックスの演算子クラスに一致する距離演算子で ORDER BY を使用し、クエリベクトルを明示的に型キャストします |
| 期待よりも少ない結果しか返されない(フィルター付きクエリ) | フィルターにより HNSW が早期に検索を終了する | 反復スキャンを有効にする、hnsw.max_scan_tuples を増やす、または事前フィルタリング(B-tree)、部分インデックス、パーティション分割を使用します |
| 期待よりも少ない結果しか返されない(フィルターなしクエリ) | ANN の再現率が低すぎる | hnsw.ef_search を増やします |
| CPU 使用率が低いのにレイテンシが高い | HNSW インデックスがメモリに常駐していない | halfvec を使用する、m/ef_construction を減らす、RAM を増やす、パーティション分割する、またはバイナリ量子化を使用します |
| インデックスの構築が遅い | 構築用メモリまたは並列処理が不十分 | maintenance_work_mem と max_parallel_maintenance_workers を増やし、一括読み込みの後に構築します |
| メモリ不足エラーが発生する | インデックスが利用可能な RAM に対して大きすぎる | halfvec を使用する、インデックスのパラメーターを減らす、または再ランキングを伴うバイナリ量子化に切り替えます |
| 結果がゼロ件、または一部の結果が欠落する | NULL またはゼロのベクトル | NULL の埋め込みを避け、コサイン距離ではゼロベクトルを使用しないでください |