2024年5月24日金曜日

DBMS_VECTOR_CHAIN.UTL_TO_EMBEDDINGおよびUTL_TO_EMBEDDINGSの動作を確認する

Oracle Database 23aiではエンべディング(embedding)の生成に、SQLファンクションのVECTOR_EMBEDDINGと、PL/SQLパッケージDBMS_VECTOR_CHAINに含まれるUTL_TO_EMBEDDINGまたはUTL_TO_EMBEDDINGSを呼び出す方法が提供されています。

今回はLM StudioのOpenAI互換のEmbedding APIを呼び出し、エンべディングを生成してみます。SQLファンクションのVECTOR_EMBEDDINGはONNXモデル向けなので、今回は確認の対象から外します。DBMS_VECTOR_CHAINのUTL_TO_EMBEDDINGおよびUTL_TO_EMBEDDINGSでは、ONNXモデル(providerがdatabase)と、それ以外の外部API呼び出しによるエンべディングの生成に対応しています。

Embeddingモデルとしてnomic-embed-text-v1.5f16.ggufを使用しています。生成されるエンべディングの評価を目的とはしていないため、必ずしもこのモデルを使わなければならない、ということではありません。


ローカルのMac上にOracle Database 23aiのコンテナ・イメージから作成したOracle APEXの環境を使って、エンべディングの生成を確認します。APEXのワークスペースはAPEXDEV、デフォルトのワークスペース・スキーマはAutonomous Databaseの仕様に合わせてWKSP_APEXDEVとして作成しています。

最初に試験に使用する表TEST_EMBEDDINGSを作成します。

create table if not exists test_embeddings(
id number primary key,
text clob,
v0 vector,
v1 vector
);
delete from test_embeddings;
insert into test_embeddings(id, text) values(1,
q'~
吾輩わがはいは猫である。名前はまだ無い。
どこで生れたかとんと見当けんとうがつかぬ。
~'
);
insert into test_embeddings(id, text) values(2,
q'~
何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。
吾輩はここで始めて人間というものを見た。
~'
);
insert into test_embeddings(id, text) values(3,
q'~
しかもあとで聞くとそれは書生という人間中で一番獰悪どうあくな種族であったそうだ。
この書生というのは時々我々を捕つかまえて煮にて食うという話である。
~'
);
commit;
SQLスクリプトとして実行します。列TEXTにエンべディングの生成に使用する文章を入力しています。


TEST_EMBEDDINGSの作成とテスト用のデータの投入が行われます。


DBMS_VECTOR_CHAIN.CREATE_CREDENTIALを呼び出し、OpenAIのAPIを呼び出すために使用するクリデンシャルを作成します。実際に呼び出すのはLM Studioのローカル・サーバーなので、access_tokenには適当な文字列を設定します。
begin
    dbms_vector_chain.create_credential(
        credential_name => 'OPENAI_CRED'
        ,params => JSON(
            json_object(
                key 'access_token' value '適当な文字列'
            )
        )
    );
end;

ORA-27486: 権限が不足していますが発生する場合は、ドキュメントのSQL RAG Exampleにあるように、デフォルトのパーシング・スキーマWKSP_APEXDEVにCREATE CREDENTIAL権限を付与します。

PDBのFREEPDB1にユーザーSYS/SYSDBAで接続し、ユーザーWKSP_APEXDEVにCREATE CREDENTIAL権限を割り当てます。

bash-4.4$ sqlplus / as sysdba


SQL*Plus: Release 23.0.0.0.0 - Production on Mon May 20 08:01:55 2024

Version 23.4.0.24.05


Copyright (c) 1982, 2024, Oracle.  All rights reserved.



Connected to:

Oracle Database 23ai Free Release 23.0.0.0.0 - Develop, Learn, and Run for Free

Version 23.4.0.24.05


SQL> alter session set container = freepdb1;


Session altered.


