ラベル ベクトル検索 の投稿を表示しています。 すべての投稿を表示
ラベル ベクトル検索 の投稿を表示しています。 すべての投稿を表示

2025年10月7日火曜日

ブラウザで生成した埋め込みベクトルを送信してサーバーでベクトル検索を実施する

Oracle Database 23aiでは、エンべディング・モデルとしてopenai/clip-vit-large-patch14をデータベースにロードできます。本記事では、ブラウザ上のTransformers.jsで実行できるXenova/clip-vit-large-patch14を呼び出してブラウザでエンべディングを生成し、それをサーバーに送信してベクトル検索を実行するAPEXアプリケーションを作成します。サーバーに保存するエンべディングは、Oracle Database 23aiにロードしたONNXモデルのopenai/clip-vit-large-patch14を使用します。

特に画像はデータが大きいため、実際の画像をサーバーに送信せずに似たような画像を検索できたら便利かもしれない、と考えました。この程度のアプリであれば、生成AIにお願いすれば1日もかからず作成できるので、とりあえず作成してみました。

作成したAPEXアプリケーションは以下のように動作します。


作ってみた所感は以下です。
  1. Oracle Databaseにロードするモデルとブラウザにロードするモデルとでは、モデルが同じでも画像のプリプロセッサやテキストのトークナイザが異なるため、生成されるエンべディングに違いがある。
  2. 生成されたエンべディングに特徴は反映されているので、類似検索はそれなりにできる。
  3. しかし、プリプロセッサやトークナイザは、クライアントとサーバーで同じものを使うようにすべきでしょう。
APEXアプリケーションのサーバー側の処理は、生成AIではなく手作業で記述しています。画像からエンべディングを生成して保存するためにUPDATE文を1行、ブラウザから送られたエンべディングを受信して画像を検索するSELECT文を1行書いています。

データベースにopenai/clip-vit-large-patch14のONNXモデルがロードされていることを前提とします。本ブログではこちらの記事こちらの記事で、ONNXモデルの作り方やロードの仕方を紹介しています。

最初にブラウザで画像およびテキストからclip-vit-large-patch14を使ってエンべディングを生成するアプリケーションを、OpenAI/GPT-5で大まかに作ってもらって、Claude Sonnet 4.5で完成してもらいました。プロンプトを渡しただけで、一行もコードは書いていません。参考までにAIが生成したコードはこちらです。


ブラウザでエンべディングを生成する処理はできたので、この処理を組み込むAPEXアプリケーションを作成します。

画像とエンべディングを保存する表を、EBAJ_TEST_IMAGESとして作成します。
create table ebaj_test_images (
    id                number generated by default on null as identity
                      constraint ebaj_test_images_id_pk primary key,
    title             varchar2(80 char) not null,
    image             blob,
    image_filename    varchar2(255 char),
    image_mimetype    varchar2(255 char),
    image_charset     varchar2(255 char),
    image_lastupd     date,
    embedding         vector(768,float32,dense)
);

SQLワークショップSQLコマンドで実行します。


空のAPEXアプリケーションを作成します。名前画像検索とします。


アプリケーションが作成されたら、画像を表EBAJ_TEST_IMAGESに保存するページを作成します。

ページの作成をクリックします。


対話モード・レポートを選択します。


レポート・ページの名前画像一覧とします。フォーム・ページを含めるオンにし、フォーム・ページ名画像編集とします。データ・ソース表/ビューの名前に先ほど作成したEBAJ_TEST_IMAGESを設定します。

へ進みます。


主キー列1ID(Number)が選択されます。そのまま、ページの作成をクリックします。


対話モード・レポートとフォームのページが作成されます。

画像の確認をしやすくするために、対話モード・レポートのリージョンのタイプカードに変更します。ソースタイプ表/ビューに変更し、表名EBAJ_TEST_IMAGESを設定します。


ボタンCREATEのスロットが無効になっているため、レイアウトスロットSort Orderに変更します。


対話モード・レポートの編集リンクの代わりとなるアクションを、カードに作成します。

