メインコンテンツへスキップ
友田 陽大
生成AI・LLM・RAG
PostgreSQL
RAG
パフォーマンス
コスト最適化
Python

pgvector チューニング完全ガイド:HNSW/IVFFlat の再現率×レイテンシ最適化と量子化(halfvec・バイナリ量子化)で速く・安く・正確に

PostgreSQL + pgvector のベクトル検索を本番品質に仕上げるチューニング実装ガイド。HNSW/IVFFlat のパラメータ(m・ef_construction・ef_search・lists・probes)と再現率の測り方、halfvec・バイナリ量子化・subvector でメモリを削る方法、過剰フィルタを防ぐ反復スキャン(iterative scan・0.8.0+)、構築の高速化と運用までを、pgvector 公式ドキュメントに忠実な実コードで解説します。

公開日
読了時間
22分
著者
友田 陽大
シェア

pgvector でベクトル検索を動かすところまでは、驚くほど簡単です。CREATE EXTENSION vectorvector(1024) の列、ORDER BY embedding <=> $1 LIMIT 10 ——これで“それっぽい”意味検索は動きます。

問題はその先です。データが数百万行に育ち、ユーザーが同時に叩き、**「この回答、なぜ的外れなの?」**という声が運用から上がってきたとき。ここで効いてくるのは、SQL の書き方ではなく インデックスと量子化のチューニング です。

この記事は、pgvector を“動く”から“本番で速く・安く・正確に動く”へ引き上げるための実装ガイドです。「どこに埋め込みを置くか」「RAGの全体設計・ハイブリッド検索・冪等インジェスト」は姉妹記事 pgvector で作る本番RAG で扱っているので、本稿はその先——HNSW/IVFFlat の再現率×レイテンシ最適化、量子化によるメモリ削減、過剰フィルタの解決に集中します。題材として、私が構築した 生成AI音声チャットボット(PostgreSQL + pgvector に業務データと埋め込みを集約したRAG接客システム)での設計判断も交えます。

この記事のルール:SQL構文・パラメータ・既定値・量子化の構文は pgvector 公式 README / ドキュメント(v0.8.x 系・2026年6月時点) に基づきます。pgvector は活発に更新されるため(反復スキャンは 0.8.0、hamming_distance / L1のHNSW対応は 0.7.0 で追加)、本番投入前に必ずお使いのバージョンの公式ドキュメントで最新値を要確認してください。コードは実運用で使える形に整えていますが、接続文字列・APIキーは環境変数前提(ハードコード厳禁)です。


0. メンタルモデル:チューニングとは「三角形のどこに立つか」を選ぶこと

近似最近傍(ANN)検索のチューニングで最初に持つべきは、3つの量はトレードオフの関係にあるという地図です。

        再現率 (Recall)
         /      \
        /        \
  レイテンシ ──── メモリ / コスト
  • 再現率(Recall):本来返すべき「正解の近傍」のうち、何割を取れているか。RAGの回答品質に直結する。
  • レイテンシ:1クエリの応答速度。ユーザー体験とスループットを決める。
  • メモリ / コスト:インデックスがRAMに乗るか。次元・行数・量子化で決まる。

3つすべてを同時に最大化することはできません。 ANN検索は「正確さ(厳密kNN)」を少し諦める代わりに「速度」を得る技術だからです。チューニングとは、この三角形のどこに自分の案件を立たせるかを、感覚ではなく計測で決める作業にほかなりません。

だからこの記事の順番は意図的です。(1) インデックスの内部を理解 → (2) 再現率を“測る” → (3) 構築を速くする → (4) 量子化でコストを削る → (5) フィルタの罠を外す → (6) 運用。とくに (2) の「測る」を飛ばしてパラメータをいじるのは、目隠しでダイヤルを回す行為です。検証パスを先に作る——これが本番品質の最大のレバーです。


1. 2つのANNインデックスの内部:HNSW と IVFFlat

pgvector の近似インデックスは HNSWIVFFlat の2種類。パラメータを正しく動かすには、それぞれが何をしているかを一段だけ深く知る必要があります。

HNSW:多層グラフをたどる

HNSW(Hierarchical Navigable Small World)は、ベクトルをノード、近いベクトル同士をエッジでつないだ多層グラフです。検索は上層の粗いグラフから入り、近いノードへ貪欲にたどって下層へ降りていきます。

