【Laravel】pgvectorでベクトル検索の環境を構築する

laravelアイキャッチ Laravel

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

RAG(RAG:AI自身の知識ではなく、検索した文書を渡して回答させる仕組み。カンニングペーパーを渡してから質問に答えてもらうイメージです)を実装するには、まずベクトル検索の基盤を作る必要があります。

今回はPostgreSQLの拡張機能であるpgvectorを使い、Laravelプロジェクトにベクトル検索環境を構築する手順を解説します。

pgvectorとは?

pgvectorは、PostgreSQLにベクトル型(数値の配列としてテキストの意味を表現するデータ型)を追加する拡張機能です。

vector型のカラムを持たせることで、文章をembedding(embedding:テキストの意味を数値ベクトルに変換したもの)に変換し、DBに直接保存できます。

検索時は<=>演算子でコサイン距離を計算し、値が小さいほど意味的に近い文書として扱えます。

追加のベクトルDB(Pinecone、Qdrantなど)を用意しなくても、使い慣れたPostgreSQLだけでRAGの土台が作れるのが最大のメリットです。

環境構築の手順

PostgreSQLにpgvector拡張をインストールする

Docker環境なら、pgvector公式イメージを使うのが最も手早い方法です。

# docker-compose.yml
services:
  db:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_DB: laravel_ai
      POSTGRES_USER: sail
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Homebrewでローカルに入れる場合は、以下のコマンドです。

brew install pgvector

拡張機能を有効化する

コンテナを起動したら、DBに接続して拡張を有効化します。

CREATE EXTENSION IF NOT EXISTS vector;

Laravelのマイグレーションから実行する形が、チーム開発では管理しやすいです。

<?php

use Illuminate\Database\Migrations\Migration;

return new class extends Migration
{
    public function up(): void
    {
        DB::statement('CREATE EXTENSION IF NOT EXISTS vector');
    }

    public function down(): void
    {
        DB::statement('DROP EXTENSION IF EXISTS vector');
    }
};

vector型カラムを持つテーブルを作成する

vector型は標準のLaravelマイグレーションビルダーにはないため、生のSQLで定義します。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('document_chunks', function (Blueprint $table) {
            $table->id();
            $table->foreignId('document_id')->constrained()->cascadeOnDelete();
            $table->text('content');
            $table->unsignedInteger('token_count')->default(0);
            $table->timestamps();
        });

        // embedding列はvector型で追加(次元数はOpenAIのtext-embedding-3-smallに合わせて1536)
        DB::statement('ALTER TABLE document_chunks ADD COLUMN embedding vector(1536)');

        // コサイン距離検索を高速化するインデックス
        DB::statement('CREATE INDEX document_chunks_embedding_idx ON document_chunks USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100)');
    }

    public function down(): void
    {
        Schema::dropIfExists('document_chunks');
    }
};

ivfflatインデックスは近似最近傍探索用で、データ件数が少ないうちは効果を実感しにくいですが、数万件を超えたあたりから検索速度に差が出てきます。

よくあるつまずきポイント・エラー対処

実際に私がこの環境構築でハマったのが、拡張機能の有効化タイミングです。

最初、通常のマイグレーションと同じ感覚でvector型カラムのマイグレーションを先に流したところ、次のようなエラーが出ました。

SQLSTATE[42704]: Undefined object: 7 ERROR:
type "vector" does not exist
LINE 1: ALTER TABLE document_chunks ADD COLUMN embedding vector(15...

原因は単純で、CREATE EXTENSION vectorを実行する前に、vector型を使うマイグレーションを実行してしまっていたことでした。

Laravelのマイグレーションはファイル名の日付順に実行されるため、拡張機能を有効化するマイグレーションのファイル名が、テーブル作成のマイグレーションより後の日時になっていたのが原因です。

  • ❌Before:拡張有効化マイグレーションを後から作成し、テーブル作成マイグレーションより日時が新しくなっていた
  • ✅After:拡張有効化専用のマイグレーションを最初に作り直し、ファイル名の日時をテーブル作成より確実に早くした

さらに、本番環境で使うマネージドPostgreSQL(RDSなど)によっては、pgvector拡張が使えるバージョンとそうでないバージョンがある点も要注意です。

事前にインフラ担当と拡張の対応バージョンを確認しておくと、本番デプロイ直前で慌てずに済みます。

応用・一歩先の使い方

インデックス方式の使い分け

ivfflatのほかに、pgvector 0.5以降ではhnswインデックスも使えます。

hnswは構築に時間がかかる代わりに検索精度・速度のバランスが良く、データ更新頻度が低いRAGコレクションに向いています。

CREATE INDEX document_chunks_embedding_hnsw_idx
ON document_chunks USING hnsw (embedding vector_cosine_ops);

Eloquentモデルでの扱い方

vector型はEloquentが標準サポートしていないため、キャストを自作するか、生のSQLで挿入・検索する形になります。

DB::table('document_chunks')->insert([
    'document_id' => $documentId,
    'content' => $chunkText,
    'embedding' => DB::raw("'[" . implode(',', $embeddingArray) . "]'::vector"),
]);

次回の記事で、このテーブルを使った検索クエリの実装まで踏み込みます。

まとめ

この記事のポイント

  • pgvectorはPostgreSQLにvector型とコサイン距離演算子<=>を追加する拡張機能
  • 拡張機能の有効化マイグレーションは、vector型を使うテーブル作成より必ず先に実行する
  • インデックスはivfflathnswをデータ量と更新頻度で使い分ける
  • マネージドDBを使う場合は事前にpgvector対応バージョンを確認する

次に読むべき記事

  • 次回:RAGパイプラインを実装する(チャンク分割〜検索〜生成)

コメント

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