アクションを作成し、識別タイプカード全体を選択します。リンクタイプこのアプリケーションのページにリダイレクトを選択し、ターゲットページにフォームのページであるを指定します。


リンク・ビルダーの設定です。ページ・アイテムP3_IDに値&ID.を設定します。


カード・リージョンの属性を開きます。データベースに保存している画像をカードに表示するために必要な、最低限の設定を行います。

カード主キー列1IDタイトルTITLEを設定します。メディアソースBLOB列とし、BLOB列IMAGEを選択します。BLOB属性MIMEタイプ列IMAGE_MIMETYPE最終更新列IMAGE_LASTUPDを設定します。


以上で表EBAJ_TEST_IMAGESに保存されている画像をカードとして表示するページと、それから呼び出せるフォームのページが作成されました。

ページを実行して作成ボタンをクリックすると、画像をアップロードするフォームが表示されます。


ページ・デザイナでフォームのページを開き、表示や処理を調整します。

ページ・アイテムP3_IMAGEタイプイメージ・アップロードに変更し、ストレージBLOB最終更新列IMAGE_LASTUPDMIMEタイプ列IMAGE_MIMETYPE、ファイル名列IMAGE_FILENAMEを設定します。


ページ・アイテムP3_IMAGE_FILENAMEP3_IMAGE_MIMETYPEP3_IMAGE_CHARSETP3_IMAGE_LASTUPDP3_EMBEDDINGはフォームから入力することは無いので、コメント・アウトします。


アップロードした画像からエンべディングを生成するプロセスを作成します。

識別名前Generate Embeddingとし、ソースPL/SQLコードに以下のUPDATE文を記述します。
update ebaj_test_images 
    set embedding = 
        (
            select vector_embedding(OPENAI_CLIP_VIT_IMG using image as data)
            from (
                select image from ebaj_test_images where id = :P3_ID
            )
        )
where id = :P3_ID;
サーバー側の条件タイプリクエストは値に含まれるを選択し、CREATE SAVEを設定します。


以上で、検索対象となる画像をデータベースにアップロードし、同時にエンべディングを作成して保存できるようになりました。

検索対象とする画像をアップロードします。


画像からエンべディングが正しく生成されているかどうかを確認します。SQLコマンドで以下のSELECT文を実行します。

select id, title, vector_dimension_count(embedding) from ebaj_test_images

エンべディングの次元数は768と表示されます。


ホーム・ページに画像の検索機能を実装します。

ページの左側に検索条件をまとめて配置するために、静的コンテンツのリージョンを作成します。名前Searchとします。


処理状況を表示するページ・アイテムをP1_STATUSとして作成します。タイプ表示のみラベルStatusとします。


検索を実行するボタンを作成します。ボタン名SEARCHラベルSearchとします。

外観ホットオンにし、テンプレート・オプションWidthStretchに変更します。動作アクション動的アクションで定義を設定します。


検索条件とするテキストを入力するページ・アイテムとして、P1_TEXTを作成します。タイプテキスト領域ラベルTextとします。


検索条件とする画像を選択するページ・アイテムとして、P1_IMAGEを作成します。タイプイメージ・アップロードラベルImage表示表示形式インライン・ファイル参照を選択します。


選択した画像をプレビューするリージョンを作成します。名前Previewタイプ静的コンテンツとします。ソースHTMLコードに以下を記述します。

<img id="image" style="width:100%;"></img>

外観テンプレートに装飾のないBlank with Attributesを選択します。


検索条件として与えられた画像やテキストから生成されたエンべディングを保持するページ・アイテムとして、P1_EMBEDDINGを作成します。タイプ非表示です。


検索結果を表示するクラシック・レポートを作成します。識別名前検索結果タイプクラシック・レポートです。

ソースタイプSQL問合せSQL問合せとして以下を記述します。
select
    ID,
    TITLE,
    sys.dbms_lob.getlength(IMAGE) image,
    IMAGE_FILENAME,
    IMAGE_MIMETYPE,
    IMAGE_CHARSET,
    IMAGE_LASTUPD,
    EMBEDDING,
    (1 - vector_distance(embedding, to_vector(:P1_EMBEDDING))) similarity