SQL> grant create credential to wksp_apexdev;


Grant succeeded.


SQL> 


クリデンシャルOPENAI_CREDを作成するために、再度、同じスクリプトを実行します。今度は正常に作成されます。


ビューUSER_CREDENTIALSを検索し、作成したクリデンシャルOPENAI_CREDが有効になっていることを確認します。(ENABLEDがTRUE)

select * from user_credentials


最初にOpenAI互換のEmbedding APIを、直接呼び出してみます。以下のコードを実行します。

declare
l_id number;
l_text clob;
l_request_json json_object_t;
l_request clob;
l_response clob;
l_dim number;
l_clob clob;
l_vector vector;
l_response_json json_object_t;
l_embeddings json_array_t;
l_object json_object_t;
l_embedding json_array_t;
begin
l_id := 1;
select text into l_text from test_embeddings where id = l_id;
/* OpenAI compatible API */
l_request_json := json_object_t();
l_request_json.put('model','text-embedding-3-small');
l_request_json.put('input', l_text);
l_request := l_request_json.to_clob();
apex_web_service.set_request_headers('Content-Type','application/json');
l_response := apex_web_service.make_rest_request(
p_url => 'http://host.docker.internal:8080/v1/embeddings'
,p_http_method => 'POST'
,p_body => l_request
);
-- dbms_output.put_line(l_response);
/* retieve embedding from OpenAI embedding API response */
l_response_json := json_object_t(l_response);
l_embeddings := l_response_json.get_array('data'); -- get data array in the response
l_object := treat(l_embeddings.get(0) as json_object_t); -- first object in the array
l_embedding := l_object.get_array('embedding'); -- get embedding in the object
/* convert json arrray into vector data */
l_dim := l_embedding.get_size();
l_clob := l_embedding.to_clob();
dbms_output.put_line('array dim = ' || l_dim);
dbms_output.put_line(l_clob);
l_vector := to_vector(l_clob, l_dim, FLOAT32); -- convert it to vector type
dbms_output.put_line('vector dim = ' || vector_dims(l_vector));
dbms_output.put_line(from_vector(l_vector));
/* store generated embedding in table test_embeddings */
update test_embeddings set v0 = l_vector where id = l_id;
commit;
end;
IDの行の列TEXTの内容からエンべディングを生成し、列V0に保存しています。


LM StudioのServer logsとして、以下が出力されます。

[2024-05-24 10:07:59.909] [INFO] Received POST request to /v1/embeddings with body: { "model": "text-embedding-3-small", "input": " \n吾輩わがはいは猫である。名前はまだ無い。 \nどこで生れたかとんと見当けんとうがつかぬ。 \n" }
[2024-05-24 10:07:59.961] [INFO] Returning embeddings (not shown in logs)

同じ処理を、DBMS_VECTOR_CHAIN.UTL_TO_EMBEDDINGを呼び出して実施します。以下のコードを実行します。

IDの行の列TEXTの内容からエンべディングを生成し、列V1に保存しています。

declare
l_id number;
l_text clob;
l_vector vector;
begin
l_id := 1;
select text into l_text from test_embeddings where id = l_id;
l_vector := dbms_vector_chain.utl_to_embedding(
data => l_text
,params => JSON(json_object(
key 'provider' value 'OpenAI'
,key 'credential_name' value 'OPENAI_CRED'
,key 'url' value 'http://host.docker.internal:8080/v1/embeddings'
,key 'model' value 'text-embedding-3-small'
,key 'transfer_timeout' value 60
)
)
);
dbms_output.put_line('vector dim = ' || vector_dims(l_vector));
dbms_output.put_line(from_vector(l_vector));
/* store generated embedding in table test_embeddings */
update test_embeddings set v1 = l_vector where id = l_id;
commit;
end;

LM StudioのServer logsとして、以下が出力されます。

