Heroku Advent Calendar 2023 24日目の記事です🎄
はじめに
はいどうもー!リバネス開発チームのトミー(@tomyf)です٩( 'ω' )و
今回は、最近発表された Heroku Postgres 15 の pgvector の機能を使う機会があったので、初めて知ったことや気がついたことを記事にしたいと思います。
最近、弊社の Salesforce に蓄積されている某イベントの申請書群を対象に、検索できるようにしたいという要望があり、せっかくなので pgvector を試してみようとなったのがきっかけです。
ベクトル検索とは?
当初、私は
(´・ω・)?「pgvector ってなんぞ?」 → ベクトル検索ができるようになるらしい
(´・ω・)?「ベクトル検索ってなんぞ?」 → データを多次元空間の空間で表して、検索ワードから距離の近いデータを検索するものらしい
( ^ω^ )「ほ〜ん、vector型のカラムにしてWhere文で比較すれば良いのかな?」
っと言った感じでした。
実際は、AIの力でデータをベクトル表現に変換する必要があり、その工程をエンべディングと呼ばれる事を知りました。
つまり、ベクトル変換を利用するためには事前にすべてのデータをエンべディング(=ベクトル化)する必要があります。
今回、エンべディングには OpenAI API の Embeddings API を利用しました。
この Embeddings API はデータを1536次元のベクトルに変換してくれます。
Heroku Connect で同期したテーブルは vector 型を使えない!?
こちらの記事から抜粋です。
https://blog.heroku.com/pgvector-launch
Search Salesforce Data: Use Heroku Connect to synchronize Salesforce data into Heroku, then create a new table with the embeddings since Heroku Connect can’t synchronize vector data types. This unlocks a whole new possibility to extend Salesforce like searching for similar support cases with embeddings from Service Cloud cases.
こちらを要約するとこうなります。
Heroku Connect を使用して、ベクトルデータ型を同期することができないため、エンベディングを使用して新しいテーブルを作成する必要があります。これにより、エンベディングを使用して類似のサポートケースを検索するなど、機能を拡張することができます。
つまり、 Heroku Connect で同期したテーブルの sfid
を外部キーとするテーブルを作成してから、そちらにベクトル型のカラムを追加してね。っということでした。
Heroku Posgres のバージョンを15にアップグレードする
pgvector に対応したのは Heroku Postgres 15 なので、まずはバージョンをアップグレードする必要があります。
弊社では Heroku Connect を使用しているので、こちらの記事を参考に進めていきました。
https://devcenter.heroku.com/ja/articles/heroku-connect-maintenance-operations#upgrading-the-version-of-a-heroku-postgres-database
以下の流れで、バージョンアップを行います。
-
フォロワーデータベースをプロビジョニングする
heroku addons:create heroku-postgresql:standard-2 --follow HEROKU_POSTGRESQL_{カラー}_URL --app {アプリ名}
-
アプリをメンテナンスモードにする
-
Heroku Connect を停止する
-
フォロワーデータベースが追いついたか確認する
heroku pg:info --app {アプリ名}
Behind By: 0 commits
なら追いついている -
フォロワデータベースの PostgreSQL バージョンをアップグレードする
heroku pg:upgrade HEROKU_POSTGRESQL_{新しく作られたカラー}_URL --app {アプリ名}
heroku pg:wait HEROKU_POSTGRESQL_{新しく作られたカラー}_URL --app {アプリ名}
で進行状況を監視できる -
バージョンアップしたフォロワデータベースをプライマリデータベースにする
heroku pg:promote HEROKU_POSTGRESQL_{新しく作られたカラー}_URL --app {アプリ名}
pgvector を有効にする
Heroku Postgres のバージョンが15になったのを確認したら、以下のコマンドで pgvector を有効化させます。
CREATE EXTENSION IF NOT EXISTS vector
環境
- Heroku Postgres 15
- Laravel v9.25.1
- PHP v8.1.26
- pgvector/pgvector v0.1.4
Laravel で pgvector を扱う
ここからは Laravel (PHP) での処理の内容になります。
Laravel のマイグレーションで pgvector を有効にする
弊社では Heroku Postgres の状態を Laravel で管理しているので、 pgvector の有効化をマイグレーションで制御します。
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::statement('CREATE EXTENSION IF NOT EXISTS vector');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::statement('DROP EXTENSION vector');
}
};
Salesforce の外部キーと vector カラムを持つテーブルを作成する
Heroku Connect で作成されたテーブルに vector カラムを追加することはできないので、外部キーと vector カラムを持つテーブルを作成します。
なお、OpenAI API で作成されるエンべディングに合わせて、 vector は1536次元を設定することにします。
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('vectors', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('lid_data')->unique();
$table->vector('embedding', 1536);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('vectors');
}
};
OpenAI API を利用してエンべディングを作成する
公式ドキュメントを参考に Embeddings API を実行します。
https://platform.openai.com/docs/guides/embeddings/what-are-embeddings
この記事では $projectEntries
の配列内にある abstract__c
を対象にエンべディングを行います。
ベクトル化されたデータと元のデータの紐付けをして、 vectors
テーブルに格納していきます。
また、DBの負荷軽減のために5000件ずつに分けて挿入しています。
private function fetchEmbeddings(Collection $projectEntries)
{
$apiKey = config('services.openai.api_key');
$response = Http::withToken($apiKey)->acceptJson()->asJson()->baseUrl('https://api.openai.com')->post('/v1/embeddings', [
'input' => $projectEntries->pluck('abstract__c')->toArray(),
'model' => 'text-embedding-ada-002',
]);
collect($response['data'])
->map(function ($value) use ($projectEntries) {
return [
'id' => Str::orderedUuid()->toString(),
'lid_data' => $projectEntries[$value['index']]->sfid,
'embedding' => new PGVector($value['embedding']),
];
})
->chunk(5000)
->each(function ($values) {
Vector::upsert($values->toArray(), ['lid_data'], ['embedding']);
});
}
ベクトル検索をする
これでベクトル検索の事前準備が終わりました!
それでは早速、ベクトル検索を試してみますo(^▽^)o
結果は載せられませんが、満足のいく結果が得られました!
検索する文章の具体性が高い程、関連するデータが出てきやすかったです。
self::search('ロボットを使って海をきれいにする');
use App\Models\ProjectEntry;
use App\Models\Vector;
private function search($query)
{
$apiKey = config('services.openai.api_key');
$response = Http::withToken($apiKey)->acceptJson()->asJson()->baseUrl('https://api.openai.com')->post('/v1/embeddings', [
'input' => $query,
'model' => 'text-embedding-ada-002',
]);
$embedding = $response['data'][0]['embedding'];
$projectEntries = ProjectEntry::query()
->joinSub(Vector::select(['lid_data', 'embedding']), 'vectors', 'lid_data__c.sfid', '=', 'vectors.lid_data')
->nearestNeighbors('embedding', $embedding, Distance::L2)
->paginate($perPage);
}
※ サブジョインで可能な限り不要な項目を省いてから、ジョインさせています。
おわりに
Salesforce 内のデータのベクトル検索に対応できました!
結構良さそうな精度でデータを引っ張ってこれるので、重宝しそうです。
所感としてはエンべディングに始まり、エンべディングで終わる感じでした。
検索時にもエンべディングが必要とは思わなかったです。
検索する度にエンべディングが必要になるので、 OpenAI API の使用料には注意が必要ですよ!
それではまたお会いしましょう!
バイバイ〜(´・ω・`)ノシ