2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HerokuAdvent Calendar 2023

Day 24

Heroku Connect で同期した Sales Cloud のデータを pgvector でベクトル検索してみた

Last updated at Posted at 2023-12-23

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

以下の流れで、バージョンアップを行います。

  1. フォロワーデータベースをプロビジョニングする

    heroku addons:create heroku-postgresql:standard-2 --follow 
    HEROKU_POSTGRESQL_{カラー}_URL --app {アプリ名}
    
  2. アプリをメンテナンスモードにする

  3. Heroku Connect を停止する

  4. フォロワーデータベースが追いついたか確認する

    heroku pg:info --app {アプリ名}
    

    Behind By: 0 commits なら追いついている

  5. フォロワデータベースの PostgreSQL バージョンをアップグレードする

    heroku pg:upgrade HEROKU_POSTGRESQL_{新しく作られたカラー}_URL --app {アプリ名}
    

    heroku pg:wait HEROKU_POSTGRESQL_{新しく作られたカラー}_URL --app {アプリ名} で進行状況を監視できる

  6. バージョンアップしたフォロワデータベースをプライマリデータベースにする

    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 の使用料には注意が必要ですよ!

それではまたお会いしましょう!
バイバイ〜(´・ω・`)ノシ

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?