特に画像はデータが大きいため、実際の画像をサーバーに送信せずに似たような画像を検索できたら便利かもしれない、と考えました。この程度のアプリであれば、生成AIにお願いすれば1日もかからず作成できるので、とりあえず作成してみました。
作成したAPEXアプリケーションは以下のように動作します。
作ってみた所感は以下です。
- Oracle Databaseにロードするモデルとブラウザにロードするモデルとでは、モデルが同じでも画像のプリプロセッサやテキストのトークナイザが異なるため、生成されるエンべディングに違いがある。
- 生成されたエンべディングに特徴は反映されているので、類似検索はそれなりにできる。
- しかし、プリプロセッサやトークナイザは、クライアントとサーバーで同じものを使うようにすべきでしょう。
APEXアプリケーションのサーバー側の処理は、生成AIではなく手作業で記述しています。画像からエンべディングを生成して保存するためにUPDATE文を1行、ブラウザから送られたエンべディングを受信して画像を検索するSELECT文を1行書いています。
最初にブラウザで画像およびテキストから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を設定します。
次へ進みます。
主キー列1にID(Number)が選択されます。そのまま、ページの作成をクリックします。
対話モード・レポートとフォームのページが作成されます。
画像の確認をしやすくするために、対話モード・レポートのリージョンのタイプをカードに変更します。ソースのタイプを表/ビューに変更し、表名にEBAJ_TEST_IMAGESを設定します。
ボタンCREATEのスロットが無効になっているため、レイアウトのスロットをSort Orderに変更します。
アクションを作成し、識別のタイプにカード全体を選択します。リンクのタイプにこのアプリケーションのページにリダイレクトを選択し、ターゲットのページにフォームのページである3を指定します。
カードの主キー列1にID、タイトルの列にTITLEを設定します。メディアのソースをBLOB列とし、BLOB列にIMAGEを選択します。BLOB属性のMIMEタイプ列にIMAGE_MIMETYPE、最終更新列にIMAGE_LASTUPDを設定します。
ページを実行して作成ボタンをクリックすると、画像をアップロードするフォームが表示されます。
ページ・デザイナでフォームのページを開き、表示や処理を調整します。
ページ・アイテムP3_IMAGEのタイプをイメージ・アップロードに変更し、ストレージのBLOB最終更新列にIMAGE_LASTUPD、MIMEタイプ列にIMAGE_MIMETYPE、ファイル名列にIMAGE_FILENAMEを設定します。
ページ・アイテムP3_IMAGE_FILENAME、P3_IMAGE_MIMETYPE、P3_IMAGE_CHARSET、P3_IMAGE_LASTUPD、P3_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とします。
外観のホットをオンにし、テンプレート・オプションのWidthをStretchに変更します。動作のアクションに動的アクションで定義を設定します。
検索条件とするテキストを入力するページ・アイテムとして、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を設定します。ソートのデフォルト順序を1に設定し、方向を降順とすることで、類似度の高い順からレポートにリストされるようにします。
列IMAGE_FILENAME、IMAGE_MIMETYPE、IMAGE_CHARSET、IMAGE_LASTUPD、EMBEDDINGはレポートに表示されないように、コメント・アウトします。
列IMAGEにバイト数ではなく画像が表示されるようにします。
識別のタイプをイメージの表示に変更します。BLOB属性の表名にEBAJ_TEST_IMAGESを選択し、BLOB列としてIMAGE、主キー列1にID、MIMEタイプ列に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に設定するため、コピーしておきます。
静的アプリケーション・ファイルとして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のアプリケーション作成の参考になれば幸いです。
完