2023年7月12日水曜日

画像から生成したベクトル埋め込みを使って画像とテキストを検索する

以前の記事でrinna社のjapanese-cloob-vit-b-16を使ってベクトル埋め込みを生成するサービスを作成しました。このサービスによって、画像とテキストのベクトル埋め込みを生成することができます。

せっかくなので、選択した画像を元に、画像とテキストを検索するアプリケーションを作成してみました。以前にエクスポートしたアプリケーションにページとして追加しています。
https://github.com/ujnak/apexapps/blob/master/exports/multimodal-search.zip

以下のように動作します。

動物(ホッキョクグマとタヌキ)の写真を選択し、そのベクトル埋め込みを生成してPineconeで検索しています。オブジェクト・ストレージに保存している画像とオラクル・データベースの表に保存しているテキストからベクトル埋め込みを生成し、その両方をPineconeのインデックスに保存しています。


以下よりアプリケーションの実装について説明します。


検索対象のテキストの準備



検索対象となるテキストを表VEC_TEXTSに保存します。以下のDDLを実行し、表を作成します。

create table vec_texts (
id varchar2(32 byte) default on null sys_guid()
constraint vec_texts_id_pk primary key,
text varchar2(4000 char) not null,
is_indexed varchar2(1 char) constraint vec_texts_is_indexed_ck
check (is_indexed in ('Y','N'))
)
;
view raw vec_texts.sql hosted with ❤ by GitHub
VEC_TEXTSをソースとした対話グリッドを作成し、検索対象となる(つまり、ベクトル埋め込みを生成しPineconeに保存する対象となる)テキストの入力を行います。


ベクトル埋め込みを生成しPineconeのインデックスにUpsertするボタンを作成します。

ボタン名UPSERT_VECTORSラベルUpsert Vectors動作アクションページの送信とします。


ボタンUPSERT_VECTORSを押下したときに実行されるプロセスをUpsert Vectorsとして作成します。

ソースPL/SQLコードに以下を記述します。

declare
l_request clob;
l_request_json json_object_t;
l_vectors json_array_t;
l_vector json_object_t;
l_embedding json_array_t;
l_response clob;
l_response_json json_object_t;
C_UPSERT constant varchar2(100) := :G_INDEX || '/vectors/upsert';
e_upsert_exception exception;
begin
/*
* Pinconeのindexに未登録のテキストを登録する。
*/
l_vectors := json_array_t();
for r in (
select id, text from vec_texts where is_indexed is null
)
loop
/* テキストのembeddingを生成する. */
select json_object(
key 'text' value r.text
) into l_request
from dual;
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Accept','application/json',p_reset => false);
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => :G_EMBED || '/embed-text'
,p_http_method => 'POST'
,p_body => l_request
);
l_response_json := json_object_t(l_response);
l_embedding := l_response_json.get_array('embedding');
l_vector := json_object_t();
l_vector.put('id', r.id);
l_vector.put('values', l_embedding);
/* このベクトルがtextから生成されたことを、metadataとして追加する。 */
l_vector.put('metadata', json_object_t('{ "type":"text" }'));
l_vectors.append(l_vector);
update vec_texts set is_indexed = 'Y' where id = r.id;
end loop;
if l_vectors.get_size = 0 then
/* 追加するテキストがないので終了 */
return;
end if;
l_request_json := json_object_t();
l_request_json.put('vectors', l_vectors);
l_request := l_request_json.to_clob;
/*
* PineconeのUpsertを呼び出す。
*/
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Accept','application/json',p_reset => false);
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => C_UPSERT
,p_http_method => 'POST'
,p_body => l_request
,p_credential_static_id => 'PINECONE_API'
);
/* レスポンス本文は解析不要。 */
apex_debug.info(l_response);
if apex_web_service.g_status_code <> 200 then
raise e_upsert_exception;
end if;
end;


Pineconeのインデックスから、テキストより生成したベクトルを削除するボタンを作成します。

ボタン名DELETE_VECTORSラベルDelete Vectors動作アクションページの送信とします。


ボタンDELETE_VECTORSを押下したときに実行されるプロセスをDelete Vectorsとして作成します。

ソースPL/SQLコードに以下を記述します。

declare
l_request clob;
l_response clob;
C_DELETE constant varchar2(100) := :G_INDEX || '/vectors/delete';
e_delete_exception exception;
begin
/*
* typeがtextのベクトルを削除する。
*/
select json_object(
key 'deleteAll' value 'false',
key 'filter' value json_object(
key 'type' value 'text'
)
) into l_request from dual;
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Accept','application/json',p_reset => false);
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => C_DELETE
,p_http_method => 'POST'
,p_body => l_request
,p_credential_static_id => 'PINECONE_API'
);
/* レスポンス本文は解析不要。 */
apex_debug.info(l_response);
if apex_web_service.g_status_code <> 200 then
raise e_delete_exception;
end if;
/* インデックス済みのフラグを初期化。 */
update vec_texts set is_indexed = null;
end;


以上で検索対象となるテキストの入力、ベクトル埋め込みの生成とインデックスへの更新、インデックスからの削除ができるようになりました。


画像を指定した検索



検索に使用する画像を選択するページ・アイテムはP2_IMAGEです。

識別タイプファイル参照...を指定します。ラベルImage Fileとしています。設定記憶域タイプとしてTable APEX_APPLICATION_TEMP_FILESを選択します。ファイルをパージするタイミングは、End of SessionおよびEnd of Requestのどちらを選択しても動作に違いはありません。