from EBAJ_TEST_IMAGES
送信するページ・アイテムとしてP1_EMBEDDINGを設定します。

検索条件の右隣に配置するため、レイアウト新規列の開始オフにします。また、リージョン内いっぱいにレポートを表示するため、外観テンプレート・オプションRemove Body Paddingをチェックします。


レポートの表示を調整します。

SIMILARITYを列TITLEの下に移動し、外観書式999G999G999G999G990D0000を設定します。ソートデフォルト順序に設定し、方向降順とすることで、類似度の高い順からレポートにリストされるようにします。

IMAGE_FILENAMEIMAGE_MIMETYPEIMAGE_CHARSETIMAGE_LASTUPDEMBEDDINGはレポートに表示されないように、コメント・アウトします。


IMAGEにバイト数ではなく画像が表示されるようにします。

識別タイプイメージの表示に変更します。BLOB属性表名EBAJ_TEST_IMAGESを選択し、BLOB列としてIMAGE主キー列1IDMIMEタイプ列IMAGE_MIMETYPEファイル名列IMAGE_FILENAME最終更新列IMAGE_LASTUPDを設定します。

ヘッダーおよびレイアウトの位置合せは左寄せ(開始)に設定します。


この状態でもレポートに画像は表示されますが、実寸で表示されるため画像が非常に大きくなります。


CSSを記述して、画像を画面の横幅の20%で表示されるようにします。

適用範囲を制限するために、検索結果のリージョンに静的IDとしてRESULTSを設定します。


ページ・プロパティCSSインラインに以下を記述します。
#RESULTS img {
  width: 20%;
  height: auto; /* アスペクト比を維持 */
}


CSSを設定するとホーム・ページの表示は以下になります。


以上で画面のデザインは完成です。

ブラウザ上でエンべディングを生成する処理を記述したファイルをapp.jsとして、静的アプリケーション・ファイルに作成します。APEXアクションのCHANGEは選択した画像のプレビューを表示します。SEARCHがブラウザ上でのエンべディングの生成を行います。


参照ページ・プロパティのJavaScriptファイルURLに設定するため、コピーしておきます。


生成したエンべディングをページ・アイテムP1_EMBEDDINGに設定すると、サーバーへの送信とレポートの更新はAPEXの動的アクションが実施します。

静的アプリケーション・ファイルとしてapp.jsと、ミニファイされたapp.min.jsが作成されます。


ホーム・ページのJavaScriptファイルURLに、作成したapp.jsの参照を設定します。

[module]#APP_FILES#app#MIN#.js


ページにAPEXアクションの呼び出しや動的アクションを作成します。

ボタンSEARCH詳細カスタム属性として、以下を設定します。クリックするとAPEXアクションのSEARCHが呼び出されます。

data-action="SEARCH"


ページ・アイテムP1_IMAGE変更されたときに、以下のJavaScriptが実行されるようにします。APEXアクションのCHANGEが呼び出されます。

apex.actions.invoke("CHANGE");


ページ・アイテムP1_EMBEDDINGが変更されたときに、クラシック・レポートのリージョン検索結果リフレッシュされるようにします。


以上でアプリケーションは完成です。アプリケーションを実行すると記事の先頭のGIF動画のように動作します。

今回作成したAPEXアプリケーションのエクスポートを以下におきました。
https://github.com/ujnak/apexapps/blob/master/exports/vector-search-with-local-embedding.zip

Oracle APEXのアプリケーション作成の参考になれば幸いです。

2025年7月24日木曜日

Oracle AI Vector SearchのBINARY Vector Generatorの例題をAPEXで実行する

先日、FLOAT32 Vector Generatorの例題について記事を書いています。今回は、その次のセクションに記載されているBINARY Vector Generatorの例題をAPEXのアプリケーションに実装してみます。

