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を作成します。

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を、直接呼び出してみます。以下のコードを実行します。

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に保存しています。


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つのベクトルは同じ値です。


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



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を呼び出して実施します。以下のコードを実行しました。



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を呼び出せるようです。

この場合は戻り値の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;
これと同じ構造の以下のコードを実行してみます。

JSONの配列ではなくベクトルの配列が返されます。プロバイダ(databaseか3rd Partyか)または、3rd Partyのプロバイダの種類に依存して、返される値も異なるのかもしれません。