-- コサイン距離なら vector_cosine_ops(演算子クラスは距離と必ず揃える)
CREATE INDEX ON doc_chunks
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);
パラメータ既定値何を決めるか上げると
m16各ノードが張るエッジの最大数(グラフの密度)再現率↑・メモリ↑・構築↓
ef_construction64構築時の候補リスト幅グラフ品質↑・構築時間↑
hnsw.ef_search40検索時の候補リスト幅(ランタイム)再現率↑・速度↓

ここで決定的に重要なのが、検索品質はランタイムパラメータ hnsw.ef_search で後から調整できることです。インデックスを作り直さずに、セッション単位・クエリ単位で精度↔速度のダイヤルを回せます。

SET hnsw.ef_search = 100;   -- 既定40。大きいほど高再現率・低速
SELECT id, content
FROM doc_chunks
ORDER BY embedding <=> $1
LIMIT 10;

HNSW の最大の運用上の利点は、公式が明記するとおり「学習ステップがないため、1行もデータが無くてもインデックスを作成できる」こと。つまり「テーブルを作る → 先にインデックスを張る → 後からデータを流し込む」という素直な順序が成立します。インジェストが継続的に発生する本番RAGと相性が良いのはこの性質です。音声チャットボットでも、FAQ・商品情報が随時更新される(=インジェストが止まらない)前提から HNSW を選びました。

IVFFlat:クラスタに分けて、近いクラスタだけ探す

IVFFlat(Inverted File with Flat compression)は、ベクトル空間を lists 個のクラスタに事前に分割し、検索時はクエリに近いクラスタ(probes 個)だけを探します。

-- lists はクラスタ数。データを投入した“後”に作るのが鉄則
CREATE INDEX ON doc_chunks
    USING ivfflat (embedding vector_cosine_ops)
    WITH (lists = 100);
パラメータ既定値公式の目安
lists(必須)〜100万行:rows / 1000 / 100万行超:sqrt(rows)
ivfflat.probes1探索するクラスタ数。大きいほど高再現率・低速
SET ivfflat.probes = 10;   -- 既定1。lists に対して何個のクラスタを見るか

IVFFlat の決定的な制約:クラスタリングという学習ステップを持つため、データがある程度入った後に作らないと再現率が壊れます。空テーブルに張ってはいけません。さらに、データ量が大きく変われば lists の見直し(=再構築)が要ります。

決定表:迷ったら HNSW

観点HNSWIVFFlat
クエリ性能(再現率×速度)高い相対的に低い
構築コスト遅い・メモリ多い速い・メモリ少ない
空テーブルへの作成可能(学習なし)不可(投入後に作る)
継続的な追加・更新への耐性強い弱い(lists 再調整が要る)
主な使いどころ本番RAGの第一候補大量データを一括ロードしバッチ運用する用途

結論はシンプルです:迷ったら HNSW。 理由は ETC(Easy To Change)。空テーブルに先に張れるので運用フローが「作る→入れる」で固定でき、データ量の見積もりに依存しません。構築速度とメモリが厳しく、かつ一括ロード+バッチ更新という明確な事情があるときだけ IVFFlat を選びます。

次元の上限に注意vector 型でインデックスを張れるのは最大2,000次元まで。これを超える埋め込みを使うなら、後述の halfvec(最大4,000次元)への式インデックスが逃げ道になります。


2. 再現率を「測ってから」チューニングする(検証ファースト)

ここが本記事で最も重要な章です。 ef_searchprobes を当てずっぽうで上下させる前に、自分のデータでの再現率を数値化する仕組みを作ります。ANN検索は「速いが、たまに正解を取りこぼす」もの。その“たまに”が許容範囲かは、測らなければ分かりません。

厳密kNN(正解)を得る

再現率の分母=「本当の正解」は、**インデックスを使わない総当たり(厳密kNN)**で得ます。pgvector では、プランナのインデックススキャンを切れば、距離計算の総当たり=厳密な並びになります。

-- このトランザクション内だけインデックスを無効化し、厳密kNN(総当たり)を得る
BEGIN;
SET LOCAL enable_indexscan = off;
SET LOCAL enable_bitmapscan = off;
SELECT id FROM doc_chunks ORDER BY embedding <=> $1 LIMIT 10;
COMMIT;