ページ・アイテムP2_TOP_Kにて、検索結果の数を指定します。


ページ・アイテムP2_TYPE選択リストでは、無指定imagetextのどれかを選択します。Pineconeのインデックスの検索条件のmetadataに含めることで、検索対象を画像とテキストの両方(無指定)、画像のみ(image)、テキストのみ(text)に限定します。


ページ・アイテムP2_PICTUREに、検索に使った画像を表示します。

設定基準Image URL stored in Page Item Valueを選択します。ページ・アイテムの値は、ファイルをアップロードしたときに実行されるプロセス内で設定します。


検索はボタンFINDをクリックして実行します。

ボタン名FINDラベルFind動作アクションページの送信です。


ボタンFINDを押下したときに実行されるプロセスをFind Images and Textsとして作成します。

ソースPL/SQLコードに以下を記述します。

declare
C_BASE_URL constant varchar2(200) := 'https://objectstorage.us-ashburn-1.oraclecloud.com/n/' || :G_NAMESPACE || '/b/' || :G_BUCKET || '/o/';
l_filename apex_application_temp_files.filename%type;
l_mime_type apex_application_temp_files.mime_type%type;
l_blob_content blob;
l_request clob;
l_response clob;
l_response_json json_object_t;
l_embedding json_array_t;
/* Pinecone */
l_query_json json_object_t;
l_matches json_array_t;
l_vector json_object_t;
/* search result */
l_id varchar2(400);
l_metadata json_object_t;
l_type varchar2(8);
l_score number;
C_QUERY constant varchar2(80) := :G_INDEX || '/query';
e_query_exception exception;
e_upload_failed_exception exception;
begin
/*
* 選択したファイルをオブジェクト・ストレージにアップロードする。
*/
select filename, mime_type, blob_content into l_filename, l_mime_type, l_blob_content
from apex_application_temp_files where name = :P2_IMAGE;
apex_web_service.set_request_headers('Content-Type', l_mime_type);
l_response := apex_web_service.make_rest_request(
p_url => C_BASE_URL || utl_url.escape(l_filename, false, 'AL32UTF8')
,p_http_method => 'PUT'
,p_body_blob => l_blob_content
,p_credential_static_id => 'OCI_API_ACCESS'
);
if apex_web_service.g_status_code <> 200 then
apex_debug.info(l_response);
raise e_upload_failed_exception;
end if;
/*
* アップロードした画像のembeddingを生成する。
*/
:P2_PICTURE := :G_PREAUTH_URL || l_filename;
select json_object(
key 'url' value :G_PREAUTH_URL || l_filename
) into l_request
from dual;
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Accept','application/json', p_reset => false);
apex_web_service.set_request_headers('Content-Type','application/json', p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => :G_EMBED || '/embed-image'
,p_http_method => 'POST'
,p_body => l_request
);
-- apex_debug.info(l_response);
l_response_json := json_object_t.parse(l_response);
l_embedding := l_response_json.get_array('embedding');
/*
* Pineconeに問い合わせる。
*/
l_query_json := json_object_t();
/* 検索対象に制限をかける。 */
if :P2_TYPE is not null then
l_query_json.put('filter', json_object_t('{ "type":"' || :P2_TYPE || '"}'));
end if;
l_query_json.put('includeValues', false);
l_query_json.put('includeMetadata', true);
l_query_json.put('vector', l_embedding);
l_query_json.put('topK', :P2_TOP_K);
l_request := l_query_json.to_clob;
/* queryを呼び出す。 */
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Accept','application/json',p_reset => false);
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => C_QUERY
,p_http_method => 'POST'
,p_body => l_request
,p_credential_static_id => 'PINECONE_API'
);
if apex_web_service.g_status_code <> 200 then
apex_debug.info(l_response);
raise e_query_exception;
end if;
/*
* レスポンスをAPEXコレクションに保存する。
*/
apex_collection.create_or_truncate_collection('IMAGES_AND_TEXTS');
l_response_json := json_object_t.parse(l_response);
l_matches := l_response_json.get_array('matches');
for i in 1..l_matches.get_size
loop
l_vector := json_object_t(l_matches.get(i-1));
l_id := l_vector.get_string('id');
l_score := l_vector.get_number('score');
l_metadata := l_vector.get_object('metadata');
l_type := '';
if l_metadata is not null then
l_type := l_metadata.get_string('type');
end if;
apex_collection.add_member(
p_collection_name => 'IMAGES_AND_TEXTS'
,p_c001 => l_id
,p_c002 => l_type
,p_n001 => l_score
);
end loop;
end;


検索結果を一覧するリージョンを作成します。

画像を一覧するリージョンとしてResult Imagesを作成します。タイプ対話モード・レポートソースSQL問合わせとして以下を記述します。
select c001, n001 from apex_collections where collection_name = 'IMAGES_AND_TEXTS' and c002 = 'image'


C001に画像が表示されるよう、列の書式HTML式として以下を記述します。

<img src="&G_PREAUTH_URL.#C001#" width="200"></img>


テキストを一覧するリージョンとしてResult Textsを作成します。タイプ対話モード・レポートソースSQL問合わせとして以下を記述します。
select t.id, t.text, c.n001 from vec_texts t join (
    select c001, n001 from apex_collections where collection_name = 'IMAGES_AND_TEXTS' and c002 = 'text'
) c
on t.id = c.c001

アプリケーションの実装の説明は以上になります。

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