pgvectorの紹介

pgvector(pgvector/pgvector: Open-source vector similarity search for Postgres)はベクトルデータの類似性検索機能を提供するPostgreSQLの拡張機能です。データベース内にベクトルデータを格納し、特定のベクトルと最も近い要素を検索できます。近似値の算出はユークリッド距離、内積、コサイン距離による距離計算が提供されており、大規模なデータセットでの検索を高速化するためのインデックス機能も提供されています。レコメンデーションシステムや画像や文章の類似コンテンツ検索などのアプリケーション開発で有用になります。

pgvectorのインストールとセットアップ

PostgreSQLにpgvectorをインストールします。OSはRocky Linux release 9.6、PostgreSQLは公式リポジトリからインストールしたバージョン16.1を使います。PostgreSQLを動作させるPATH等の環境変数は設定済みで、すでにPostgreSQLインスタンスは起動済みとします。

pgvectorはPostgreSQLの公式リポジトリ経由でPostgreSQL 16用のpgvector_16をパッケージからインストールします。

# dnf install pgvector_16

続けて、testデータベースにpgvectorを登録します。

$ psql -U postgres -d test
psql (16.1)
Type "help" for help.

test=# CREATE EXTENSION vector;
CREATE EXTENSION
test=# \dx
                             List of installed extensions
  Name   | Version |   Schema   |                     Description
---------+---------+------------+------------------------------------------------------
 plpgsql | 1.0     | pg_catalog | PL/pgSQL procedural language
 vector  | 0.5.1   | public     | vector data type and ivfflat and hnsw access methods

ベクトル列の作成とベクトルデータの挿入

ベクトルデータを格納する列を作成します。pgvectorを登録すると、vector型が使えます。

test=# CREATE TABLE items (id bigserial PRIMARY KEY, embedding vector(3));

vector(3)は3次元のベクトル、つまり、ベクトル空間を形成する基底ベクトルの個数が3つであることを意味します。ここでは3次元に指定していますが、最大で16000次元まで指定できます。
異なる次元のベクトルを同じ列に保存したい場合は(vector(3)ではなく)vectorを使用できます。ただしその場合、式に対するインデックス部分インデックスを使って同じ次元数の行にしかインデックスを作成できません。

また、vector型の各要素はPostgreSQLのreal型と同様の単精度浮動小数点数で格納されます。より高精度のベクトルデータ格納したい場合は、vector型ではなく、double precision[]やnumeric[]型が使えます。ただし、高精度の値を格納はできますが、pgvectorが提供する機能を使う場合はvector型にキャストする必要があり、格納したデータの検索時に精度値が喪失します。

test=# CREATE TABLE items2 (id bigserial PRIMARY KEY, embedding double precision[]);

データがvector型に変換可能であり、かつ、想定の次元数であることを保証するためにチェック制約を追加してもよいでしょう。

test=# ALTER TABLE items2 ADD CHECK (vector_dims(embedding::vector) = 3);

次に、作成した列にベクトルデータを挿入します。vector型は以下のようにARRAYキーワードを使った配列や、[要素1,要素2,…]といった典型的な配列を表す文字列定数を解釈できます。
なお、vector型ではなく、double precision[]やnumeric[]型を定義した場合は、ベクトルデータをPostgreSQLの配列リテラル定数で格納できます。

test=# INSERT INTO items (embedding)
SELECT
  ARRAY[
  random() * 1000, -- 最初の次元のランダム値
  random() * 1000, -- 二番目の次元のランダム値
  random() * 1000  -- 三番目の次元のランダム値
]
FROM
  generate_series(1 , 10000);

itemsテーブルに、乱数を使った3次元のベクトルデータを10000行挿入しました。

test=# SELECT * FROM items LIMIT 5;
 id |            embedding
----+---------------------------------
  1 | [747.1946,30.812382,226.11743]
  2 | [389.10852,188.70128,151.08716]
  3 | [176.6698,954.8121,233.49654]
  4 | [295.3235,539.09283,721.9699]
  5 | [786.6494,895.81323,160.88304]
(5 rows)

最近傍検索の実行

最近傍検索を実行して、特定のベクトルに最も近いデータのトップ3を検索します。

test=# SELECT * FROM items ORDER BY embedding <#> '[10.0, 2, 0.3]' LIMIT 3;
  id  |            embedding
------+---------------------------------
 7945 | [998.16565,992.58215,926.16675]
 4633 | [996.8208,985.77405,989.8139]
 3345 | [997.30383,966.3805,949.19257]
(3 rows)

<#>演算子はベクトル間の負の内積を表す演算子です。似ているベクトルデータほど小さい数値となります。これは<#>演算子が昇順のインデックススキャンのみに対応しているためです。昇順ソートで最近似のベクトルデータをインデックスを効かせて取り出せます。

提供される演算子と関数

pgvectorは2つのベクトルの乗算、減算、乗算、ユークリッド距離、内積、コサイン距離等の演算子が用意されています。