厳密kNNは行数に比例して重くなります。再現率の評価は代表クエリ100〜200本のサンプルで回すのが現実的です。総当たりを速くしたいときは公式どおり SET max_parallel_workers_per_gather = 4; で並列化できます。

再現率ハーネス:ANN vs 厳密kNN の一致率

「同じクエリを、ANNインデックス有りと厳密kNNで叩き、返ってきたIDの重なり(recall@k)を測る」だけです。これを ef_search を変えながら回せば、自分のデータでの再現率↔レイテンシのカーブが手に入ります。

"""pgvector の recall@k を計測するハーネス。
ANN(hnsw.ef_search を可変)と、インデックスを切った厳密kNN を突き合わせ、
ef_search ごとの再現率と p95 レイテンシを出す。チューニングは“この表”を見て決める。"""
from __future__ import annotations

import time
from statistics import quantiles

import psycopg  # 接続情報は環境変数 PGHOST/PGUSER/... 経由(ハードコード禁止)

K = 10
EF_GRID = (40, 80, 120, 200)  # 試す hnsw.ef_search の格子


def exact_topk(conn: psycopg.Connection, qvec: str, k: int) -> set[int]:
    """インデックスを切った厳密kNN = 再現率の“正解”。"""
    with conn.cursor() as cur:
        cur.execute("SET LOCAL enable_indexscan = off")
        cur.execute("SET LOCAL enable_bitmapscan = off")
        cur.execute(
            "SELECT id FROM doc_chunks ORDER BY embedding <=> %s LIMIT %s",
            (qvec, k),
        )
        return {r[0] for r in cur.fetchall()}


def ann_topk(conn: psycopg.Connection, qvec: str, k: int, ef: int) -> tuple[set[int], float]:
    """ANN(HNSW)で取得し、IDの集合と所要時間(ms)を返す。"""
    with conn.cursor() as cur:
        cur.execute("SET LOCAL hnsw.ef_search = %s", (ef,))
        t0 = time.perf_counter()
        cur.execute(
            "SELECT id FROM doc_chunks ORDER BY embedding <=> %s LIMIT %s",
            (qvec, k),
        )
        rows = cur.fetchall()
        elapsed_ms = (time.perf_counter() - t0) * 1_000
    return {r[0] for r in rows}, elapsed_ms


def evaluate(conn: psycopg.Connection, query_vecs: list[str], k: int = K) -> None:
    """ef_search ごとに recall@k 平均と p95 レイテンシを表示。"""
    # 厳密kNN は ef に依存しないので一度だけ計算してキャッシュ
    truth = [exact_topk(conn, q, k) for q in query_vecs]

    for ef in EF_GRID:
        recalls: list[float] = []
        latencies: list[float] = []
        for q, gold in zip(query_vecs, truth):
            got, ms = ann_topk(conn, q, k, ef)
            recalls.append(len(got & gold) / k)  # 重なり / k = recall@k
            latencies.append(ms)
        recall = sum(recalls) / len(recalls)
        p95 = quantiles(latencies, n=20)[18]  # 95パーセンタイル
        print(f"ef_search={ef:>3}  recall@{k}={recall:.3f}  p95={p95:6.1f}ms")

出力はたとえばこうなります(値はデータ依存・例示)。

ef_search= 40  recall@10=0.892  p95=  3.1ms
ef_search= 80  recall@10=0.961  p95=  5.4ms
ef_search=120  recall@10=0.984  p95=  8.2ms
ef_search=200  recall@10=0.995  p95= 14.7ms

この表があれば、チューニングは意思決定になります。 「RAGの回答品質には recall@10 ≥ 0.95 が要る、レイテンシ予算は10ms」なら ef_search=80〜120 を選ぶ——という具合に。再現率がどうしても伸びないなら、構築側の m / ef_construction を上げてインデックスを作り直します。

EXPLAIN でインデックスが効いているか確認する

「速くならない」の大半は、そもそもANNインデックスが使われていないことが原因です。必ず EXPLAIN で確認します。

EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM doc_chunks ORDER BY embedding <=> $1 LIMIT 10;
--   → 出力に "Index Scan using ..._hnsw_..." が出ていればOK。
--     "Seq Scan" なら、距離演算子と演算子クラスの不一致、
--     ORDER BY の形崩れ、または式の不一致を疑う。