[2024-05-23 13:44:59.925] [INFO] Received POST request to /v1/embeddings with body: { "input": [ " \n吾輩わがはいは猫である。名前はまだ無い。 \nどこで生れたかとんと見当けんとうがつかぬ。 \n" ], "model": "text-embedding-3-small" }
[2024-05-23 13:44:59.989] [INFO] Returning embeddings (not shown in logs)

Embedding APIの呼び出しリクエストがほぼ同じ(inputが配列として渡されているところは異なります)なので、生成されているエンべディングも同一になるはずです。確認してみます。2つのベクトルのユークリッド距離を求めます。

select l2_distance(v0, v1) from test_embeddings where id = 1;

結果として0が返されるので、2つのベクトルは同じ値です。


続いて、複数の文章からエンべディングの配列を生成します。以下のコードを実行します。

declare
l_id number;
l_embed_data clob;
l_request_json json_object_t;
l_request clob;
l_response clob;
l_dim number;
l_clob clob;
l_vector vector;
l_response_json json_object_t;
l_embeddings json_array_t;
l_object json_object_t;
l_embedding json_array_t;
/* text array */
l_text_array json_array_t;
begin
l_request_json := json_object_t();
l_request_json.put('model','text-embedding-3-small');
l_text_array := json_array_t();
for r in (select id, text from test_embeddings order by id asc)
loop
l_text_array.append(r.text);
end loop;
l_request_json.put('input', l_text_array);
l_request := l_request_json.to_clob();
apex_web_service.set_request_headers('Content-Type','application/json');
l_response := apex_web_service.make_rest_request(
p_url => 'http://host.docker.internal:8080/v1/embeddings'
,p_http_method => 'POST'
,p_body => l_request
);
-- dbms_output.put_line(l_response);
/* retieve embedding from OpenAI embedding API response */
l_response_json := json_object_t(l_response);
l_embeddings := l_response_json.get_array('data'); -- get data array in the response
for i in 1..l_embeddings.get_size()
loop
l_object := treat(l_embeddings.get(i-1) as json_object_t);
l_embedding := l_object.get_array('embedding'); -- get embedding in the object
/* convert json array into vector data */
l_dim := l_embedding.get_size();
l_clob := l_embedding.to_clob();
dbms_output.put_line('array dim = ' || l_dim);
dbms_output.put_line(l_clob);
l_vector := to_vector(l_clob, l_dim, FLOAT32); -- convert it to vector type
dbms_output.put_line('vector dim = ' || vector_dims(l_vector));
dbms_output.put_line(from_vector(l_vector));
/* store generated embedding in table test_embeddings */
update test_embeddings set v0 = l_vector where id = i;
end loop;
commit;
end;


LM StudioのServer logsとして、以下が出力されます。

[2024-05-23 20:10:53.473] [INFO] Received POST request to /v1/embeddings with body: { "model": "text-embedding-3-small", "input": [ " \n吾輩わがはいは猫である。名前はまだ無い。 \nどこで生れたかとんと見当けんとうがつかぬ。 \n", " \n何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。 \n吾輩はここで始めて人間というものを見た。 \n", " \nしかもあとで聞くとそれは書生という人間中で一番獰悪どうあくな種族であったそうだ。 \nこの書生というのは時々我々を捕つかまえて煮にて食うという話である。 \n" ] }
[2024-05-23 20:10:53.605] [INFO] Returning embeddings (not shown in logs)

同じ処理を、DBMS_VECTOR_CHAIN.UTL_TO_EMBEDDINGSを呼び出して実施します。以下のコードを実行しました。

