Oracle Database 23ai FreeとAPEXでRAGを使った生成AIアプリをローコード開発してみた (アプリ実装 前編)

前回は23aiとAPEXでRAGアプリを実装するための事前準備について触れました。
[Oracle Database 23ai FreeとAPEXでRAGを使った生成AIアプリをローコード開発してみた (事前準備編)](https://qiita.com/ssfujita/items/34d9e3c39ca730d29351)
今回はアプリ実装を進めていきますが、やや長いので前編と後編に分けたいと思います。
実装する機能は以下を予定しています。
+ 前編：任意のファイル (PDFやOfficeファイル) をアップロードしてEmbeddingするページ
+ 後編：質問文でベクトル検索し、質問文と検索結果をもとにした生成AIの回答を表示するページ

なおアプリのexportファイルを以下に置いています。
https://github.com/mago1chi/apex/blob/main/ragapp_cohere.sql
今回のアプリ実装は作業数が多いため、すぐに動くものを見たい方は上記ファイルをご自分のAPEX環境にimportしてお試しください。
その場合でも、以下記事の作業は事前に行う必要がありますのでご注意ください。
+ [Oracle Database 23ai FreeとAPEXでRAGを使った生成AIアプリをローコード開発してみた (事前準備編)](https://qiita.com/ssfujita/items/34d9e3c39ca730d29351)
+ 本記事の「ワークスペースの作成」までを実施します。
+ [Oracle Database 23ai FreeとAPEXでRAGを使った生成AIアプリをローコード開発してみた (アプリ実装 後編)](https://qiita.com/ssfujita/items/53df4f342e28c238a5a2)
    - ｢Web資格証明の設定」までを実施します。

前編で作成するページは以下のようなイメージになります。

登録済みドキュメント一覧
![WS000088.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/9934945c-c821-ba19-f596-ad26ec17cebf.jpeg)

ドキュメント登録フォーム
![WS000033.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/2df4e2fe-9887-1f84-e431-1571b113e0dc.jpeg)

フォームに入力してSubmitすると、裏で以下の処理を行います。
+ 非構造化データとメタデータを表に保存
+ 非構造化データからのテキスト抽出、チャンク分割を実施
+ チャンク分割したテキストをEmbeddingモデルによってベクトル化し、表にVector型で保存

以降は手順を記載しつつ、実装に使うベクトル検索機能 (AI Vector Search) の説明も簡単に挟みたいと思います。

## ワークスペースの作成

まずはAPEX上にワークスペースを作成します。
APEXではワークスペースという論理的なオブジェクト内にアプリケーションを定義します。
前回作成したAPEXの管理画面に、ORDS経由でアクセスします。

```
http://your_public_ip/ords/r/apex/workspace-sign-in/oracle-apex-sign-in
```

`your_public_ip` の箇所はご自分のサーバに割り当てられたパブリックIPを指定します。

ページ下段にある「管理」を選択します。
![WS000000.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/7c553be9-bf56-b452-d31e-b044a55aaea0.jpeg)

APEX管理ユーザでログインします。
![WS000002.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/72e2bdcd-63aa-f962-65f7-434b7d7997cd.jpeg)

｢ワークスペースの作成」を選択します。
![WS000003.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/587084c5-16be-5072-2a9b-7d82974d6e6c.jpeg)

ワークスペース名を入力し「次」を選択します。
![WS000004.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/973196bf-3881-52b0-91ac-afdd25b07ca7.jpeg)

スキーマ名には前回作成した「VECTOR」を選び「次」を選択します。
![WS000005.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/baade4d2-8792-2e68-00e4-dbd06debdf9e.jpeg)

ワークスペース用の管理ユーザ名、パスワード、電子メールを入力し「次」を選択します。
![WS000006.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/37e07d94-9594-5bc7-9347-9fc4bef17c86.jpeg)

｢ワークスペースの作成」を選択します。
![WS000007.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/728f29da-ba66-4352-8fe5-519b759a579a.jpeg)

先ほど作成したワークスペース名、管理ユーザ名、パスワードを入力し「サインイン」を選択します。
![WS000008.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/370b9bee-df2e-d1d5-1257-26f0a4604fb2.jpeg)

## アプリに使うオブジェクトの作成

DDLを流してアプリに使うDBオブジェクトを作成します。
ワークスペースのトップ画面で「SQLワークショップ」にある「SQLコマンド」を選択します。
![WS000009.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/c7aa2f42-0839-5a6d-676b-3c4b407675ea.jpeg)

画面上段に表作成のDDLを入力し「実行」を選択します。
![WS000010.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/09a83375-0e92-a1d8-7af8-636b09fdc309.jpeg)
作成する表は「DOCUMENTS表」「EMBED表」の2つです。
DDLは以下になります。

```sql
CREATE TABLE "DOCUMENTS" 
 (	"ID" NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 1 START WITH 1 CACHE 20 NOORDER  NOCYCLE  NOKEEP  NOSCALE  NOT NULL ENABLE, 
  "NAME" VARCHAR2(256) NOT NULL ENABLE, 
  "CATEGORY" VARCHAR2(512) NOT NULL ENABLE,
  "DOC" BLOB, 
  "DOC_FILENAME" VARCHAR2(512), 
  "DOC_MIMETYPE" VARCHAR2(512), 
  "DOC_LASTUPD" DATE, 
   CONSTRAINT "DOCUMENTS_PK" PRIMARY KEY ("ID") USING INDEX  ENABLE
 );

CREATE TABLE "EMBED" 
 (	"DOC_ID" NUMBER, 
  "EMBED_ID" NUMBER  GENERATED BY DEFAULT ON NULL AS IDENTITY MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 1 START WITH 1 CACHE 20 NOORDER  NOCYCLE  NOKEEP  NOSCALE  NOT NULL ENABLE, 
  "EMBED_DATA" VARCHAR2(4000), 
  "EMBEDDING" VECTOR,
  CONSTRAINT "EMBED_PK" PRIMARY KEY ("EMBED_ID") USING INDEX  ENABLE,
  CONSTRAINT "DOC_FK" FOREIGN  KEY (DOC_ID)
  REFERENCES DOCUMENTS(ID) ON DELETE CASCADE
 );
```

## 土台となるアプリケーションの作成

APEXには定義済みDBオブジェクトから自動でアプリケーションを作成する機能があります。
今回はその機能を使い、土台となるアプリケーションを作ってしまいます。
前項で2つの表を作成できたら「SQLワークショップ」にある「オブジェクト・ブラウザ」を選択します。
![WS000011.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/f9e2a6c5-fa3d-5983-c1f3-7eeb40ecf1db.jpeg)

左ペインの「表」を展開すると「DOCUMENTS」「EMBED」があり、「DOCUMENTS」を選択します。
右ペインの「その他」にある「アプリケーションの作成」を選択します。
![WS000012.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/4c117576-df85-e56f-3a2e-51cb7a88352c.jpeg)

｢アプリケーションの作成」画面に遷移したら「名前」を任意に修正します。
また「ページ」にはデフォルトでいくつかのページが用意されていますが、こちらを編集します。
![WS000013.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/7fed8838-def0-61d6-9889-6f4bc372ba86.jpeg)

｢Documents検索」「カレンダ」は使いませんので「編集」ボタンを押下し「削除」を選択します。
![WS000014.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/3f183a61-2a37-a600-2c72-f56fcbae136f.jpeg)

また「ページの追加」を選択し、「RAG」という名前の「空白ページ」を追加します。
![WS000015.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/c0e89e76-6318-f5ed-c2ae-334d778c9581.jpeg)

最終形は以下のようになります。
問題なければ「アプリケーションの作成」を選択します。
![WS000016.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/1f414db8-76f4-166c-4ded-ec64e1ee6eb9.jpeg)

これで土台となるアプリケーションが出来ましたので、こちらを編集し機能追加していきます。

## ユーザ・インターフェイスの編集

ドキュメント登録するフォームのUIを編集します。
本アプリではアップロードする非構造化データ本体に加え、メタデータ情報としてファイルのカテゴリ、作成日も登録できるようにします。

前項で「アプリケーションの作成」を選択し無事にアプリケーションが出来ましたら、アプリケーション・ビルダーのトップ画面に遷移します。
本画面の「3 - Document」を選択し、ページ・デザイナへアクセスします。
![WS000017.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/cb018b8a-d61b-f4ad-2e67-35a36b9af8fc.jpeg)

表示された画面の右ペイン赤枠の3項目を編集します。
本ページはドキュメント登録時に使うフォームのため「ドキュメント登録フォーム」と命名しています。
![WS000018.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/fc1ae882-f454-64c4-3d30-6dd29bd09439.jpeg)

左ペインの「P3_NAME」を選択し、右ペイン赤枠を画像の通りに編集します。
これでアップロードするファイルの登録名を入力するフォームが、テキスト・フィールドになります。
![WS000019.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/d0b6b2bd-2739-79dc-7184-a58e8ce8de91.jpeg)

左ペインの「P3_CATEGORY」を選択し、右ペイン赤枠を画像の通りに編集します。
![WS000020.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/b3a1c343-edaf-4408-bb89-567f563ff5f7.jpeg)

右ペインを下へスクロールし、赤枠を画像の通りに編集します。
![WS000021.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/1304bdb8-2de2-4a91-884c-3974839ccba3.jpeg)

｢SQL問合せ」で入力するSQLは以下の通りです。

```sql
SELECT category d, category v FROM documents GROUP BY CATEGORY ORDER BY 1;
```

これでカテゴリ入力フォームは選択リストと手動入力が利用できるようになります。
既存カテゴリから選ぶ場合は選択リストを、新規カテゴリを作成する場合は手動入力します。

左ペインの「P3_DOC」を選択し、右ペイン赤枠を画像の通りに編集します。
![WS000022.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/287f253f-ea08-d77a-e0a5-83fcba32d161.jpeg)

右ペインを下へスクロールし、赤枠を画像の通りに編集します。
![WS000022-2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/dc99d74b-68ff-4f1b-0194-e3716b7c3ced.jpeg)

上記設定は後述するPL/SQLによる内部処理で利用します。
具体的には、アップロードされた非構造化データを `APEX_APPLICATION_TEMP_FILES` という一時的な表に格納させ、DML文を作成する際に利用します。

左ペインの「P3_DOC_LASTUPD」を選択し、「P3_DOC」より上に移動します。
さらに右ペイン赤枠を画像の通りに編集します。
![WS000023.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/7ff6d33a-fb59-3975-461d-1a230f445f84.jpeg)

右ペインを下にスクロールし、赤枠を画像の通りに編集します。
![WS000024.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/4c8dc373-02e8-1f15-b252-e8639f84eed1.jpeg)

これで「ファイル作成日」を必須項目にします。
さらに右ペインを下にスクロールし、赤枠を画像の通りに編集します。
![WS000024-2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/0981064a-5de4-dad0-e726-dd4fc278381b.jpeg)

｢SQL問合せ」に入力するSQLは以下の通りです。

```sql
SELECT sysdate FROM dual;
```

これで「ファイル作成日」のデフォルト値として、その日の日付を入れる仕様にします。

## PL/SQLを使ったデータ保存処理の実装

ページ・デザイナの「プロセス」タブ (矢印が円になっているアイコン) を選択します。
｢プロセス」に既存で作成されている「プロセス・フォームDocument」を右クリックし「削除」を選択します。
![WS000027.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/00966b98-0b96-c9fb-12f3-dd65ef3c8cfa.jpeg)

データ保存処理はデフォルトで用意されているプロセスではなく、新たに作成するプロセスで実装します。

削除したら「プロセス」を右クリックし「プロセスの作成」を選択します。
![WS000028.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/2f09b7ad-32f2-904c-a7da-c4b49ce99ae2.jpeg)

右ペイン赤枠を画像の通りに編集します。
![WS000029.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/3d506683-6174-be23-b942-1cb7ddc9d6c4.jpeg)
![WS000030.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/a4f7637c-8318-b8c1-4fde-dc663e9fc1f7.jpeg)

｢PL/SQLコード」はエディタを開き、以下PL/SQLコードを入力します。
本処理によりアップロードされた非構造化データとメタデータの保存、および非構造化データから抽出したテキストのEmbedding結果の保存が行われます。

```sql
DECLARE
    l_file apex_application_temp_files%rowtype;
    chunk_params clob;
    cohere_params clob;
BEGIN
    -- チャンク分割する際のパラメータ
    chunk_params := '
    {
      "max": "200",
      "overlap": "40"
    }';
    
    -- Cohereの embed-multilingual-v3.0 を使ってEmbeddingするためのパラメータ
    cohere_params := '
    { 
      "provider": "cohere",
      "credential_name": "COHERE_CRED", 
      "url": "https://api.cohere.ai/v1/embed",
      "model": "embed-multilingual-v3.0",
      "input_type": "search_query"
    }';
    
    -- アップロードするファイルが未指定の場合、メタデータのみ保存して終了
    IF :P3_DOC IS NULL THEN
        INSERT INTO DOCUMENTS (name, category, doc_lastupd) VALUES (:P3_NAME, :P3_CATEGORY, :P3_DOC_LASTUPD);
    ELSE
        -- 非構造化データの情報を抽出
        SELECT * INTO l_file
            FROM APEX_APPLICATION_TEMP_FILES
            WHERE name IN 
            (
                SELECT column_value
                FROM APEX_STRING.SPLIT(:P3_DOC, ':')
            );
    
        -- DOCUMENTS表に非構造化データ、メタデータを保存
        INSERT INTO DOCUMENTS (name, category, doc, doc_filename, doc_mimetype, doc_lastupd)
            VALUES (:P3_NAME, :P3_CATEGORY, l_file.BLOB_CONTENT, l_file.FILENAME, l_file.MIME_TYPE, :P3_DOC_LASTUPD);
        
        -- チャンク分割したテキストをEMBED表に保存
        INSERT INTO EMBED (doc_id, embed_data)
            SELECT dt.id, chunk_t.chunk_data
            FROM
            documents dt,
            DBMS_VECTOR_CHAIN.UTL_TO_CHUNKS(REPLACE(DBMS_VECTOR_CHAIN.UTL_TO_TEXT(dt.doc), CHR(10)), JSON(chunk_params)) chunk_jt,
            JSON_TABLE(chunk_jt.column_value, '$[*]' COLUMNS (chunk_id NUMBER PATH
            '$.chunk_id', chunk_data VARCHAR2(4000) PATH '$.chunk_data')) chunk_t
            WHERE dt.name = :P3_NAME;
        
        -- EMBED表に新規保存された各テキストについて、Cohereの embed-multilingual-v3.0 を使ってEmbeddingし、EMBED表に保存
        FOR rec IN (SELECT em.embed_id, em.embed_data
            FROM documents doc, embed em
            WHERE doc.name = :P3_NAME AND doc.id = em.doc_id)
        LOOP
            UPDATE embed
            SET
                embedding =DBMS_VECTOR.UTL_TO_EMBEDDING(rec.embed_data, JSON(cohere_params))
            WHERE embed_id = rec.embed_id;
        END LOOP; 
    END IF;
    
    COMMIT;
END;
```

コードの大まかな内容はコメントをご確認ください。
AI Vector Searchの機能を使っている箇所は別途解説したいと思います。
まず 45行目 のプロシージャは AI Vector Search 用に23aiで新たに追加されたプロシージャを使っています。

```sql
DBMS_VECTOR_CHAIN.UTL_TO_CHUNKS(REPLACE(DBMS_VECTOR_CHAIN.UTL_TO_TEXT(dt.doc), CHR(10)), JSON(chunk_params)) chunk_jt,
```

- `DBMS_VECTOR_CHAIN.UTL_TO_TEXT`: 引数に指定された非構造化データからテキストを抽出
- `DBMS_VECTOR_CHAIN.UTL_TO_CHUNKS`: 引数に指定されたテキストをチャンクに分割
    - チャンク分割する際のパラメータは 7～11行目 にJSON形式で定義
    - 本例では200文字ごとにチャンク分割し、40文字のオーバーラップを設定

57行目も AI Vector Search 用に23aiで追加されたプロシージャを使っています。

```sql
embedding =DBMS_VECTOR.UTL_TO_EMBEDDING(rec.embed_data, JSON(cohere_params))
```

- `DBMS_VECTOR.UTL_TO_EMBEDDING`: 第一引数のテキストを、第二引数に指定したモデルを使ってEmbedding
    - Embedding用のパラメータは 14～21行目 にJSON形式で定義
    - パラメータ内の `credential_name` には事前準備の中で作成した資格証明オブジェクトのオブジェクト名を指定
    - 同様の方法でOpenAIが提供するEmbeddingモデルも利用可

コード入力できましたら、右ペインを下にスクロールし、赤枠を画像の通りに編集します。
![WS000031.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/4d10b583-2046-f85a-318d-22b290163e91.jpeg)

これでフォーム内の「作成」ボタンを押下すると、先ほど定義したPL/SQLコードが実行されます。

作成したUIと処理を試してみます。
ページ・デザイナ上段にある赤枠のアイコンを選択し、「ページ・ファインダ」から「2」を選択します。
![WS000032.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/20126652-0e12-883b-43be-9aae3ef0f69a.jpeg)

ページ・デザイナ右上の緑色のボタンを押下します。
![WS000032-2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/f6ce4b58-6227-d79f-fcc0-a8beb7b7a5c0.jpeg)

アプリのログイン画面が表示されるので、ワークスペース管理ユーザでログインします。
![WS000032-3.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/c1ef8de2-94dc-3211-2f1b-1fda87eff302.jpeg)

｢ドキュメント登録」を選択します。
![WS000032-4.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/b7362de1-ddc2-eb17-a265-eec948d20ab5.jpeg)

右側の「作成」ボタンを押下します。
![WS000032-5.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/e1d66097-dbfb-4215-c813-8d8427baecfc.jpeg)

好きなファイル (PDFやOfficeファイル) をアップロードします。
登録名やメタデータ情報も任意に入力してください。
一通り入力できたら「作成」を選択します。
※ちなみにPDFによっては日本語が文字化けしてしまうケースがあります。
![WS000033.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/2df4e2fe-9887-1f84-e431-1571b113e0dc.jpeg)

すると以下のように「ドキュメント登録」画面に戻り、追加したファイルの情報が表示されます。
![WS000034.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/4d6479e4-6bcd-cbc0-6f9b-c77f34b87751.jpeg)

本画面にファイル作成日なども表示したいので、「アクション」から「列」を選択し、表示列の編集を行います。
![WS000035.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/bfdba66c-f071-3cd2-a804-0f4d267499fa.jpeg)

画像赤枠のように設定し「適用」を選択します。
![WS000036.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/3b272301-cc7b-4eed-4138-cb0297bd4b90.jpeg)

するとファイル作成日などの情報も表示されるようになりました。
このフォーマットを永続化するため、「アクション」の「レポート」にある「レポートの保存」を選択します。
![WS000037.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/01000ce4-a9ec-8265-4439-eacdec610828.jpeg)

｢デフォルト・レポートの保存」において画像赤枠のように設定し、「適用」を選択します。
![WS000038.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/333dd1e1-46a4-ad6c-ebcc-4d006d9a274b.jpeg)

これで表示フォーマットが保存されました。
ついでに表のカラム名も編集しておきます。
ページ2 (ドキュメント登録) のページ・デザイナを開き、左ペインの「NAME」を選択します。
その状態で右ペイン赤枠を画像の通りに編集します。
![WS000038-2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/e56b6a0d-3c17-3434-3d7c-e0cb252a83e0.jpeg)

同様に「CATEGORY」「DOC」「DOC_FILENAME」「DOC_LASTUPD」も編集します。
最終的に「ドキュメント登録」画面のカラム名は以下のようになります。
![WS000038-3.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/cf4435f3-06cc-54fd-3348-700e13df6046.jpeg)

ここで試しに、先ほど登録したファイルがちゃんとチャンク分割され、Embedding結果が保存されているか確認してみます。
｢SQLワークショップ」の「SQLコマンド」を選択します。
![WS000039.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/b393417a-1d39-db6a-f56f-d8e845df8c87.jpeg)

上段に以下のSQLを入力し「実行」を選択します。

```sql
SELECT doc_id, embed_id, embed_data, embedding FROM embed ORDER BY embed_id
    FETCH FIRST 2 ROWS ONLY;
```

下記画像のように、チャンク分割されたテキストと対応するベクトルデータが表示され、想定通り処理されていることが分かります。
![WS000040.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/fedf9dfc-9f3c-4327-0226-2e82be67f086.jpeg)

続いて「作成」処理と同様に「更新」処理も追加します。
｢更新」は一度登録したドキュメントのファイル本体やメタデータ情報を変更し、保存する処理です。

ページ3 (ドキュメント登録フォーム) のプロセスタブに戻り、新しいプロセスを追加します。
※左ペインのプロセスの順序は「ダイアログを閉じる」より上に来るよう注意してください。
右ペイン赤枠を画像の通りに編集します。
![WS000041.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/a88b465d-8979-baab-2b43-8c954af1b989.jpeg)

｢PL/SQLコード」に入力するPL/SQLは以下の通りです。

```sql
DECLARE
    l_file apex_application_temp_files%rowtype;
    chunk_params clob;
    cohere_params clob;
BEGIN
    -- チャンク分割する際のパラメータ
    chunk_params := '
    {
      "max": "200",
      "overlap": "40"
    }';
    
    -- Cohereの embed-multilingual-v3.0 を使ってEmbeddingするためのパラメータ
    cohere_params := '
    { 
      "provider": "cohere",
      "credential_name": "COHERE_CRED", 
      "url": "https://api.cohere.ai/v1/embed",
      "model": "embed-multilingual-v3.0",
      "input_type": "search_query"
    }';
    
    -- アップロードするファイルが未指定の場合、メタデータのみ更新して終了
    IF :P3_DOC IS NULL THEN
        UPDATE DOCUMENTS 
        SET 
            name = :P3_NAME,
            category = :P3_CATEGORY,
            doc_lastupd = :P3_DOC_LASTUPD
        WHERE
            id = :P3_ID;
    ELSE
        -- 非構造化データの情報を抽出
        SELECT * INTO l_file
            FROM APEX_APPLICATION_TEMP_FILES
            WHERE name IN 
            (
                SELECT column_value
                FROM APEX_STRING.SPLIT(:P3_DOC, ':')
            );
        
        -- DOCUMENTS表の該当行について非構造化データ、メタデータを更新
        UPDATE DOCUMENTS 
        SET 
            name = :P3_NAME,
            category = :P3_CATEGORY,
            doc = l_file.BLOB_CONTENT,
            doc_filename = l_file.FILENAME,
            doc_mimetype = l_file.MIME_TYPE,
            doc_lastupd = :P3_DOC_LASTUPD
        WHERE
            id = :P3_ID;
        
        -- EMBED表に保存されている更新ドキュメントに対応する情報を一度すべて削除
        DELETE FROM embed WHERE doc_id = :P3_ID;
        
        -- チャンク分割したテキストをEMBED表に保存
        INSERT INTO EMBED (doc_id, embed_data)
            SELECT dt.id, chunk_t.chunk_data
            FROM
            documents dt,
                DBMS_VECTOR_CHAIN.UTL_TO_CHUNKS(REPLACE(DBMS_VECTOR_CHAIN.UTL_TO_TEXT(dt.doc), CHR(10)), JSON(chunk_params)) chunk_jt,
            JSON_TABLE(chunk_jt.column_value, '$[*]' COLUMNS (chunk_id NUMBER PATH
            '$.chunk_id', chunk_data VARCHAR2(4000) PATH '$.chunk_data')) chunk_t
            WHERE dt.name = :P3_NAME;
        
        -- EMBED表に再保存された各テキストについて、Cohereの embed-multilingual-v3.0 を使ってEmbeddingし、EMBED表に保存
        FOR rec IN (SELECT em.embed_id, em.embed_data
            FROM documents doc, embed em
            WHERE doc.name = :P3_NAME AND doc.id = em.doc_id)
        LOOP
            UPDATE embed
            SET
                embedding =DBMS_VECTOR.UTL_TO_EMBEDDING(rec.embed_data, JSON(cohere_params))
            WHERE embed_id = rec.embed_id;
        END LOOP; 
    END IF;
    
    COMMIT;
END;
```

コードの大まかな内容はコメントをご確認ください。
また上記コード内で使用している AI Vector Search 関連のプロシージャは、一度解説した以下のみ利用しています。
- `DBMS_VECTOR_CHAIN.UTL_TO_TEXT`: 引数に指定された非構造化データからテキストを抽出
- `DBMS_VECTOR_CHAIN.UTL_TO_CHUNKS`: 引数に指定されたテキストをチャンクに分割
- `DBMS_VECTOR.UTL_TO_EMBEDDING`: 第一引数のテキストを、第二引数に指定したモデルを使ってEmbedding

コード入力できましたら、右ペインを下にスクロールし、赤枠を画像の通りに編集します。
![WS000042.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/777df0f3-de69-eaf8-8fa6-4f6755fb7ff4.jpeg)

これでフォーム内の「変更の適用」ボタンを押下すると、先ほど定義したPL/SQLコードが実行されます。
試しに先ほど登録したファイルの鉛筆マークを選択し、編集してみます。
![WS000043.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/32f62ba0-ff7b-cb09-c776-5b451f927c19.jpeg)
![WS000044.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/d64d6295-086b-5623-a311-e48eae2eaecc.jpeg)

問題なく変更した内容が表示されることを確認します。
気になる場合は再度「SQLコマンド」からSQLを発行し、保存されたデータを確認してみてください。
![WS000045.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/1604537c-b82d-6568-f677-3c8182602379.jpeg)

最後に「削除」機能を追加します。
これで登録済みファイル本体とメタデータ情報を削除できるようにします。

ページ3 (ドキュメント登録フォーム) のプロセスタブに戻り、新しいプロセスを追加します。
※左ペインのプロセスの順序は「ダイアログを閉じる」より上に来るよう注意してください。
右ペイン赤枠を画像の通りに編集します。
![WS000046.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/e990bd37-9db0-04b7-670b-ed56c1ee74c7.jpeg)

｢PL/SQLコード」に入力するPL/SQLは以下の通りです。

```sql
BEGIN
    DELETE FROM documents WHERE id = :P3_ID;
    COMMIT;
END;
```

上記ではDOCUMENTS表のみ削除していますが、これでEMBED表の対応行も削除されます。
EMBED表にはDOCUMENTS表への外部参照キーに対して `ON DELETE CASCADE` が設定されています。
そのためDOCUMENTS表の対象行を削除すると、対象ドキュメントに紐づくEMBED表の行も削除されます。

コード入力できましたら、右ペインを下にスクロールし、赤枠を画像の通りに編集します。
![WS000047.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/9a6d2ec5-fc00-0833-980c-c3af4056cb0f.jpeg)

これでフォーム内の「削除」ボタンを押下すると、先ほど定義したPL/SQLコードが実行されます。
試しに先ほど登録したファイルの鉛筆マークを選択し「削除」を選択してみます。
![WS000048.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/12bdbd0c-8156-331c-01e9-7cf7725564e2.jpeg)
![WS000049.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/97219f86-90e2-0842-7cf5-1abc4f01dec8.jpeg)

問題なくデータが削除され、表示されなくなったことを確認します。
![WS000050.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/3ee3e9dc-f833-5020-b99c-5efb31a8dd26.jpeg)

## (参考) EmbeddingにONNX形式でimportしたモデルを使う場合

ご参考までに、ONNX形式でimportしたモデルを使ってEmbeddingするコードを掲載します。
importしたモデルを使うことで、DB内でEmbedding処理を実行できます。
インターネット経由でアクセスするOpenAIやCohereなどが提供するモデルを使えず、NW的に閉じた環境で実装したい場合に有用です。

ファイル新規登録の処理 (「作成」ボタンを押下した際の処理) は以下の通りです。

```sql
DECLARE
    l_file apex_application_temp_files%rowtype;
    chunk_params clob;
    onnx_params clob;
BEGIN
    -- チャンク分割する際のパラメータ
    chunk_params := '
    {
      "max": "200",
      "overlap": "40"
    }';
    
    -- ONNX形式でimportしたモデルを使ってEmbeddingするためのパラメータ
    onnx_params := '
    {
      "provider": "database",
      "model": "doc_model"
    }';
    
    -- アップロードするファイルが未指定の場合、メタデータのみ保存して終了
    IF :P3_DOC IS NULL THEN
        INSERT INTO DOCUMENTS (name, category, doc_lastupd) VALUES (:P3_NAME, :P3_CATEGORY, :P3_DOC_LASTUPD);
    ELSE
        -- 非構造化データの情報を抽出
        SELECT * INTO l_file
            FROM APEX_APPLICATION_TEMP_FILES
            WHERE name IN 
            (
                SELECT column_value
                FROM APEX_STRING.SPLIT(:P3_DOC, ':')
            );
    
        -- DOCUMENTS表に非構造化データ、メタデータを保存
        INSERT INTO DOCUMENTS (name, category, doc, doc_filename, doc_mimetype, doc_lastupd)
            VALUES (:P3_NAME, :P3_CATEGORY, l_file.BLOB_CONTENT, l_file.FILENAME, l_file.MIME_TYPE, :P3_DOC_LASTUPD);
        
        -- チャンク分割したテキストをEMBED表に保存
        INSERT INTO EMBED (doc_id, embed_data)
            SELECT dt.id, chunk_t.chunk_data
            FROM
            documents dt,
            DBMS_VECTOR_CHAIN.UTL_TO_CHUNKS(REPLACE(DBMS_VECTOR_CHAIN.UTL_TO_TEXT(dt.doc), CHR(10)), JSON(chunk_params)) chunk_jt,
            JSON_TABLE(chunk_jt.column_value, '$[*]' COLUMNS (chunk_id NUMBER PATH
            '$.chunk_id', chunk_data VARCHAR2(4000) PATH '$.chunk_data')) chunk_t
            WHERE dt.name = :P3_NAME;
        
        -- EMBED表に新規保存された各テキストについて、ONNX形式でimportしたモデルを使ってEmbeddingし、EMBED表に保存
        FOR rec IN (SELECT em.embed_id, em.embed_data
            FROM documents doc, embed em
            WHERE doc.name = :P3_NAME AND doc.id = em.doc_id)
        LOOP
            UPDATE embed
            SET
                embedding =DBMS_VECTOR.UTL_TO_EMBEDDING(rec.embed_data, JSON(onnx_params))
            WHERE embed_id = rec.embed_id;
        END LOOP; 
    END IF;
    
    COMMIT;
END;
```

Cohereのモデルを利用したコードとの相違点は、14～18行目 のパラメータ指定の箇所だけです。
パラメータ中の `model` には、事前準備でONNX形式のモデルをimportした際に指定したオブジェクト名 (本例では `doc_model` ) を入力します。

また登録済みファイルの変更処理 (「変更の適用」ボタンを押下した際の処理) は以下の通りです。

```sql
DECLARE
    l_file apex_application_temp_files%rowtype;
    chunk_params clob;
    onnx_params clob;
BEGIN
    -- チャンク分割する際のパラメータ
    chunk_params := '
    {
      "max": "200",
      "overlap": "40"
    }';
    
    -- ONNX形式でimportしたモデルを使ってEmbeddingするためのパラメータ
    onnx_params := '
    {
      "provider": "database",
      "model": "doc_model"
    }';
    
    -- アップロードするファイルが未指定の場合、メタデータのみ保存して終了
    IF :P3_DOC IS NULL THEN
        UPDATE DOCUMENTS 
        SET 
            name = :P3_NAME,
            category = :P3_CATEGORY,
            doc_lastupd = :P3_DOC_LASTUPD
        WHERE
            id = :P3_ID;
    ELSE
        -- 非構造化データの情報を抽出
        SELECT * INTO l_file
            FROM APEX_APPLICATION_TEMP_FILES
            WHERE name IN 
            (
                SELECT column_value
                FROM APEX_STRING.SPLIT(:P3_DOC, ':')
            );
        
        -- DOCUMENTS表に非構造化データ、メタデータを保存
        UPDATE DOCUMENTS 
        SET 
            name = :P3_NAME,
            category = :P3_CATEGORY,
            doc = l_file.BLOB_CONTENT,
            doc_filename = l_file.FILENAME,
            doc_mimetype = l_file.MIME_TYPE,
            doc_lastupd = :P3_DOC_LASTUPD
        WHERE
            id = :P3_ID;
        
        -- EMBED表に保存されている更新ドキュメントに対応する情報を一度すべて削除
        DELETE FROM embed WHERE doc_id = :P3_ID;
        
        -- チャンク分割したテキストをEMBED表に保存
        INSERT INTO EMBED (doc_id, embed_data)
            SELECT dt.id, chunk_t.chunk_data
            FROM
            documents dt,
                DBMS_VECTOR_CHAIN.UTL_TO_CHUNKS(REPLACE(DBMS_VECTOR_CHAIN.UTL_TO_TEXT(dt.doc), CHR(10)), JSON(chunk_params)) chunk_jt,
            JSON_TABLE(chunk_jt.column_value, '$[*]' COLUMNS (chunk_id NUMBER PATH
            '$.chunk_id', chunk_data VARCHAR2(4000) PATH '$.chunk_data')) chunk_t
            WHERE dt.name = :P3_NAME;
        
        -- EMBED表に再保存された各テキストについて、ONNX形式でimportしたモデルを使ってEmbeddingし、EMBED表に保存
        FOR rec IN (SELECT em.embed_id, em.embed_data
            FROM documents doc, embed em
            WHERE doc.name = :P3_NAME AND doc.id = em.doc_id)
        LOOP
            UPDATE embed
            SET
                embedding =DBMS_VECTOR.UTL_TO_EMBEDDING(rec.embed_data, JSON(onnx_params))
            WHERE embed_id = rec.embed_id;
        END LOOP; 
    END IF;
    
    COMMIT;
END;
```

こちらもCohere利用時との相違点はパラメータ指定の部分だけです。

以上がアプリ実装の前編になります。
後編では生成AIに質問し、回答を表示する画面と内部処理を実装していきます。
生成AIと連携する処理では、質問文を使った登録済みファイルのベクトル検索、および質問文と検索結果を生成AIに連携し回答に利用させるRAGも実装します。
[Oracle Database 23ai FreeとAPEXでRAGを使った生成AIアプリをローコード開発してみた (アプリ実装 後編)](https://qiita.com/ssfujita/items/53df4f342e28c238a5a2)