【PostgreSQL】pgvectorでベクトル検索・類似検索を試す

PostgreSQL

こんにちは、かつコーチです。

前回はJSONB型とGINインデックスで半構造化データを扱う方法を解説しました。

今回は「データ型・JSON/pgvector」編の最終回として、pgvector拡張機能によるベクトル検索を扱います。

上級者向けの内容なので、embeddingやコサイン類似度といった概念は前提知識として説明を省略し、PostgreSQL上での実装・運用に焦点を当てます。

Pinecone・Weaviateといった専用のベクトルDBを別途構築せずに、既存のPostgreSQLインフラにベクトル検索機能を組み込みたいケースを想定して解説します。

pgvectorのセットアップと選択肢比較

拡張機能の有効化

pgvectorはPostgreSQLの拡張機能として提供されているため、まずは有効化が必要です。

CREATE EXTENSION IF NOT EXISTS vector;

Dockerで検証する場合は、公式のpgvector/pgvector:pg16イメージを使うと拡張機能のビルド作業を省略できます。

services:
  db:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: apppassword
      POSTGRES_DB: appdb
    ports:
      - "5432:5432"

専用ベクトルDBとpgvector、どちらを選ぶか

項目専用ベクトルDB(Pinecone等)pgvector
インフラ構成別サービスとして構築・運用が必要既存のPostgreSQLに同居できる
リレーショナルデータとの結合アプリ側で別々に取得して突き合わせるSQLのJOINでそのまま結合できる
大規模スケール時の性能専用設計のため優位な場合が多い規模によってはチューニングが必要
運用コストサービス利用料・別途運用工数既存DBの運用に相乗り
  • 既存のリレーショナルデータとベクトル検索結果を1つのクエリで結合したい、運用インフラを増やしたくない → pgvector
  • 数億件規模のベクトルを扱う、検索レイテンシを極限まで詰めたい → 専用ベクトルDB

「ECサイトの商品データに類似商品検索を追加する」程度の規模であれば、私は基本的にpgvectorを第一候補にしています。

商品テーブルとの結合が1クエリで完結する開発体験のメリットが大きいためです。

ベクトル型カラムの設計と類似検索

テーブル定義とデータ登録

CREATE TABLE articles (
    id SERIAL PRIMARY KEY,
    title TEXT NOT NULL,
    body TEXT NOT NULL,
    embedding VECTOR(1536)  -- OpenAIのtext-embedding-3-small相当の次元数
);

INSERT INTO articles (title, body, embedding)
VALUES (
    'PostgreSQL入門',
    'PostgreSQLは拡張性の高いRDBMSです。',
    '[0.012, -0.034, 0.081, ...]'  -- 実際はembedding APIから取得した1536次元の配列
);

VECTOR(1536)のように次元数を明示して定義します。

次元数はembeddingモデルによって決まるため、使用するモデルの仕様に合わせてカラム定義を変更してください。

距離関数の選択

pgvectorは複数の距離関数(演算子)をサポートしています。

-- <-> : ユークリッド距離
-- <#> : 負の内積(コサイン類似度と相関)
-- <=> : コサイン距離(1 - コサイン類似度)

SELECT title, embedding <=> '[0.01, -0.03, 0.08, ...]' AS distance
FROM articles
ORDER BY distance
LIMIT 5;

一般的なテキストembedding(OpenAI等)ではコサイン距離(<=>)が使われることが多く、値が小さいほど類似度が高いことを示します。

ORDER BY distance LIMIT Nという形が、いわゆるk近傍検索(k-NN)の基本パターンです。

ANNインデックスで検索を高速化する

インデックスなしでの限界

pgvectorはインデックスなしでも動作しますが、その場合は全行に対して距離計算を行う厳密検索(Exact Nearest Neighbor)になります。

数百件程度なら問題ありませんが、件数が数万〜数十万件を超えると、クエリのたびに全件の距離計算が発生し、レスポンスが著しく悪化します。

IVFFlatとHNSW、どちらを選ぶか

pgvectorには近似最近傍探索(ANN)用のインデックスとしてIVFFlatHNSWが用意されています。

項目IVFFlatHNSW
インデックス構築速度速い遅い(データ量が多いと顕著)
検索速度HNSWよりやや遅い速い
メモリ使用量少なめ多め
データ追加時の挙動クラスタ再構築が推奨される場面がある追加データにも比較的頑健
事前準備ある程度のデータ件数が必要データが少なくても構築可能
  • 検索速度を最優先し、メモリに余裕がある → HNSW
  • インデックス構築を高速に済ませたい、メモリ制約がある → IVFFlat
  • 迷ったら → pgvectorの公式ドキュメントでもHNSWが概ね推奨されており、特別な制約がなければHNSWを選んでおくのが無難
-- HNSWインデックスの作成(コサイン距離用)
CREATE INDEX idx_articles_embedding_hnsw
ON articles USING hnsw (embedding vector_cosine_ops);

vector_cosine_opsのように、使用する距離関数に対応した演算子クラスを指定する必要がある点に注意してください。

<->(ユークリッド距離)を使う場合はvector_l2_opsを指定します。

つまずきやすいポイント:次元数の不一致

embeddingモデルを変更した際の実体験

❌ Before:既存のVECTOR(1536)カラムに、次元数の異なるembeddingをそのまま挿入しようとする

-- text-embedding-3-largeなど、次元数が異なるモデルに切り替えた場合
INSERT INTO articles (title, body, embedding)
VALUES ('新規記事', '本文...', '[0.01, -0.02, ...]');  -- 3072次元のデータ
-- ERROR: expected 1536 dimensions, not 3072

私が実際にembeddingモデルをtext-embedding-3-smallからtext-embedding-3-largeへ切り替える検証を行った際、この次元数不一致エラーに遭遇しました。

VECTOR型は次元数を固定長で管理しているため、モデルを変更すると既存のカラム定義との間で不整合が起きます。

✅ After:新しいカラムを追加し、移行期間を設けてから切り替える

-- 新しい次元数用のカラムを追加
ALTER TABLE articles ADD COLUMN embedding_v2 VECTOR(3072);

-- 既存データを新しいembeddingで再計算し、embedding_v2に投入する
-- (アプリケーション側でembedding APIを呼び出してUPDATEする処理を実行)

-- 移行完了後、古いカラムとインデックスを削除する
-- ALTER TABLE articles DROP COLUMN embedding;

embeddingモデルのバージョンアップは今後も起こりうる前提で、カラムを使い回すのではなく、新カラムを追加して段階的に移行する設計にしておくと、本番運用での事故を防げます。

まとめ

この記事のポイント

  • pgvectorは既存のPostgreSQLにベクトル検索機能を組み込める拡張機能で、リレーショナルデータとの結合に強みがある
  • <->(ユークリッド距離)、<#>(負の内積)、<=>(コサイン距離)を用途に応じて使い分ける
  • データ件数が増えたらHNSW(基本推奨)またはIVFFlatのANNインデックスを構築する
  • embeddingモデルを変更する際は次元数の不一致に注意し、カラムを新設して段階移行する

次に読むべき記事

これで「データ型・JSON/pgvector」編は完結です。

次のカテゴリでは、CTEやウィンドウ関数といったPostgreSQLのクエリ応用テクニックを扱っていきます。

→ 次の記事:CTE(WITH句)の基本:クエリを読みやすく整理する

タグ: PostgreSQL, 上級者向け, データ型

タイトルとURLをコピーしました