declare
l_inputs vector_array_t;
l_input json_object_t;
l_vector vector;
l_outputs vector_array_t;
l_output json_object_t;
l_embedding json_array_t;
l_dim number;
l_clob clob;
begin
/*
 * conform input data to the result of utl_to_chunks
*/
l_inputs := vector_array_t();
for r in (select rownum, text from test_embeddings order by id asc)
loop
l_input := json_object_t();
l_input.put('chunk_id', r.rownum);
l_input.put('chunk_offset', 1);
l_input.put('chunk_length', dbms_lob.getlength(r.text));
l_input.put('chunk_data', r.text);
l_inputs.extend;
l_inputs(r.rownum) := l_input.to_clob();
end loop;
/*
* generate embeddings by calling 3rd party api
*/
l_outputs := dbms_vector_chain.utl_to_embeddings(
data => l_inputs
,params => JSON(json_object(
key 'provider' value 'OpenAI'
,key 'credential_name' value 'OPENAI_CRED'
,key 'url' value 'http://host.docker.internal:8080/v1/embeddings'
,key 'model' value 'text-embedding-3-small'
,key 'batch_size' value 10
,key 'transfer_timeout' value 60
)
)
);
/*
* return value of dbms_vector_chain.utl_to_embeddings is
* array of verctor (in form of json array of float number).
* e.g.
* [0.07786194980144501,0.002595797646790743,...,-0.14850468933582306]
* [0.061151809990406036,0.040036916732788086,...,-0.13171206414699554]
* [0.01802247017621994,0.027299214154481888,...,-0.12203855812549591]
* not the following format, why???
* { "embed_id":"1", "embed_data":"....", "embed_vector":"[0.07786194980144501,...,-0.14850468933582306]"}
* { "embed_id":"2", "embed_data":"....", "embed_vector":"[0.061151809990406036,...,-0.13171206414699554]"}
* { "embed_id":"3", "embed_data":"....", "embed_vector":"[0.01802247017621994,...,-0.12203855812549591]"}
*/
for i in l_outputs.first .. l_outputs.last
loop
l_embedding := json_array_t(l_outputs(i));
l_dim := l_embedding.get_size();
l_clob := l_embedding.to_clob();
dbms_output.put_line('array dim = ' || l_dim);
dbms_output.put_line(l_clob);
l_vector := to_vector(l_clob, l_dim, FLOAT32); -- convert it to vector type
dbms_output.put_line('vector dim = ' || vector_dims(l_vector));
dbms_output.put_line(from_vector(l_vector));
update test_embeddings set v1 = l_vector where id = i;
end loop;
commit;
end;


LM StudioのServer logsとして、以下が出力されます。OpenAIのAPIを直接呼び出したときと、同じリクエストになっています。

[2024-05-24 10:12:51.502] [INFO] Received POST request to /v1/embeddings with body: { "input": [ " \n吾輩わがはいは猫である。名前はまだ無い。 \nどこで生れたかとんと見当けんとうがつかぬ。 \n", " \n何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。 \n吾輩はここで始めて人間というものを見た。 \n", " \nしかもあとで聞くとそれは書生という人間中で一番獰悪どうあくな種族であったそうだ。 \nこの書生というのは時々我々を捕つかまえて煮にて食うという話である。 \n" ], "model": "text-embedding-3-small" }
[2024-05-24 10:12:51.592] [INFO] Returning embeddings (not shown in logs)

引数dataとして与えるVECTOR_ARRAY_Tの内容は、DBMS_VECTOR_CHAIN.UTL_TO_CHUNKSの出力形式に合わせます。

OpenAIのAPIを直接呼び出して取得したエンべディングと、DBMS_VECTOR_CHAIN.UTL_TO_EMBEDDINGSを呼び出して取得したエンべディングのユークリッド距離を確認します。

select id, l2_distance(v0, v1) from test_embeddings order by id asc

すべて0となり、同一のエンべディングが作成できていることが確認できます。


ドキュメントではbatch sizeと記載されていますが、正確にはbatch_sizeです。batch_sizeに指定された数の文章をinputに含め、複数回のAPI呼び出しによりすべてのエンべディングが生成されます。

batch_sizeに1を指定すると、LM StudioのServer logsに以下のように出力され、複数回のAPI呼び出しが行われていることが確認できます。

