こんにちは、かつコーチです。
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型を使うテーブル作成より必ず先に実行する - インデックスは
ivfflatかhnswをデータ量と更新頻度で使い分ける - マネージドDBを使う場合は事前にpgvector対応バージョンを確認する
次に読むべき記事
- 次回:RAGパイプラインを実装する(チャンク分割〜検索〜生成)
コメント