演算子 説明
+ 2つのベクトルの各要素同士を加算します
2つのベクトルの各要素同士を減算します
* 2つのベクトルの各要素同士を乗算します
<-> 2つのベクトルのユークリッド距離を計測します
<#> 2つのベクトルの内積に-1を乗算します
<=> 2つのベクトルのコサイン距離を計測します

同様の機能を持った関数もあります。

関数 説明
cosine_distance(vector, vector) → double precision コサイン距離
inner_product(vector, vector) → double precision 内積
l2_distance(vector, vector) → double precision ユークリッド距離
l1_distance(vector, vector) → double precision マンハッタン距離
vector_dims(vector) → integer 次元数
vector_norm(vector) → double precision ユークリッドノルム

また、avg(vector) および sum(vector) でベクトルデータの平均と合計を算出する集約関数もあります。

ベクトルデータのインデックス

デフォルトのpgvectorは最近傍検索を行うので、リコール率(近傍アイテムを取りこぼさない確率)は100%ですが、線形探索のために速度が問題になる場合があります。
そのため、pgvectorには近似最近傍検索を用いた、IVFFlatとHNSWの2種類のインデックスが追加できます。ただし、正確な最近傍検索でないので高速にデータを取得できますが、実行時間・メモリ・精度のトレードオフが発生します。

インデックス 説明 インデックス構築時間 メモリ使用量 クエリのパフォーマンス
IVFFlat ベクトルをリストに分割してクエリベクトルに最も近いリストのサブセットを検索する 短い 少ない 速度とリコールのトレードオフが大きい
HNSW 最下層に全てのベクトル点が含まれ上の層になるほどベクトル点が少なくなる層状のグラフ構造で上層から下層にアイテムを辿っていく 長い 多い 速度とリコールのトレードオフが少ない

IVFFlatはHNSWよりもインデックス構築時間は短く、メモリ使用量は少ないですが、ベクトルデータがある程度テーブルに格納されていないとインデックスが構築できません。また、速度とリコールのトレードオフが大きく、速度を重視するとリコールが下がりやすくなり、データの取りこぼしが発生しやすくなります。
一方、HNSWはIVFFlatよりもインデックス構築時間は長く、メモリ使用量は多くなりますが、テーブルにトレーニングデータを格納しておく必要はありません。また、速度とリコールのトレードオフが少なく、速度を重視してもリコール率の低下が見られず、両者のバランスが取れています。

また、2000次元以上のベクトルをインデックス化することはpgvector 0.5.1時点ではできません。ベクトルデータを次元削減して対応する必要があります。

ここではHNSWインデックスを構築します。

HNSWインデックス

インデックスの構築はCREATE INDEX文で行います。
利用できる演算子クラスは以下です。

演算子クラス 説明 vector_cosine_ops コサイン距離 vector_ip_ops 内積 vector_l2_ops ユークリッド距離

たとえば、以下でitemsテーブルのembedding列にコサイン距離に基づくHNSWインデックスを構築します。

test=# CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);

HNSWインデックのスオプション

CREATE INDEX文にWITH句を加えてHNSWインデクスのインデックスパラメータを指定できます。

パラメータ 説明 m 層ごとの最大接続数(デフォルトは16) ef_construction グラフ構築用の動的候補リストのサイズ(デフォルトは64)
test=# CREATE INDEX ON items USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);

m、ef_constructionの値が高いほど、インデックスの構築時間、挿入速度を犠牲にしてリコールが向上します。また、ef_constructionはm*2未満だとエラーとなります。

HNSWインデックのクエリオプション

SET文で検索用の動的候補リストのサイズを指定します。デフォルトは40です。

SET hnsw.ef_search = 100;

高い値はリコールを向上させますが、速度を犠牲にします。

トランザクション内でSET LOCALを使用して、単一のクエリに設定します。

test=# BEGIN;
test=# SET LOCAL hnsw.ef_search = 100;
test=# SELECT ...
test=# COMMIT;

HNSWインデックのス作成進行状況

PostgreSQL 12以上であればインデックス作成の進行状況を、たとえば以下のSQL文で、インデックスを作成中のバックエンド毎に確認できます。

test=# SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;

HNSWインデックスの場合、pg_stat_progress_create_indexのphase列はinitializing(初期化中)またはloading tuples(タプルの読み込み中)が表示されます。

終わりに

pgvectorは、PostgreSQLの拡張機能としてベクトルデータの効率的な管理と類似性検索を可能にします。この記事では、pgvectorのインストールから設定、ベクトルデータの操作方法、最近傍検索の実行、さらにはインデックスの構築と最適化について解説しました。
pgvectorは近年目覚ましい技術発展を遂げている、大規模言語モデルによる文書生成にも利用可能です。大量のテキストデータに含まれるパターンや構造をベクトルデータとして抽出し、それを効率的に検索・分析することで文書の自動生成において重要な役割を果たします。
データベース技術やAI分野に関心がある方は、ぜひpgvectorを使ってみてください