[2024-05-24 10:46:41.451] [INFO] Received POST request to /v1/embeddings with body: { "input": [ " \n吾輩わがはいは猫である。名前はまだ無い。 \nどこで生れたかとんと見当けんとうがつかぬ。 \n" ], "model": "text-embedding-3-small" }
[2024-05-24 10:46:41.503] [INFO] Returning embeddings (not shown in logs)
[2024-05-24 10:46:41.537] [INFO] Received POST request to /v1/embeddings with body: { "input": [ " \n何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。 \n吾輩はここで始めて人間というものを見た。 \n" ], "model": "text-embedding-3-small" }
[2024-05-24 10:46:41.552] [INFO] Returning embeddings (not shown in logs)
[2024-05-24 10:46:41.584] [INFO] Received POST request to /v1/embeddings with body: { "input": [ " \nしかもあとで聞くとそれは書生という人間中で一番獰悪どうあくな種族であったそうだ。 \nこの書生というのは時々我々を捕つかまえて煮にて食うという話である。 \n" ], "model": "text-embedding-3-small" }
[2024-05-24 10:46:41.604] [INFO] Returning embeddings (not shown in logs)

ドキュメントに記載されていないようですが、引数dataにCLOB型の値を渡し(つまりUTL_TO_EMBEDDINGSを呼び出しますが、対象となる文章は1つだけ)、戻り値の方をVECTOR_ARRAY_TとしてUTL_TO_EMBEDDINGSを呼び出せるようです。

declare
l_id number;
l_text clob;
l_vector vector;
l_outputs vector_array_t;
l_output json_object_t;
l_embedding json_array_t;
l_dim number;
l_clob clob;
begin
l_id := 1;
select text into l_text from test_embeddings where id = l_id;
l_outputs := dbms_vector_chain.utl_to_embeddings(
data => l_text
,params => JSON(json_object(
key 'provider' value 'OpenAI'
,key 'credential_name' value 'OPENAI_CRED'
,key 'url' value 'http://host.docker.internal:8080/v1/embeddings'
,key 'model' value 'text-embedding-3-small'
,key 'transfer_timeout' value 60
)
)
);
/* response is single row of json document.
* e.g.
* { "embed_id"; "1", "embed_data":"...", "embed_vector":"[0.07786194980144501,...,0.002595797646790743]"}
*/
dbms_output.put_line(l_outputs(1));
/* retrieve vector from json */
l_output := json_object_t(l_outputs(1));
l_clob := l_output.get_clob('embed_vector'); -- embed_vector is string instead of json array.
l_embedding := json_array_t(l_clob);
l_dim := l_embedding.get_size();
l_clob := l_embedding.to_clob();
dbms_output.put_line('array dim = ' || l_dim);
dbms_output.put_line(l_clob);
l_vector := to_vector(l_clob, l_dim, FLOAT32); -- convert it to vector type
dbms_output.put_line('vector dim = ' || vector_dims(l_vector));
dbms_output.put_line(from_vector(l_vector));
end;
この場合は戻り値のVECTOR_ARRAY_T(これはTABLE OF CLOBとして定義されています)であるため、1つだけCLOBの要素が返されます。内容はJSONになっていました。


