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のアプリケーション作成の参考になれば幸いです。