落とし穴:インデックスは「ORDER BY embedding <演算子> $1 LIMIT k」という形にだけ効きます。SELECT 句に距離を出すのは表示用。ef_search を上げても EXPLAINSeq Scan なら、まずを直します。


3. インデックス構築を速くする

数百万行への HNSW 構築は、放っておくと数十分〜数時間かかります。公式が挙げる3つのレバーで短縮します。

-- 1) 構築用メモリを増やす。グラフ全体が収まると劇的に速い。
--    収まらないと "graph no longer fits into maintenance_work_mem" の NOTICE が出る=遅くなる合図。
SET maintenance_work_mem = '8GB';

-- 2) 並列ワーカーを増やす(既定2)。+リーダーで構築を並列化。
SET max_parallel_maintenance_workers = 7;
-- ワーカー数を大きくするときは全体上限も忘れずに(既定8)
SET max_parallel_workers = 8;

-- 3) 初期データはロード後にインデックスを張る(公式の基本方針)
CREATE INDEX ON doc_chunks USING hnsw (embedding vector_cosine_ops);

ポイントを公式に忠実に整理します。

  1. maintenance_work_mem:HNSWグラフが丸ごと収まるサイズを確保するのが理想。収まらなくなると公式は NOTICE で警告します。次元を抑える(姉妹記事dimensions=1024 戦略)のは、ここのコストにも直接効きます。
  2. max_parallel_maintenance_workers:既定の2から増やすと並列構築されます。CPUコアと相談して設定。
  3. 初期ロードは「入れてから張る」:大量の初期データがある場合、空インデックスに1行ずつ入れるより、COPY で投入してから一括構築する方が速い。継続インジェストとは別の“初期移行”の話です。

構築の進捗を監視する

長い構築を「ただ待つ」のは不安です。pgvector は標準ビューで進捗を出せます。

SELECT phase,
       round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%"
FROM pg_stat_progress_create_index;
--   HNSW のフェーズ: "initializing" → "loading tuples"

大量初期ロードの定番:テキスト埋め込みの初期投入は COPY doc_chunks (...) FROM STDIN WITH (FORMAT BINARY) が最速。INSERT の1行ずつより桁違いに速く、構築前のロードを縮められます。


4. 量子化でメモリとコストを削る(pgvector集約戦略の心臓部)

ここが**「Postgresに集約してなお軽い」を成立させる**章です。vector(n) は1次元あたり4バイト(float32)。1024次元なら本体だけで約4KB/行、これにHNSWグラフが乗る。行数が増えるとRAMを圧迫し、shared_buffers から溢れた瞬間に性能が崖を落ちます。

対策は量子化——精度をわずかに譲ってストレージを大きく削る。pgvector は式インデックスでこれを実現します。

4-1. halfvec:半精度でストレージ半分

halfvec は1次元2バイト(float16)。ストレージはほぼ半分、インデックスは最大4,000次元まで対応(vector は2,000次元まで)。テキスト埋め込みでは精度劣化はごく小さいことが多く、最初に試すべき低リスクな手です。

-- 列ごと halfvec にする場合
CREATE TABLE items (id bigserial PRIMARY KEY, embedding halfvec(1024));

-- 既存の vector 列はそのまま、インデックスだけ halfvec にキャストして作る(式インデックス)
CREATE INDEX ON doc_chunks
    USING hnsw ((embedding::halfvec(1024)) halfvec_cosine_ops);

-- 検索もキャストを合わせる(インデックスの式と一致させるのが条件)
SELECT id, content
FROM doc_chunks
ORDER BY embedding::halfvec(1024) <=> $1::halfvec(1024)
LIMIT 10;

4-2. バイナリ量子化:約1/30のサイズ+再ランクで精度回復

最も攻めたコスト削減がバイナリ量子化です。各次元の符号だけを1ビットに落とす(binary_quantize)。bit(1024) は約128バイト——vector(1024) の約4KBに対しておよそ1/30。ハミング距離(<~>)で超高速に粗く絞り、元のベクトルで上位だけ再ランクして精度を取り戻す、という二段構えが定石です。

-- 粗い検索用:符号を1ビットに量子化した式インデックス(bit_hamming_ops + <~>)
CREATE INDEX ON doc_chunks
    USING hnsw ((binary_quantize(embedding)::bit(1024)) bit_hamming_ops);