一つだけの要素の内容は以下です。
{"embed_id":"1","embed_data":" \n吾輩わがはいは猫である。名前はまだ無い。 \nどこで生れたかとんと見当けんとうがつかぬ。 \n","embed_vector":"[0.07786194980144501,0.002595797646790743,-0.14850468933582306,0.01288861595094204,0.02572513557970524,0.03886890783905983,-0.02595830149948597,-0.006078328471630812,-0.030467288568615913,0.0006770426989533007,-0.06241714581847191,-0.01921042799949646, [中略] -0.009982775896787643,0.017772963270545006,0.010874804109334946,-0.01866289973258972,0.03308163210749626,-0.004839506931602955,-0.006120910868048668,-0.008617770858108997,0.032157570123672485,0.059361301362514496,0.01376398466527462,0.02976307086646557,-0.01138649694621563,0.019222760573029518,-0.024225575849413872,0.00522598996758461,0.036297716200351715,0.0064460234716534615,-0.08576755225658417]"}
戻り値となるJSONには、仕様にそってembed_id(なぜか値は文字列)、embed_dataが含まれていて、embed_vectorとしてエンべディングがJSON配列の文字列として返されています。UTL_TO_EMBEDDING、UTL_TO_EMBEDDINGSともに、出力がベクトルのみの方が扱いやすいのですが、ドキュメントの記載とは違っているようにも思います。

Oracle AI Vector Search User's GuideのConvert File to Text to Chunks to Embeddingsには、以下のコード例が載っています。
SELECT et.* from 
  documentation_tab dt,
  dbms_vector_chain.utl_to_embeddings(
    dbms_vector_chain.utl_to_chunks(dbms_vector_chain.utl_to_text(dt.data)),
    json(:embed_params)) et;
これと同じ構造の以下のコードを実行してみます。

with documentation_tab as (
select q'~
吾輩わがはいは猫である。名前はまだ無い。
 どこで生れたかとんと見当けんとうがつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪どうあくな種族であったそうだ。この書生というのは時々我々を捕つかまえて煮にて食うという話である。しかしその当時は何という考もなかったから別段恐しいとも思わなかった。ただ彼の掌てのひらに載せられてスーと持ち上げられた時何だかフワフワした感じがあったばかりである。掌の上で少し落ちついて書生の顔を見たのがいわゆる人間というものの見始みはじめであろう。この時妙なものだと思った感じが今でも残っている。第一毛をもって装飾されべきはずの顔がつるつるしてまるで薬缶やかんだ。その後ご猫にもだいぶ逢あったがこんな片輪かたわには一度も出会でくわした事がない。のみならず顔の真中があまりに突起している。そうしてその穴の中から時々ぷうぷうと煙けむりを吹く。どうも咽むせぽくて実に弱った。これが人間の飲む煙草たばこというものである事はようやくこの頃知った。
 この書生の掌の裏うちでしばらくはよい心持に坐っておったが、しばらくすると非常な速力で運転し始めた。書生が動くのか自分だけが動くのか分らないが無暗むやみに眼が廻る。胸が悪くなる。到底とうてい助からないと思っていると、どさりと音がして眼から火が出た。それまでは記憶しているがあとは何の事やらいくら考え出そうとしても分らない。
 ふと気が付いて見ると書生はいない。たくさんおった兄弟が一疋ぴきも見えぬ。肝心かんじんの母親さえ姿を隠してしまった。その上今いままでの所とは違って無暗むやみに明るい。眼を明いていられぬくらいだ。はてな何でも容子ようすがおかしいと、のそのそ這はい出して見ると非常に痛い。吾輩は藁わらの上から急に笹原の中へ棄てられたのである。
~' as data from dual
)
select et.* from
documentation_tab dt,
dbms_vector_chain.utl_to_embeddings(
data => dbms_vector_chain.utl_to_chunks(
data => dbms_vector_chain.utl_to_text(
data => dt.data
)
,params => json(json_object(
key 'by' value 'words'
,key 'max' value 100
)
)
)
,params => JSON(json_object(
key 'provider' value 'OpenAI'
,key 'credential_name' value 'OPENAI_CRED'
,key 'url' value 'http://host.docker.internal:8080/v1/embeddings'
,key 'model' value 'text-embedding-3-small'
,key 'transfer_timeout' value 60
)
)
) et;
JSONの配列ではなくベクトルの配列が返されます。プロバイダ(databaseか3rd Partyか)または、3rd Partyのプロバイダの種類に依存して、返される値も異なるのかもしれません。