Oracle AI Vector Search User's Guide
3 Getting Started
SQL Quick Start Using a BINARY Vector Generator

注意:
ドキュメントに「Do not use the vector generator on production databases. The program is made available for testing and demo purposes.」記載されているように、Getting Startedの作業を本番データベースで実施しないでください。

作成したAPEXアプリケーションは以下のように動作します。今回は2次元の散布図なので、APEX標準のチャートを使用します。2次元の散布図にバイナリ・ベクトルを表示するため、バイナリ・ベクトルに対してt-SNEを適用し、2次元の埋め込み空間へ次元圧縮を行っています。t-SNEを適用するOML4Pyの埋め込みファンクションを定義し、OML4Py SQL APIから呼び出しています。


作成したAPEXアプリケーションのエクスポートを以下に置いています。
https://github.com/ujnak/apexapps/blob/master/exports/binary-vector-search-genbvec-jet.zip

Plotlyを組み込んでいないため、FLOAT32 Vector Generatorを扱うAPEXアプリケーションより簡単な作りになっています。

OML4Pyを使うため、このAPEXアプリケーションを実行できるプラットフォームはx86_64に限定されます。本記事ではAlways FreeのAutonomous Database上でアプリケーションを開発しています。

Autonomous DatabaseでOML4PyのSQL APIを呼び出す手順については、以前に記事「Autonomous DatabaseでPythonで記述したファンクションを表関数として実行する」で紹介しています。以下の2つの作業は、あらかじめ実施しておきます。
  1. APEXのワークスペース・スキーマのOMLユーザーオンにします。
  2. 管理者ユーザーADMINにて、pyqAppendHostAceを呼び出してネットワークACLを追加します。
OML4Pyが標準で持っているパッケージのみを使用するため、condaによる環境作成とアップロードは不要です。

また、実行計画を表示するため、APEXのワークスペース・スキーマに以下の権限を割り当てています。

grant select_catalog_role to <APEXワークスペース・スキーマ>;
grant select on sys.v_$sql to <APEXワークスペース・スキーマ>;
grant select on sys.v_$session to <APEXワークスペース・スキーマ>;
grant select on sys.v_$sql_plan to <APEXワークスペース・スキーマ>;

以下より、APEXアプリケーションの実装と使用しているAPEXの機能について、大まかに説明します。

サポートするオブジェクトインストール・スクリプトで、アプリケーションの動作に必要なデータベース・オブジェクトを作成しています。


この中で、表GENBVECの作成(スクリプトgenbvec)、プロシージャgenerate_random_binary_vectorgenerate_binary_clustergenerate_binary_vectorsの作成スクリプトは、ドキュメントに記載されているコードをそのままインストール・スクリプトにしています。

スクリプトgenbvec_tsneにて、表GENBVEC_TSNEを作成します。OML4PyのSQL APIを呼び出してt-SNEを適用した結果が、表GENBVEC_TSNEに保存されます。
create table genbvec_tsne(
    name varchar2(500),
    x number,
    y number
);
スクリプトgenbvec_vにてビューGENBVEC_Vを作成します。
create or replace view genbvec_v
as
select name, bv from genbvec;
OML4PyのpyqTableEvalの引数inp_namに与える表はVECTOR型を含むことができないようです。t-SNEの適用に必要な列NAMEと列BV(バイナリ・ベクトルの文字列表現)のみを含むビューGENBVEC_Vを作成し、pyqTableEvalのinp_namに指定します。

スクリプトcompute_tsne_embeddingにて、OML4PyのSQL APIから呼び出すPythonファンクションcompute_tsne_embeddingを作成します。


Pythonスクリプトcompute_tsne_embeddingは、入出力仕様を与えた上で、Claude Sonnet 4とOpenAI GPT-4oの両方を使って書いてもらっています。

以上のインストール・スクリプトが実行されると、このAPEXアプリケーションが必要としているデータベース・オブジェクトが準備されます。

インストール・スクリプトによって作成されたオブジェクトは、削除スクリプトによる削除されます。そのため、APEXアプリケーションを削除すると元の状態に戻ります。

