- ファイルをアップロードする。
- アップロードしたファイルからテキストを取り出す。
- 取り出したテキストをチャンクに分割する。
- 分割したチャンクよりエンべディングを生成する。
- 問合せ文字列から、類似度の高いチャンクを一覧する。
- 問合せ文字列と類似度の高いチャンクをプロンプトに組み込み、言語モデルを使って回答文を生成する。
アプリケーションは基本的なワークフローの実装を目的としていて、実用性や精度は求めません。すべてローカルで実行し、費用の発生やセキュリティ上の心配は不要にします。
作成したアプリケーションは以下のように動作します。
アプリケーションの作成と実行には、以下の環境を使っています。
- エンべディングの生成と言語モデルの実行に、LM Studioを使います。
- 言語モデルとして、mmnga/ELYZA-japanese-Llama-2-7b-fast-instruct-ggufをロードします。
- エンべディングには、nomic-ai/nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.f16.ggufをロードします。
- Oracle Database 23aiはMacbook上のDocker/Colimaのx86_64エミュレーション環境で実行します。非常に遅いです。
- Oracle APEXは、23.2をOracle Database 23ai上で実行します。
以下より、サンプル・アプリケーションの作成手順を紹介します。
最初にアップロードしたファイルと、そのファイルに含まれる文字列を分割したチャンクを保存する表を作成します。
ファイルを保存する表はEBMJ_FILES、チャンクを保存する表はEBMJ_CHUNKSとして作成します。ファイルを保存する表EBMJ_FILESには、ファイルから取り出した文字列を列TEXTに保存します。チャンクを保存する表EBMJ_CHUNKSには、チャンクから生成したエンべディングを列EMBED_VECTORに保存します。
クイックSQLの以下のモデルを元に、これらの表を作成します。
# prefix: ebmj
files
title varchar2(200) /nn
content file
text clob
chunks /cascade
chunk_id num /nn
chunk_offset num /nn
chunk_length num /nn
chunk_data clob
embed_vector vector
レビューおよび実行を行います。
Oracle APEX 23.2のクイックSQLは、ベクトル型を認識しません。そのため、列EMBED_VECTORについては、以下の記述に置き換えます。
embed_vector vector
実行するDDLは以下になります。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
create table ebmj_files ( | |
id number generated by default on null as identity | |
constraint ebmj_files_id_pk primary key, | |
title varchar2(4000 char) not null, | |
content blob, | |
content_filename varchar2(255 char), | |
content_mimetype varchar2(255 char), | |
content_charset varchar2(255 char), | |
content_lastupd date, | |
text clob | |
); | |
create table ebmj_chunks ( | |
id number generated by default on null as identity | |
constraint ebmj_chunks_id_pk primary key, | |
file_id number constraint ebmj_chunks_file_id_fk | |
references ebmj_files on delete cascade, | |
chunk_id number not null, | |
chunk_offset number not null, | |
chunk_length number not null, | |
chunk_data clob, | |
embed_vector vector | |
); | |
-- table index | |
create index ebmj_chunks_i1 on ebmj_chunks (file_id); |
名前はSample Vector Searchとし、アプリケーションの作成ウィザードを使用します。
アプリケーション作成ウィザードが開きます。
ページの追加をクリックし、表EBMJ_FILESのフォーム付き対話モード・レポートのページを追加します。
ページ名はFilesとします。表またはビューとしてEBMJ_FILESを選択し、フォームを含めるにチェックを入れます。
ページの追加をクリックします。
表EBMJ_FILESをソースとしたフォーム付き対話モード・レポートのページが追加されます。
同様の手順で、表EBMJ_CHUNKSの対話モード・レポートのページを追加します。手作業での編集は行わないため、フォームは付けません。
ページ名はChunks、表またはビューとしてEBMJ_CHUNKSを選択します。
ページの追加をクリックします。
アプリケーションが作成されます。
これから、それぞれのページに修正を加えていきます。
最初にページ番号2の表EBMJ_FILESの対話モード・レポートを修正します。
ページ・デザイナで開きます。
アップロードしたファイルから取り出したテキストを保存する列TEXTは長文なので、レポートでは表示できません。コメント・アウトして変更を保存します。
ページ番号3の、表EBMJ_FILESを対象としたフォームのページを開きます。
こちらも列TEXTをソースとしたページ・アイテムP3_TEXTをコメント・アウトします。
アップロードしたファイルより、テキストの取り出し、チャンク分割、エンべディングの生成までを行うプロセスを実装します。
左ペインでプロセス・ビューを表示します。プロセスを作成します。
識別の名前はExtract, Chunking and Embeddingとします。タイプとして実行チェーンを選択します。
サーバー側の条件のタイプにリクエストは値に含まれるを選択し、値としてCREATE SAVEを指定します。
作成した実行チェーンに子プロセスを追加します。
識別の名前はExtract、ソースのPL/SQLコードとして以下を記述します。アップロードしたファイルを列CONTENTから読み出し、取り出したテキストを列TEXTに保存します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare | |
C_PARAMS constant json := json( | |
json_object( | |
key 'plaintext' value true | |
,key 'charset' value 'UTF8' | |
) | |
); | |
C_FILE_ID constant number := :P3_ID; | |
l_content blob; | |
l_text clob; | |
begin | |
select content into l_content from ebmj_files where id = C_FILE_ID; | |
l_text := dbms_vector_chain.utl_to_text( | |
data => l_content | |
,params => C_PARAMS | |
); | |
update ebmj_files set text = l_text where id = C_FILE_ID; | |
end; |
子プロセスを追加します。識別の名前はChunking、ソースのPL/SQLコードとして以下を記述します。ファイルから取り出したテキストをチャンクに分割し、表EBMJ_CHUNKSに保存します。ファンクションVECTOR_CHUNKSに与えているパラメータについては、調整の余地が多分にあります。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare | |
C_FILE_ID constant number := :P3_ID; | |
l_text clob; | |
begin | |
select text into l_text from ebmj_files where id = C_FILE_ID; | |
delete from ebmj_chunks where id = C_FILE_ID; | |
insert into ebmj_chunks(file_id, chunk_id, chunk_offset, chunk_length, chunk_data) | |
select C_FILE_ID file_id, rownum chunk_id, t.chunk_offset, t.chunk_length, t.chunk_text chunk_data | |
from vector_chunks( | |
l_text | |
by words | |
max 300 | |
overlap 0 | |
split by recursively | |
language japanese | |
normalize all | |
) t; | |
end; |
子プロセスを追加します。識別の名前はEmbedding、ソースのPL/SQLコードに以下を記述します。チャンクからエンべディングを生成し、表EBMJ_CHUNKSに保存します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare | |
C_FILE_ID constant number := :P3_ID; | |
l_chunks vector_array_t; | |
l_chunk json_object_t; | |
l_vectors vector_array_t; | |
l_embedding json_array_t; | |
begin | |
-- チャンクの配列を作成する | |
l_chunks := vector_array_t(); | |
for r in ( | |
select chunk_id, chunk_offset, chunk_length, chunk_data | |
from ebmj_chunks where file_id = C_FILE_ID order by chunk_id asc | |
) | |
loop | |
l_chunks.extend; | |
l_chunk := json_object_t(); | |
l_chunk.put('chunk_id', r.chunk_id); | |
l_chunk.put('chunk_offset', r.chunk_offset); | |
l_chunk.put('chunk_length', r.chunk_length); | |
l_chunk.put('chunk_data', r.chunk_data); | |
apex_debug.info('l_chunks(%s) = %s', r.chunk_id, r.chunk_data); | |
l_chunks(r.chunk_id) := l_chunk.to_clob(); | |
end loop; | |
-- チャンクの配列からベクトル(エンべディング)の配列を生成する。 | |
l_vectors := dbms_vector_chain.utl_to_embeddings( | |
data => l_chunks | |
,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 | |
) | |
) | |
); | |
-- エンべディングを保存する | |
for i in l_vectors.first .. l_vectors.last | |
loop | |
apex_debug.info('l_vectors(%s) = %s', i, l_vectors(i)); | |
update ebmj_chunks set embed_vector = to_vector(l_vectors(i)) | |
where file_id = C_FILE_ID and chunk_id = i; | |
end loop; | |
end; |
以上で、ファイルのアップロードからエンべディングを生成するまでの、一連の処理が実装できました。
アプリケーションを実行し、いくつかファイルをアップロードします。
その前にページ番号4の表EBMJ_CHUNKSの対話モード・レポートより、列EMBED_VECTORをコメント・アウトします。Oracle APEX 23.2のレポートでは、ベクトル型を扱うことができません。
アプリケーションを実行し、Filesのページを開きます。
作成をクリックします。
Titleを入力し、アップロードするファイルを選択します。
作成をクリックします。
ファイルがアップロードされます。
Chunksを開き、分割されたチャンクを確認します。エンべディングは表示されませんが、エラーが発生していなければ正常に生成されています。また、LM Studioのサーバー・ログからも確認できます。
単純にファイルからテキストを取り出して、単語数(日本語だとほとんど文字数)でチャンクに分割すると、さすがに精度がでない感じで分割されるな、とは思います。
ページ番号4の表EBMJ_CHUNKSの対話モード・レポートで、問合せ文字列との類似検索を実装します。
類似検索を行う文字列を入力するページ・アイテムを作成します。
識別の名前はP4_TEXT、タイプはテキスト・フィールド、ラベルはTextとします。
設定の[Enter]を押すと送信をオンにします。
類似検索を行うように、対話モード・レポートのソースを置き換えます。
ソースのタイプをSQL問合せに変更し、SQL問合せとして以下を記述します。FETCH句の部分が決めうちになっています。類似したチャンクを検索するにあたって、調整が必要になるでしょう。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
select | |
id | |
,file_id | |
,chunk_id | |
,chunk_offset | |
,chunk_length | |
,chunk_data | |
from ebmj_chunks | |
order by | |
vector_distance( | |
( | |
select | |
dbms_vector_chain.utl_to_embedding( | |
data => :P4_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 | |
) | |
) | |
) | |
) | |
,embed_vector | |
) asc | |
fetch first 2 partitions by file_id, 2 rows only |
ページ・アイテムP4_TEXTが空白のときは、SELECT文でエラーが発生します。そのため、サーバー側の条件のタイプにアイテムはNULLではないを選択し、アイテムにP4_TEXTを指定します。
以上で、ページ・アイテムP4_TEXTに入力した文字列に類似しているチャンクを検索できます。
選択したエンべディング・モデルが日本語に対応していないのだろうと思いますが、あまり関連の無さそうなチャンクが検索されています。LM Studioで利用可能なエンべディング・モデルに選択肢がほぼ無いので、仕方がありません。実用を考えると、OpenAIやCohereといった外部サービスを呼び出す必要があるでしょう。
最後にホーム・ページに、問合せ文字列から類似検索したチャンクを含めたプロンプトを作成し、言語モデルを呼び出して回答文を生成する機能を実装します。
ページ・デザイナで、ページ番号1のホーム・ページを開きます。
Body以下にあるリージョンのページ・ナビゲーションを削除します。
問合せ文字列を入力するページ・アイテムを作成します。
識別の名前はP1_QUERY、タイプはテキスト・フィールド、ラベルはQueryとします。
設定の[Enter]を押すと送信をオンにします。
言語モデルを呼び出して生成した回答文を表示するページ・アイテムを作成します。
識別の名前はP1_ANSWER、タイプはテキスト領域、ラベルはAnswerとします。セッション・ステートのデータ型としてCLOBを選択します。
デバッグのために、回答文の生成に使用したプロンプトを表示するページ・アイテムを作成します。
識別の名前はP1_PROMPT、タイプはテキスト領域、ラベルはPromptとします。セッション・ステートのデータ型としてCLOBを選択します。
プロセス・ビューを開き、問合せ文字列から回答を生成するプロセスを作成します。
識別の名前はGenerate Answerとします。タイプはコードの実行、ソースのPL/SQLコードとして、以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare | |
l_vector_q vector; | |
l_prompt clob; | |
l_answer clob; | |
l_norm clob; | |
begin | |
-- 問合せ文字列のエンべディングを取得する | |
l_vector_q := dbms_vector_chain.utl_to_embedding( | |
data => :P1_QUERY | |
,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 | |
) | |
) | |
); | |
-- プロンプトを生成する | |
l_prompt := l_prompt || '以下に与える情報を元に、最後の質問に回答してください。'; | |
l_prompt := l_prompt || apex_application.CRLF; | |
-- ベクトル検索を行って、プロンプトに検索されたチャンクを埋め込む | |
for r in ( | |
select chunk_data | |
from ebmj_chunks | |
order by vector_distance(l_vector_q, embed_vector) asc | |
fetch first 2 partitions by file_id, 2 rows only | |
) | |
loop | |
l_prompt := l_prompt || '--------------------------'; | |
l_prompt := l_prompt || apex_application.CRLF; | |
l_norm := r.chunk_data; | |
-- CRとLFを取り除く。 | |
l_norm := replace(l_norm, chr(10)); | |
l_norm := replace(l_norm, chr(13)); | |
l_prompt := l_prompt || l_norm; | |
l_prompt := l_prompt || apex_application.CRLF; | |
end loop; | |
l_prompt := l_prompt || '--------------------------'; | |
l_prompt := l_prompt || apex_application.CRLF; | |
l_prompt := l_prompt || :P1_QUERY; | |
-- 回答を生成する | |
apex_debug.info(l_prompt); | |
utl_http.set_body_charset('utf-8'); | |
l_answer := dbms_vector_chain.utl_to_generate_text( | |
data => l_prompt | |
,params => JSON(json_object( | |
key 'provider' value 'OpenAI' | |
,key 'credential_name' value 'OPENAI_CRED' | |
,key 'url' value 'http://host.docker.internal:8080/v1/chat/completions?' | |
,key 'model' value 'gpt-3.5-turbo' | |
,key 'transfer_timeout' value 160 | |
,key 'max_tokens' value 260 | |
-- ,key 'temerature' value 1.0 | |
) | |
) | |
); | |
:P1_ANSWER := l_answer; | |
-- デバッグ用にプロンプトを表示する | |
:P1_PROMPT := l_prompt; | |
end; |
サーバー側の条件のタイプにアイテムはNULLではないを選択し、アイテムとしてP1_QUERYを指定します。
以上でアプリケーションは完成です。アプリケーションを実行すると、記事の先頭のGIF動画のように動作します。
ほとんど質問と関連のないチャンクがプロンプトに含まれているにもかかわらず、それっぽい回答文が生成されるので、少々驚きました。言語モデルにELYZAを選択しているからかもしれません。
今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/sample-vector-search.zip
Oracle APEXのアプリケーション作成の参考になれば幸いです。
完