-- 二段検索:① bit で広めに20件 → ② 元ベクトルのコサイン距離で上位10件に精密化
SELECT id, content
FROM (
    SELECT id, content, embedding
    FROM doc_chunks
    ORDER BY binary_quantize(embedding)::bit(1024) <~> binary_quantize($1)
    LIMIT 20                                   -- 粗いふるい(ハミング距離・激速)
) AS coarse
ORDER BY embedding <=> $1                       -- 精密な再ランク(元ベクトル・コサイン)
LIMIT 10;

なぜ効くか:1段目のビット検索は計算もメモリも桁違いに軽い。取りこぼしを避けるため**広め(LIMIT 20〜数倍)**に拾い、2段目で元ベクトルの正確な距離で並べ直す。インデックスはビット版だけがRAMに乗れば済むので、大規模でもインデックスをメモリに収めやすい。再ランク幅(LIMIT 20)は第2章のハーネスで再現率を見ながら決めます。

4-3. subvector(Matryoshka):先頭次元で粗く、全次元で精密に

OpenAI の text-embedding-3-* のような Matryoshka(MRL)対応の埋め込みは「先頭ほど重要な情報が詰まる」性質を持ちます。これを使い、subvector先頭の数百次元だけでインデックスして粗く絞り、全次元で再ランクできます。

-- 先頭256次元だけでインデックス(粗い・軽い)
CREATE INDEX ON doc_chunks
    USING hnsw ((subvector(embedding, 1, 256)::vector(256)) vector_cosine_ops);

-- 二段検索:先頭256次元で20件 → 全1024次元で再ランク
SELECT id, content
FROM (
    SELECT id, content, embedding
    FROM doc_chunks
    ORDER BY subvector(embedding, 1, 256)::vector(256) <=> subvector($1, 1, 256)
    LIMIT 20
) AS coarse
ORDER BY embedding <=> $1
LIMIT 10;

量子化の早見表

手法ストレージ目安(1024次元)精度使いどころ
vector(無圧縮・float32)約4KB/行基準行数が中規模で素直に精度を出したい
halfvec(float16)約2KB/行ほぼ同等まず試す。低リスクにメモリ半減
subvector+再ランク粗い段が小さい高(再ランクで回復)MRL埋め込み(text-embedding-3 等)で大規模
binary_quantize+再ランク約128B/行(粗い段)中〜高(再ランクで回復)超大規模。インデックスをRAMに収めたい

設計判断:量子化は「いきなり最強(バイナリ)」ではなく、halfvec → 再現率を測る → 足りなければ二段検索(subvector / binary) の順で。第2章のハーネスがそのまま判断材料になります。次元を1024に抑えてある 音声チャットボット の設計は、この量子化レバーと噛み合い、専用ベクトルDBを増やさず Postgres 内にインデックスを収め続けることを可能にしました。


5. フィルタの罠:「過剰フィルタ」と反復スキャン(0.8.0+)

本番RAGはほぼ必ずメタデータでの絞り込みを伴います。「このテナントの」「このカテゴリの」「公開済みの」ドキュメントだけを意味検索したい。ところがここに、ANNインデックス特有の最も誤解されやすい罠があります。

過剰フィルタ(over-filtering)とは

公式が挙げる例がそのまま核心です。

条件が全体の10%にマッチする場合、HNSW で既定の hnsw.ef_search = 40 だと、平均で約4行しか条件を満たさない。

理由は、ANNインデックスは「クエリに近い候補を ef_search先に集め、そのあとで WHERE を適用する」順で動くから。候補40件のうちフィルタ条件を満たすのが10%なら、手元に残るのは平均4件——LIMIT 10 に足りません。これが「フィルタを足したら急に結果が減った/精度が落ちた」の正体です。

解き方を選択率で使い分ける

公式が示す対処を、**フィルタの選択率(何割が残るか)**で整理します。

-- (a) フィルタ列に通常インデックス:選択率が低い(=ほとんど落ちる)クエリで有効。
--     プランナが ANN ではなく B-tree を選び、絞ってから距離計算する方が速いことがある。
CREATE INDEX ON doc_chunks (tenant_id);
CREATE INDEX ON doc_chunks (location_id, category_id);   -- 複合条件なら複合インデックス

