- 技術 💻
pgvectorの使い方|PostgreSQLでベクトル検索をコピペで構築
pgvectorとは、PostgreSQLに「vector型」と「距離で並べ替える演算子・インデックス」を追加し、いつものSQLで類似検索(ベクトル検索)ができるようにする拡張機能です。 専用のベクトルデータベースを別に立てなくても、既存のテーブルに列を1つ足すだけで「意味が近い文章」「似ている商品」を探せます。
この記事では、CREATE EXTENSION → vector列 → INSERT → 距離演算子 → インデックス → Pythonからの登録と検索までを、すべて手元で実行した結果つきで紹介します。さらに、10万件のベクトルで「インデックスなし/IVFFlat/HNSW」の作成時間・サイズ・検索時間・再現率を実測し、ef_search・lists・probes をどう決めればよいかを表にしました。フィルタ(WHERE)を付けたときに件数が減る現象も、実際の数値で確認しています。
検証環境:PostgreSQL 16.13 + pgvector 0.6.0(Ubuntu の postgresql-16-pgvector パッケージ)、Python 3.11、psycopg 3.3.6、numpy 2.4.4、pgvector(Pythonパッケージ)0.5.0。2026年10月時点の最新版は pgvector 0.8.6(公式README)です。0.7以降・0.8以降でしか使えない機能は、その旨を明記し「未実行」と書いています。
pgvectorの使い方 早見表
| やりたいこと | SQL / 設定 | 補足 |
|---|---|---|
| 有効化 | CREATE EXTENSION vector; |
DBごとに1回。拡張名は vector |
| 列を作る | embedding vector(1536) |
次元数はモデルに合わせる。最大16,000次元 |
| 登録 | INSERT ... VALUES ('[0.1,0.2,0.3]') |
文字列 '[...]' で渡せる |
| コサイン距離で近い順 | ORDER BY embedding <=> '[...]' LIMIT 10 |
文章の埋め込みで最もよく使う |
| L2(ユークリッド)距離 | <-> |
画像特徴量などで使う |
| 内積(符号反転) | <#> |
値が小さいほど近い。正規化済みなら最速 |
| L1距離 | <+> |
0.7.0以降(0.6.0ではエラー) |
| 類似度(0〜1)にする | 1 - (embedding <=> '[...]') |
コサイン類似度 |
| HNSWインデックス | CREATE INDEX ON t USING hnsw (embedding vector_cosine_ops); |
速くて再現率が高い。作成は遅め |
| IVFFlatインデックス | USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100) |
作成が速い。データ投入後に作る |
| HNSWの精度を上げる | SET hnsw.ef_search = 100; |
既定40。上げると再現率↑・速度↓ |
| IVFFlatの精度を上げる | SET ivfflat.probes = 10; |
既定1。目安は sqrt(lists) |
| インデックス作成を速く | SET maintenance_work_mem = '1GB'; |
メモリの範囲内で |
| フィルタ付きで件数を確保 | 部分インデックス/ef_searchを上げる/0.8の hnsw.iterative_scan |
後述 |
| 2,000次元を超える列にインデックス | halfvec へキャスト(0.7.0以降) |
vectorのHNSW/IVFFlatは2,000次元まで |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「pgvectorの使い方|PostgreSQLでベクトル検索をコピペで構築」
pgvectorでできること・バージョンごとの違い
pgvectorは「埋め込みベクトル(数値の並び)」を保存し、距離が近い順に取り出すための拡張です。文章や画像をAIモデルで数値ベクトルに変換しておけば、「キーワードが一致しなくても意味が近いもの」を検索できます。
| バージョン | 主な追加機能(公式CHANGELOG/READMEより) |
|---|---|
| 0.5.0 | HNSWインデックスの追加 |
| 0.6.0 | HNSWの並列作成で作成時間を短縮 |
| 0.7.0 | halfvec(半精度)・sparsevec(疎ベクトル)・bit 型、<+>(L1)、l2_normalize() など |
| 0.8.0 | 反復インデックススキャン(hnsw.iterative_scan / ivfflat.iterative_scan)でフィルタ付き検索の取りこぼしを補える |
| 0.8.6 | 2026年10月時点の最新(README記載) |
手元のバージョンは SELECT extversion FROM pg_extension WHERE extname = 'vector'; で確認できます。Ubuntu 24.04 の標準パッケージで入る版は 0.6.0 でした。
SELECT extversion FROM pg_extension WHERE extname = 'vector';
extversion
------------
0.6.0
(1 row)
型と次元の上限
| 型 | 1要素 | 保存できる最大次元 | インデックスできる最大次元 | 対応版 |
|---|---|---|---|---|
vector |
4バイト浮動小数 | 16,000 | 2,000 | 0.1〜 |
halfvec |
2バイト浮動小数 | 16,000 | 4,000 | 0.7〜 |
bit |
1ビット | 64,000 | 64,000 | 0.7〜 |
sparsevec |
非ゼロ要素のみ | 16,000(非ゼロ1,000まで) | 1,000(非ゼロ) | 0.7〜 |
インストールと有効化の手順
pgvectorはPostgreSQL本体とは別のパッケージです。環境に合わせて入れてから、使うデータベースで CREATE EXTENSION します。
| 環境 | 入れ方 |
|---|---|
| Ubuntu / Debian | apt-get install -y postgresql-16-pgvector(数字はPostgreSQLの版。PGDGリポジトリなら新しい版) |
| macOS | brew install pgvector |
| ソース | git clone --branch v0.8.6 https://github.com/pgvector/pgvector.git → make && make install |
| Docker/マネージドDB | pgvector/pgvector イメージ、または多くのクラウドで CREATE EXTENSION vector; だけで使える |
sudo apt-get install -y postgresql-16-pgvector
sudo -u postgres psql -d mydb -c 'CREATE EXTENSION IF NOT EXISTS vector;'
拡張を有効にしていないと type "vector" does not exist になります。 拡張はデータベース単位なので、別のDBでは改めて実行します。
ERROR: type "vector" does not exist
LINE 5: embedding vector(3)
テーブル作成からベクトル検索までをSQLで試す
まずは3次元の小さな例で、列の作り方・登録・検索の流れをつかみます(実際の埋め込みは数百〜数千次元です)。
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE items (
id bigserial PRIMARY KEY,
name text NOT NULL,
category text NOT NULL,
embedding vector(3)
);
INSERT INTO items (name, category, embedding) VALUES
('りんご', 'fruit', '[1, 0.9, 0.1]'),
('みかん', 'fruit', '[0.9, 1, 0.2]'),
('にんじん', 'vegetable', '[0.2, 0.3, 1]'),
('ボールペン','stationery','[-1, 0.1, 0.1]');
距離演算子の早見表と実行結果
| 演算子 | 意味 | 小さいほど | インデックス用の演算子クラス |
|---|---|---|---|
<-> |
L2距離(ユークリッド) | 近い | vector_l2_ops |
<#> |
内積 × -1 | 近い(内積が大きい) | vector_ip_ops |
<=> |
コサイン距離(1 - コサイン類似度) | 近い | vector_cosine_ops |
<+> |
L1距離(0.7.0〜) | 近い | vector_l1_ops |
<~> |
ハミング距離(bit型、0.7.0〜) | 近い | bit_hamming_ops |
<%> |
ジャッカード距離(bit型、0.7.0〜) | 近い | bit_jaccard_ops |
SELECT name,
round((embedding <-> '[1,1,0]')::numeric,4) AS l2,
round((embedding <#> '[1,1,0]')::numeric,4) AS neg_inner,
round((embedding <=> '[1,1,0]')::numeric,4) AS cosine_dist
FROM items ORDER BY embedding <=> '[1,1,0]' LIMIT 3;
name | l2 | neg_inner | cosine_dist
----------+--------+-----------+-------------
りんご | 0.1414 | -1.9000 | 0.0041
みかん | 0.2236 | -1.9000 | 0.0122
にんじん | 1.4595 | -0.5000 | 0.6674
(3 rows)
<#> は内積を -1 倍した値を返す点に注意します。PostgreSQLのインデックスは「小さい順(ASC)」でしか使えないため、内積が大きいほど値が小さくなるよう符号を反転しています。
類似度(0〜1)として表示する
SELECT name, round((1 - (embedding <=> '[1,1,0]'))::numeric, 4) AS cosine_similarity
FROM items ORDER BY embedding <=> '[1,1,0]' LIMIT 2;
name | cosine_similarity
--------+-------------------
りんご | 0.9959
みかん | 0.9878
(2 rows)
並べ替えは ORDER BY embedding <=> ... のまま、表示だけ 1 - 距離 にするのがポイントです。ORDER BY similarity DESC と書くとインデックスが使われません。
便利な関数・演算
SELECT vector_dims(embedding), vector_norm(embedding) FROM items WHERE id = 1;
SELECT category, avg(embedding) FROM items GROUP BY category ORDER BY category;
vector_dims | vector_norm
-------------+--------------------
3 | 1.3490737405281743
category | avg
------------+------------------
fruit | [0.95,0.95,0.15]
stationery | [-1,0.1,0.1]
vegetable | [0.2,0.3,1]
ほかに +・-・*(要素ごとの演算)、::real[] との相互変換も使えます。
avg(embedding) でカテゴリの「代表ベクトル」を作れるので、「このカテゴリに近い記事」などの検索にも応用できます。
インデックスの選び方:HNSWとIVFFlatを10万件で実測
インデックスなしの検索は全件と距離を計算する「厳密な検索(再現率100%)」です。件数が増えると遅くなるため、近似最近傍(ANN)インデックスのHNSWかIVFFlatを使います。近似なので「本当の上位10件」のうち何件を返せたか(再現率 recall@10)もあわせて見ます。
計測条件
| 項目 | 内容 |
|---|---|
| 件数・次元 | 100,000件 × 128次元、コサイン距離(vector_cosine_ops) |
| データA(クラスタあり) | 1,000個の中心のまわりに散らばるベクトル(実際の埋め込みに近い「似たもの同士が集まる」形) |
| データB(完全ランダム) | 正規分布の乱数そのまま(構造がない、ANNにとって最も難しい形) |
| クエリ | 100件。正解はnumpyで全件計算した上位10件 |
| 設定 | maintenance_work_mem = '1GB'、検索は並列なし、各設定で10回ウォームアップ |
| 環境 | 2コア / 7GB、PostgreSQL 16.13、pgvector 0.6.0。共有環境のため時間は±数十%ぶれます |
計測コードの要点
データの作成とCOPYでの投入を除いた、計測部分の中心は次のとおりです(全体で約60行)。
# pip install "psycopg[binary]" numpy pgvector
truth = np.argsort(-(qs @ X.T), axis=1)[:, :10] + 1 # numpyで全件計算した正解(id = 行番号 + 1)
def run(label):
times, rec = [], []
for qi in range(len(qs)):
t = time.perf_counter()
cur.execute("SELECT id FROM bench ORDER BY embedding <=> %s LIMIT 10", (qs[qi],))
ids = [r[0] for r in cur.fetchall()]
times.append(time.perf_counter() - t)
rec.append(len(set(ids) & set(truth[qi])) / 10)
print(f"{label}: avg {np.mean(times)*1000:.2f} ms, recall@10 {np.mean(rec):.3f}")
cur.execute("SET maintenance_work_mem = '1GB'")
cur.execute("CREATE INDEX ON bench USING ivfflat (embedding vector_cosine_ops) WITH (lists = 316)")
for p in (1, 5, 10, 20, 40):
cur.execute(f"SET ivfflat.probes = {p}"); run(f"IVFFlat probes={p}")
# IVFFlatを消してから
cur.execute("CREATE INDEX ON bench USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64)")
for ef in (10, 40, 100, 200):
cur.execute(f"SET hnsw.ef_search = {ef}"); run(f"HNSW ef_search={ef}")
lists = 316 は sqrt(100,000) です。READMEの目安(100万件までは 行数/1000)より大きめですが、probesの効果を見やすくするためにこの値にしました。
実測結果(検索時間は100クエリの平均)
| 方式・設定 | 作成時間 | サイズ | A:平均 | A:再現率 | B:平均 | B:再現率 |
|---|---|---|---|---|---|---|
| インデックスなし | ― | ―(表は56MB) | 29.22 ms | 1.000 | 72.27 ms | 1.000 |
| IVFFlat probes=1 | A 1.6秒 / B 6.4秒 | 53 MB | 0.29 ms | 0.997 | 1.40 ms | 0.040 |
| IVFFlat probes=5 | 同上 | 同上 | 0.75 ms | 1.000 | 1.92 ms | 0.129 |
| IVFFlat probes=10 | 同上 | 同上 | 1.27 ms | 1.000 | 3.09 ms | 0.194 |
| IVFFlat probes=20 | 同上 | 同上 | 2.40 ms | 1.000 | 4.41 ms | 0.316 |
| IVFFlat probes=40 | 同上 | 同上 | 4.71 ms | 1.000 | 13.78 ms | 0.481 |
| HNSW ef_search=10 | A 17.4秒 / B 85.1秒 | 79 MB | 0.54 ms | 0.893 | 0.88 ms | 0.046 |
| HNSW ef_search=40(既定) | 同上 | 同上 | 0.67 ms | 1.000 | 1.75 ms | 0.169 |
| HNSW ef_search=100 | 同上 | 同上 | 0.88 ms | 1.000 | 5.17 ms | 0.329 |
| HNSW ef_search=200 | 同上 | 同上 | 2.02 ms | 1.000 | 6.31 ms | 0.472 |
出力の一部(データA):
no index (seq scan): avg 29.22 ms, p95 37.59 ms, recall@10 1.000
IVFFlat build: 1.6s
ivfflat size 53 MB
HNSW build: 17.4s
hnsw size 79 MB
HNSW ef_search=10: avg 0.54 ms, p95 0.77 ms, recall@10 0.893
HNSW ef_search=40: avg 0.67 ms, p95 0.94 ms, recall@10 1.000
実測からわかること
- インデックスを使うと検索は約30〜100倍速くなりました(29 ms → 0.3〜0.9 ms)。
- HNSWは作成に時間がかかり(IVFFlatの約10倍)、サイズも大きい(79MB 対 53MB)一方、既定の
ef_search=40で再現率1.000でした。 ef_searchとprobesは「速度と再現率のつまみ」です。上げるほど再現率が上がり、検索は遅くなります。ef_search=10では再現率0.893まで下がりました。- データBのように構造のないベクトルでは、どちらも再現率が大きく下がります。 128次元の一様な乱数は「どの点もほぼ同じ距離」になり、近似検索が手がかりを持てないためです。実際の文章埋め込みはデータAのように「似たもの同士が集まる」ので、自分のデータで再現率を測ってから設定を決めるのが確実です。
- IVFFlatはデータAで特に有利でした。クラスタ(似たものの塊)がリストの分け方とよく合うためで、データの形で結果が変わります。
HNSWとIVFFlatの選び方
| 判断ポイント | HNSW | IVFFlat |
|---|---|---|
| 検索の速さと再現率のバランス | 高い(既定値で十分なことが多い) | probes次第 |
| インデックス作成時間 | 長い | 短い |
| サイズ・メモリ | 大きい | 小さい |
| 空のテーブルに先に作れるか | 作れる(追加のたびにグラフを更新) | 作れるが、データ投入後に作る(中心点を学習するため) |
| データが大きく入れ替わる | そのまま追従しやすい | 分布が変わったら作り直しが必要 |
| 迷ったら | こちらを選ぶ | 作成時間・容量を抑えたいとき |
パラメータの決め方
| パラメータ | 既定 | 意味 | 決め方 |
|---|---|---|---|
m(HNSW) |
16 | 各ノードがつながる数 | 既定のまま。再現率が足りないとき24〜32 |
ef_construction(HNSW) |
64 | 作成時の候補数 | 既定のまま。上げると作成が遅く精度が上がる |
hnsw.ef_search |
40 | 検索時の候補数 | LIMITより大きく。再現率を測って40→100→200 |
lists(IVFFlat) |
100 | 分割するリスト数 | 100万件まで 行数/1000、それ以上 sqrt(行数) |
ivfflat.probes |
1 | 調べるリスト数 | sqrt(lists) から始める |
maintenance_work_mem |
64MB | 作成時に使うメモリ | 作成時だけ SET で上げる。グラフが収まらないと通知が出て遅くなる |
SET はセッション単位、SET LOCAL はトランザクション単位、ALTER DATABASE ... SET hnsw.ef_search = 100; でデータベース全体の既定値にできます。
インデックスが使われているか確認する
EXPLAIN SELECT id FROM bench ORDER BY embedding <=> '[...]' LIMIT 10;
Limit (cost=36.60..39.59 rows=10 width=16)
-> Index Scan using bench_hnsw on bench (cost=36.60..29958.60 rows=100000 width=16)
Order By: (embedding <=> '[...]'::vector)
同じテーブルで、コサイン用のインデックスに <->(L2)で検索すると、プランは Sort → Seq Scan on bench(全件走査)になりました。
インデックスが効く条件は「演算子クラスと演算子が一致」「ORDER BY 距離 ASC」「LIMIT あり」の3つです。
Python(psycopg + numpy)から登録・検索する
pgvector パッケージの register_vector() を呼ぶと、numpy配列とvector型が自動で変換されます。ここでは外部APIを使わず、文字2-gramをハッシュして数値にする簡易的な埋め込みで、登録から検索までを動かします。
# pip install "psycopg[binary]" numpy pgvector
import hashlib
import numpy as np
import psycopg
from pgvector.psycopg import register_vector
DIM = 256
def embed(text: str) -> np.ndarray:
"""外部API不要の簡易埋め込み:文字2-gramをハッシュしてDIM次元に集計し、正規化する"""
v = np.zeros(DIM, dtype=np.float32)
s = f" {text} "
for i in range(len(s) - 1):
h = int(hashlib.md5(s[i:i+2].encode()).hexdigest(), 16)
v[h % DIM] += 1.0
n = np.linalg.norm(v)
return v / n if n else v
docs = [
("在庫の引当処理を非同期にする手順", "inventory"),
("在庫数がマイナスになるときの確認方法", "inventory"),
("会員登録メールが届かないときの確認方法", "member"),
("会員のパスワード再設定の流れ", "member"),
("注文キャンセル時に在庫を戻す処理", "order"),
("注文履歴をCSVで出力する方法", "order"),
]
with psycopg.connect("host=127.0.0.1 dbname=t86 user=t86 password=t86", autocommit=True) as conn:
conn.execute("CREATE EXTENSION IF NOT EXISTS vector")
register_vector(conn) # numpy配列 <-> vector型 を自動変換
conn.execute("DROP TABLE IF EXISTS faq")
conn.execute(f"""CREATE TABLE faq (
id bigserial PRIMARY KEY,
body text NOT NULL,
category text NOT NULL,
embedding vector({DIM}) NOT NULL)""")
with conn.cursor() as cur:
cur.executemany(
"INSERT INTO faq (body, category, embedding) VALUES (%s, %s, %s)",
[(b, c, embed(b)) for b, c in docs],
)
conn.execute("CREATE INDEX ON faq USING hnsw (embedding vector_cosine_ops)")
q = "在庫がマイナスになった"
rows = conn.execute(
"""SELECT body, category, 1 - (embedding <=> %s) AS similarity
FROM faq ORDER BY embedding <=> %s LIMIT 3""",
(embed(q), embed(q)),
).fetchall()
print(f"query: {q}")
for body, cat, sim in rows:
print(f" {sim:.3f} [{cat}] {body}")
rows = conn.execute(
"""SELECT body FROM faq WHERE category = %s
ORDER BY embedding <=> %s LIMIT 2""",
("member", embed("メールが届かない")),
).fetchall()
print("filter member:", [r[0] for r in rows])
実行結果(Python 3.11、psycopg 3.3.6、pgvector 0.5.0):
query: 在庫がマイナスになった
0.557 [inventory] 在庫数がマイナスになるときの確認方法
0.123 [inventory] 在庫の引当処理を非同期にする手順
0.069 [member] 会員のパスワード再設定の流れ
filter member: ['会員登録メールが届かないときの確認方法', '会員のパスワード再設定の流れ']
大量登録は executemany より COPY ... FORMAT BINARY が速いです(上の計測コードでは10万件×128次元を0.4〜1.6秒で投入できました)。
実際の埋め込みモデルに差し替えるとき
上の embed() は文字の並びしか見ないため、「意味」は捉えられません。実際には次のようなモデルで作ったベクトルを入れます。
| 種類 | 例 | 次元数の例 | 特徴 |
|---|---|---|---|
| クラウドの埋め込みAPI | OpenAI text-embedding-3-small / -large など |
1536 / 3072 | 導入が簡単。通信と利用料がかかる |
| ローカルで動かすモデル | sentence-transformers の多言語モデル(multilingual-e5 系など) | 384〜1024 | 外部送信なし。CPU/GPUで計算 |
差し替えるときの注意は3つです。①列の次元数をモデルの出力に合わせる、②登録と検索で同じモデルを使う、③正規化済みベクトルなら <#>(内積)でも同じ順位になります。2,000次元を超える場合は、0.7.0以降なら halfvec へのキャストでインデックスを作れます(READMEの記載。0.6.0環境のため未実行)。
-- pgvector 0.7.0以降(未実行)
CREATE INDEX ON items USING hnsw ((embedding::halfvec(3072)) halfvec_cosine_ops);
検索結果を生成AIに渡して回答させる仕組み(RAG)は別記事「RAGの作り方」で扱っているので、ここではデータベース側の使い方に絞っています。
フィルタ付き検索の注意と対処
近似インデックスでは、WHERE句は「インデックスで候補を取り出したあと」に適用されます。 HNSWの候補数は ef_search(既定40)なので、条件に合う行が少ないと、LIMIT 10を指定しても10件に届かないことがあります。
全体の10%だけが category = 3 のデータで、上位10件を取り出した結果です(データA)。
SELECT id FROM bench WHERE category = 3 ORDER BY embedding <=> '[...]' LIMIT 10;
| 対処 | 返った件数(100クエリの平均) | 最少 |
|---|---|---|
HNSW、ef_search = 40(既定) |
4.2件 | 0件 |
HNSW、ef_search = 200 |
10.0件 | 9件 |
部分インデックス WHERE category = 3、ef_search = 40 |
10.0件 | 10件 |
filter category=3 (10%) ef_search=40: avg rows 4.2, min 0
filter category=3 (10%) ef_search=200: avg rows 10.0, min 9
partial index, ef_search=40: avg rows 10.0 min 10 avg 0.87 ms
部分インデックスは次のように作ります(作成後のサイズは8MB)。
CREATE INDEX bench_cat3 ON bench USING hnsw (embedding vector_cosine_ops) WHERE category = 3;
| 状況 | おすすめの対処 |
|---|---|
| 絞り込みの値が少数で固定(例:言語、公開状態) | 値ごとの部分インデックス |
| 絞り込みの値が多い/条件に合う行が少数 | パーティション分割、またはB-treeで絞ってから全件距離計算 |
| 0.8.0以降を使える | SET hnsw.iterative_scan = relaxed_order; で件数が揃うまで自動でスキャンを続ける |
| すぐ対応したい | ef_search を上げる(速度とのトレードオフ) |
pgvector 0.8.0以降の反復スキャンの設定(README記載。本記事の0.6.0環境では未実行):
SET hnsw.iterative_scan = relaxed_order; -- off / strict_order / relaxed_order
SET hnsw.max_scan_tuples = 20000; -- 既定 20,000。探索の上限
SET ivfflat.iterative_scan = relaxed_order;
SET ivfflat.max_probes = 100;
relaxed_order は順位がわずかに入れ替わることがあるため、厳密な順位が必要なら外側で距離順に並べ直します(README記載)。
よくあるエラーと対処
| エラー文(実際の出力) | 原因 | 対処 |
|---|---|---|
type "vector" does not exist |
拡張が未有効 | 対象DBで CREATE EXTENSION vector; |
expected 3 dimensions, not 2 |
列の次元数と違うベクトルを登録 | モデルの出力次元と列定義を揃える |
different vector dimensions 3 and 2 |
比較するベクトルの次元が違う | 検索ベクトルの次元を確認 |
column cannot have more than 2000 dimensions for hnsw index |
vector型のインデックス上限 | 0.7以降で halfvec キャスト、または次元削減 |
column does not have dimensions |
vector(次元なし)列にインデックス |
vector(1536) のように次元を指定 |
operator does not exist: vector <+> unknown/function l2_normalize(vector) does not exist/type "halfvec" does not exist |
0.7.0で追加された機能を0.6.0で使用 | 拡張を更新(ALTER EXTENSION vector UPDATE;) |
| 件数がLIMITより少ない | フィルタがインデックス後に適用 | 前章の対処表 |
| インデックスが使われない | 演算子クラスと演算子が不一致/DESCで並べている | EXPLAIN で確認し、演算子を揃える |
よくある質問
Q. HNSWとIVFFlat、まずどちらを選べばよいですか?
A. 迷ったらHNSWです。今回の実測でも既定の ef_search=40 で再現率1.000、検索は1ms未満でした。インデックスの作成時間やサイズを小さくしたい場合はIVFFlatを選び、probes を sqrt(lists) から調整します。
Q. 何件くらいからインデックスが必要ですか? A. 今回の環境では10万件×128次元の全件検索が約30〜70msでした。応答時間の目標に収まるうちはインデックスなしでも正確(再現率100%)です。件数や次元が増えて目標を超えたら、HNSWを追加します。
Q. 再現率はどうやって測ればよいですか?
A. 代表的なクエリを数十〜100件用意し、SET enable_indexscan = off;(または numpy の全件計算)で求めた正解の上位10件と、インデックス使用時の結果の重なりを数えます。本記事の計測コードの run() がその例です。
Q. 0.6.0から0.8系へ上げるには?
A. 新しいパッケージを入れたあと、各DBで ALTER EXTENSION vector UPDATE; を実行します。版によってはインデックスの作り直しが推奨される場合があるため、公式のCHANGELOGを確認してから行います。
まとめ
- pgvectorはPostgreSQLにvector型と距離演算子を追加する拡張で、
CREATE EXTENSION vector;から使える - コサイン距離
<=>でORDER BY ... LIMITが基本形。類似度は1 - 距離で表示する - 10万件の実測でインデックスは検索を約30〜100倍速くした。HNSWは作成が遅く大きいが、既定値で再現率1.000
ef_search/probesは速度と再現率のつまみ。自分のデータで再現率を測って決める- フィルタ付き検索は件数が減ることがある。部分インデックス・
ef_search・0.8の反復スキャンで対処 - Pythonは
register_vector()でnumpy配列をそのまま渡せる。大量登録はCOPYが速い - 0.7以降の機能(halfvec、
<+>など)は版を確認してから使う
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「pgvectorの使い方|PostgreSQLでベクトル検索をコピペで構築」
株式会社RJCでは、PostgreSQLとベクトル検索の使い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。