APEXアプリケーションをインストールして実行します。

ナビゲーション・メニューにはホームTableConfigurationsOMLの4つのメニューが含まれます。記事の先頭にあるGIF動画は、ホーム・ページでの作業になります。

Configurationsのページでは、完全一致検索に使用するSELECT文と、検索条件が与えられていない場合に使用するSELECT文が登録されています。Getting Startedに記載されているSELECT文がデフォルトになっています。


OMLのページでは、OML4PyのSQL APIの呼び出しに必要な認証を実行します。APEXのワークスペース・スキーマのパスワードを入力し、ボタンGet Token / Set Auth Tokenをクリックします。

このページの実装については、OML4Pyの記事で説明しています。


Tableのページでは、表GENBVECへのデータ生成を行います。


プロセスGenerate Vectorsが、プロシージャGENERATE_BINARY_VECTORSを呼び出しています。ページ・アイテムはぞれぞれプロシージャGENERATE_BINARY_VECTORSの引数に対応しています。


表GENBVECへのデータ生成に続けてt-SNEを適用します。t-SNEの適用はプロセスcompute_tsne_embeddingが実行します。実行するPL/SQLコードは以下になります。適用結果は表GENBVEC_TSNEに保存されます。

Pythonファンクションcompute_tsne_embeddingは引数としてrandom_stateperplexitylearning_raten_tierを含んでいます。pyqTableEvalの引数par_lstに与えるJSONで、これらの引数の値を指定できます。


ボタンTruncate Tableをクリックすると、プロセスTruncate Tableが実行されます。表GENBVECおよびGENBVEC_TSNEがトランケートされます。


ホーム・ページには2つのボタン、SearchResetがあります。ボタンResetはページ・アイテムに設定された値を空白にします。そのため、実質的な処理を行うボタンはSearchのみになります。


ボタンSearchをクリックすると、以下のPL/SQLコードをサーバー側のコードとして実行します。検索されたバイナリ・ベクトルの名前をAPEXコレクションに保存します。

検索が完了した後に、チャート、実行計画、レポートのリージョンをリフレッシュして検索結果を反映します。


2次元の散布図はAPEXの標準チャートを使用しています。シリーズソースとして以下のSQL問合せを記述しています。

select REGEXP_SUBSTR(name, '^[^-]+') series, name, x, y from genbvec_tsne
where name in (
    select c001 from apex_collections where collection_name = 'SEARCH_RESULT'
)

列のマッピングシリーズ名に列SERIESラベルに列NAMEXに列XはいこれはYの翻訳ミスでしょう)に列Yを割り当てます。


Oracle APEXのチャートでは、チャートのX軸やY軸の最大値と最小値をページ・アイテムを元に設定できません。最大値と最小値が無指定の場合、表示するデータに合わせて最大値と最小値が決まります。

例えばベクトルC1の近傍20件を検索すると、チャートの表示は以下のようになります。


ベクトルC2の近傍20件を検索すると、チャートの表示は以下のようになります。検索されたベクトルに合わせてX軸やY軸の範囲が代わり、検索されたベクトルはつねにチャート全体に表示されます。


チャートのX軸、Y軸の最小値と最大値を、検索結果によらず、すべてのベクトルが表示できる値に固定するために、チャートの初期化JavaScriptファンクションに以下を記述しています。
function(config) {
    minVal = Number(apex.item("P1_MIN_VALUE").getValue());
    maxVal = Number(apex.item("P1_MAX_VALUE").getValue());
    config.xAxis.min = minVal;
    config.xAxis.max = maxVal;
    config.yAxis.min = minVal;
    config.yAxis.max = maxVal;
    return config;
}

X軸、Y軸の最小値(ページ・アイテムP1_MIN_VALUE)最大値(ページ・アイテムP1_MAX_VALUE)は表GENBVEC_TSNEの列XとYの値より算出しています。

今回の記事は以上になります。

Oracle APEXのアプリケーション作成の参考になれば幸いです。