-- (b) 部分インデックス:少数の固定値で絞るとき(例:特定テナント専用)。
CREATE INDEX ON doc_chunks
    USING hnsw (embedding vector_cosine_ops)
    WHERE (tenant_id = 123);

-- (c) パーティショニング:多数の値で分割するとき(テナントが多い等)。
CREATE TABLE doc_chunks (embedding vector(1024), tenant_id int /* ... */)
    PARTITION BY LIST (tenant_id);

反復スキャン(iterative scans・pgvector 0.8.0)

そして 0.8.0 の目玉が反復スキャンです。過剰フィルタが起きたとき、十分な結果が集まるまでインデックスを自動で追加走査してくれます。既定は off(オプトイン)

-- HNSW:strict_order(距離順を厳密に保つ) or relaxed_order(多少前後するが再現率↑)
SET hnsw.iterative_scan = strict_order;
SET hnsw.max_scan_tuples = 20000;       -- 走査する最大タプル数(既定20000)
SET hnsw.scan_mem_multiplier = 2;       -- 使用メモリを work_mem の倍数で(既定1)

-- IVFFlat:relaxed_order をサポート
SET ivfflat.iterative_scan = relaxed_order;
SET ivfflat.max_probes = 100;           -- 反復で探索するクラスタ数の上限

relaxed_order は再現率を稼げますが、結果が厳密な距離順から少しズレることがあります。厳密な並びが必要なら、公式どおり MATERIALIZED CTE で受けて並べ直すのが定石です。

-- relaxed_order の結果を、確実に距離順へ整え直す
SET hnsw.iterative_scan = relaxed_order;

WITH relaxed AS MATERIALIZED (
    SELECT id, content, embedding <=> $1 AS distance
    FROM doc_chunks
    WHERE tenant_id = 123
    ORDER BY distance
    LIMIT 20
)
SELECT id, content, distance
FROM relaxed
ORDER BY distance + 0      -- "+ 0" は Postgres 17+ で並べ直しが消えるのを防ぐためのイディオム
LIMIT 10;

判断の指針:①まずフィルタ列にインデックスを張って EXPLAIN を見る。プランナが賢く B-tree で絞れているなら、それで十分なことも多い。②それでも過剰フィルタが残るなら iterative_scan を strict_order で有効化。③再現率がさらに要るなら relaxed_order + MATERIALIZED CTE。どの段でも第2章のハーネスで再現率を必ず確認します。

アクセス制御は「DB側」に閉じる

メタデータフィルタは性能の問題であると同時に、セキュリティの境界でもあります。専用ベクトルDBだと「全件検索してアプリでフィルタ」になりがちで情報漏えいの温床ですが、Postgres集約なら**行レベルセキュリティ(RLS)**で「自テナントの行しか物理的に返らない」をDB側で強制できます(Supabase RLSの設計も参照)。

ALTER TABLE doc_chunks ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON doc_chunks
    USING (tenant_id = current_setting('app.tenant_id', true)::int);

アプリがどう間違えても他テナントの行は候補に入らない——最小権限の原則をDBの不変条件として実装するわけです。


6. 運用:HNSW の VACUUM・REINDEX・レプリケーション

チューニングは「作って終わり」ではありません。更新・削除が続く本番では、運用面の癖を知っておく必要があります。

  • VACUUM が重くなりがち:公式いわく「HNSWインデックスの VACUUM は時間がかかることがある。先に REINDEX すると速くなる」。削除が多いテーブルでは、REINDEX INDEX CONCURRENTLY でインデックスを作り直してから VACUUM する運用が有効です。
REINDEX INDEX CONCURRENTLY doc_chunks_embedding_idx;   -- 無停止で再構築
VACUUM doc_chunks;
  • レプリケーション対応:pgvector のインデックスは WAL に乗るため、ストリーミングレプリケーションでそのまま複製されます。リードレプリカに検索を逃がす構成が素直に組めます。
  • 継続的な監視項目
    • インデックスサイズ\di+ / pg_relation_size)が shared_buffers に収まっているか。溢れると性能が崖を落ちる先行指標。
    • 再現率の代理指標:第2章のハーネスを定期実行し、データ分布の変化で再現率が落ちていないか。落ちたら ef_search / m を見直す。
    • EXPLAIN (ANALYZE, BUFFERS) のバッファヒット率:ディスク読みが増えていればメモリ不足のサイン。

