2024年5月27日月曜日

Oracle Database 23aiのベクトル検索を使ったOracle APEXアプリを作る

Oracle Database 23aiのベクトル検索を使うAPEXのサンプル・アプリを作ってみました。以下の処理を実装しています。
  1. ファイルをアップロードする。
  2. アップロードしたファイルからテキストを取り出す。
  3. 取り出したテキストをチャンクに分割する。
  4. 分割したチャンクよりエンべディングを生成する。
  5. 問合せ文字列から、類似度の高いチャンクを一覧する。
  6. 問合せ文字列と類似度の高いチャンクをプロンプトに組み込み、言語モデルを使って回答文を生成する。
アプリケーションは基本的なワークフローの実装を目的としていて、実用性や精度は求めません。すべてローカルで実行し、費用の発生やセキュリティ上の心配は不要にします。

作成したアプリケーションは以下のように動作します。


アプリケーションの作成と実行には、以下の環境を使っています。
  1. エンべディングの生成と言語モデルの実行に、LM Studioを使います。
  2. 言語モデルとして、mmnga/ELYZA-japanese-Llama-2-7b-fast-instruct-ggufをロードします。
  3. エンべディングには、nomic-ai/nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.f16.ggufをロードします。
  4. Oracle Database 23aiはMacbook上のDocker/Colimaのx86_64エミュレーション環境で実行します。非常に遅いです。
  5. 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は以下になります。



後はDDLを実行し、2つの表を作成します。


アプリケーションの作成を開始します。

名前Sample Vector Searchとし、アプリケーションの作成ウィザードを使用します。


アプリケーション作成ウィザードが開きます。

ページの追加をクリックし、表EBMJ_FILESフォーム付き対話モード・レポートのページを追加します。


対話モード・レポートを選択します。


ページ名Filesとします。表またはビューとしてEBMJ_FILESを選択し、フォームを含めるチェックを入れます。

ページの追加をクリックします。


EBMJ_FILESをソースとしたフォーム付き対話モード・レポートのページが追加されます。


同様の手順で、表EBMJ_CHUNKS対話モード・レポートのページを追加します。手作業での編集は行わないため、フォームは付けません。

ページ名Chunks表またはビューとしてEBMJ_CHUNKSを選択します。

ページの追加をクリックします。


以上のページ構成で、アプリケーションの作成をクリックします。


アプリケーションが作成されます。

これから、それぞれのページに修正を加えていきます。

最初にページ番号の表EBMJ_FILES対話モード・レポートを修正します。

ページ・デザイナで開きます。


アップロードしたファイルから取り出したテキストを保存する列TEXTは長文なので、レポートでは表示できません。コメント・アウトして変更を保存します。


ページ番号の、表EBMJ_FILESを対象としたフォームのページを開きます。

こちらも列TEXTをソースとしたページ・アイテムP3_TEXTコメント・アウトします。


アップロードしたファイルより、テキストの取り出し、チャンク分割、エンべディングの生成までを行うプロセスを実装します。

左ペインでプロセス・ビューを表示します。プロセス作成します。

識別名前Extract, Chunking and Embeddingとします。タイプとして実行チェーンを選択します。

サーバー側の条件タイプリクエストは値に含まれるを選択し、としてCREATE SAVEを指定します。


作成した実行チェーンに子プロセスを追加します。

識別名前ExtractソースPL/SQLコードとして以下を記述します。アップロードしたファイルを列CONTENTから読み出し、取り出したテキストを列TEXTに保存します。



子プロセスを追加します。識別名前ChunkingソースPL/SQLコードとして以下を記述します。ファイルから取り出したテキストをチャンクに分割し、表EBMJ_CHUNKSに保存します。ファンクションVECTOR_CHUNKSに与えているパラメータについては、調整の余地が多分にあります。