PIIの注意:検索クエリ本文には個人情報が含まれ得ます。可観測性のためにクエリやチャンク本文を生ログに残さない。記録するのは「クエリID・Top-kのID・距離・レイテンシ・再現率」といったメタデータに留めます。


7. まとめ:pgvector チューニング・チートシート

迷ったときの早見表です。

  • メンタルモデル:再現率 × レイテンシ × メモリの三角形。測ってから動かす。
  • インデックス:迷ったら HNSW(空テーブルに先に張れる=継続インジェストに強い)。検索品質は再構築不要の hnsw.ef_search、構築品質は m / ef_construction。一括ロード+バッチ運用なら IVFFlat(lists はデータ量に合わせる)。
  • 再現率は必ず測る:ANN vs インデックスを切った厳密kNN の recall@k。EXPLAINIndex Scan を確認。Seq Scan ならまずを直す。
  • 構築の高速化maintenance_work_mem を大きく、max_parallel_maintenance_workers を増やし、ロード後に構築。進捗は pg_stat_progress_create_index
  • 量子化でコスト削減halfvec(半分)から試す → 足りなければ binary_quantize+元ベクトル再ランク(約1/30)subvector(Matryoshka)+再ランク
  • フィルタの罠過剰フィルタは「候補を先に集めてから WHERE」で起きる。フィルタ列インデックス/部分インデックス/パーティショニングを選択率で使い分け、反復スキャン(0.8.0) で自動追加走査。厳密順が要るなら relaxed_order + MATERIALIZED CTE(+ 0)。
  • アクセス制御:メタデータフィルタ=セキュリティ境界。RLSでDB側に閉じる
  • 運用:HNSW の VACUUM は重い → 先に REINDEX CONCURRENTLY。インデックスサイズと再現率を継続監視。

ベクトル検索のチューニングは、魔法のパラメータを探す作業ではありません。「再現率を測る検証パスを先に作り、三角形のどこに立つかを意思決定する」——これに尽きます。pgvector が優れているのは、その意思決定を使い慣れた PostgreSQL の中で、EXPLAIN・式インデックス・RLS といった既存の道具で完結できることです。

私は 生成AI音声チャットボット で、専用ベクトルDBを増設せず PostgreSQL + pgvector に業務データと埋め込みを集約し、text-embedding-3-large(1024次元)・HNSW・top-10検索の本番RAGを、一人 × 生成AI(Claude Code)で設計・実装・運用まで通しました。次元・インデックス・量子化・フィルタを計測に基づいて選び、“データストアを1つに保つ”運用の単純さの上に、速さと正確さを両立させています。

「pgvector が遅い/精度が出ない/コストが膨らむ」——その原因の切り分けから、インデックス設計・量子化・フィルタ最適化・運用までを一気通貫で伴走できます。 RAGの全体設計から入りたい方は姉妹記事 pgvector で作る本番RAG もどうぞ。要件整理の段階からでも、お気軽にご相談ください。


参考(公式ドキュメント)

  • pgvector(GitHub・README)vector / halfvec / bit / sparsevec 型と次元上限・距離演算子・HNSW / IVFFlat の作成構文とパラメータ(m / ef_construction / hnsw.ef_search / lists / ivfflat.probes)・反復スキャンhnsw.iterative_scan / max_scan_tuples / scan_mem_multiplier / ivfflat.max_probes)・量子化halfvec 式インデックス / binary_quantize + bit_hamming_ops / subvector)・フィルタと過剰フィルタ・構築高速化(maintenance_work_mem / max_parallel_maintenance_workers)・pg_stat_progress_create_index・VACUUM/REINDEX
  • pgvector CHANGELOG — 反復スキャン(0.8.0)、hamming_distance / jaccard_distance と L1 の HNSW 対応(0.7.0)など、バージョンごとの追加機能
  • OpenAI Embeddings ガイドtext-embedding-3-large / -smalldimensions パラメータによる次元短縮(Matryoshka)・subvector 再ランクの前提
友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事で解説した技術の適用事例

生成AI音声チャットボット(PostgreSQL + pgvector に業務データと埋め込みを集約したRAG接客システム)

ケーススタディを見る