子プロセスを追加します。識別名前EmbeddingソースPL/SQLコードに以下を記述します。チャンクからエンべディングを生成し、表EBMJ_CHUNKSに保存します。



以上で、ファイルのアップロードからエンべディングを生成するまでの、一連の処理が実装できました。

アプリケーションを実行し、いくつかファイルをアップロードします。

その前にページ番号の表EBMJ_CHUNKSの対話モード・レポートより、列EMBED_VECTORコメント・アウトします。Oracle APEX 23.2のレポートでは、ベクトル型を扱うことができません。


アプリケーションを実行し、Filesのページを開きます。

作成をクリックします。


Titleを入力し、アップロードするファイルを選択します。

作成をクリックします。


ファイルがアップロードされます。


Chunksを開き、分割されたチャンクを確認します。エンべディングは表示されませんが、エラーが発生していなければ正常に生成されています。また、LM Studioのサーバー・ログからも確認できます。

単純にファイルからテキストを取り出して、単語数(日本語だとほとんど文字数)でチャンクに分割すると、さすがに精度がでない感じで分割されるな、とは思います。


ページ番号4の表EBMJ_CHUNKS対話モード・レポートで、問合せ文字列との類似検索を実装します。

類似検索を行う文字列を入力するページ・アイテムを作成します。

識別名前P4_TEXTタイプテキスト・フィールドラベルTextとします。

設定[Enter]を押すと送信オンにします。


類似検索を行うように、対話モード・レポートのソースを置き換えます。

ソースタイプSQL問合せに変更し、SQL問合せとして以下を記述します。FETCH句の部分が決めうちになっています。類似したチャンクを検索するにあたって、調整が必要になるでしょう。

ページ・アイテムP4_TEXTが空白のときは、SELECT文でエラーが発生します。そのため、サーバー側の条件タイプアイテムはNULLではないを選択し、アイテムP4_TEXTを指定します。


以上で、ページ・アイテムP4_TEXTに入力した文字列に類似しているチャンクを検索できます。

選択したエンべディング・モデルが日本語に対応していないのだろうと思いますが、あまり関連の無さそうなチャンクが検索されています。LM Studioで利用可能なエンべディング・モデルに選択肢がほぼ無いので、仕方がありません。実用を考えると、OpenAIやCohereといった外部サービスを呼び出す必要があるでしょう。


最後にホーム・ページに、問合せ文字列から類似検索したチャンクを含めたプロンプトを作成し、言語モデルを呼び出して回答文を生成する機能を実装します。

ページ・デザイナで、ページ番号ホーム・ページを開きます。

Body以下にあるリージョンのページ・ナビゲーションを削除します。


問合せ文字列を入力するページ・アイテムを作成します。

識別名前P1_QUERYタイプテキスト・フィールドラベルQueryとします。

設定[Enter]を押すと送信オンにします。


言語モデルを呼び出して生成した回答文を表示するページ・アイテムを作成します。

識別名前P1_ANSWERタイプテキスト領域ラベルAnswerとします。セッション・ステートデータ型としてCLOBを選択します。


デバッグのために、回答文の生成に使用したプロンプトを表示するページ・アイテムを作成します。

識別名前P1_PROMPTタイプテキスト領域ラベルPromptとします。セッション・ステートデータ型としてCLOBを選択します。


プロセス・ビューを開き、問合せ文字列から回答を生成するプロセスを作成します。

識別名前Generate Answerとします。タイプコードの実行ソースPL/SQLコードとして、以下を記述します。


サーバー側の条件タイプアイテムはNULLではないを選択し、アイテムとしてP1_QUERYを指定します。


以上でアプリケーションは完成です。アプリケーションを実行すると、記事の先頭のGIF動画のように動作します。

ほとんど質問と関連のないチャンクがプロンプトに含まれているにもかかわらず、それっぽい回答文が生成されるので、少々驚きました。言語モデルにELYZAを選択しているからかもしれません。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/sample-vector-search.zip

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