日本オラクルと協創！企業内のあらゆる業務データを元に回答する”生成AIエージェント”とは？ - (Part 2)

## はじめに

こんにちは、株式会社日立製作所の [Lumada Data Science Lab.](https://www.hitachi.co.jp/products/it/lumada/about/ai/ldsl/index.html) の渡邉です！

株式会社日立製作所（以下、日立）と日本オラクル株式会社（以下、オラクル）で3か月の協創プロジェクトを実施しました。

協創プロジェクトで実施した内容は以下の通りです！

1. [ユーザーの質問文から次のアクションをLLMに考えさせるAgentアーキテクチャの実装 (本記事Part 1で説明)](https://qiita.com/wtnbrs/private/cbf6a70e7051e687c2c7)
2. めんどうなベクトル化処理不要！今ある業務DBに活用したいテキストデータを組み合わせるだけのOracle Database構築 (本記事Part 2で説明)
3. [自然言語からSQL文へ！便利なSelect AI機能とAI Vector Search機能 (Part 3で説明)](https://qiita.com/wtnbrs/private/61d91570a1fd89379033)

このプロジェクトは以下のメンバーで、若手が中心になって進めました！
本記事もプロジェクトメンバー全員で執筆しています。

- 株式会社日立製作所　3年目データサイエンティスト　渡邉理沙
- 株式会社日立ソリューションズ・クリエイト　2年目データサイエンティスト　山口蓮
- 株式会社日立製作所　7年目オラクル製品担当エンジニア　野中一鴻
- 日本オラクル株式会社　4年目クラウドソリューションエンジニア　出口龍之介
- 日本オラクル株式会社　4年目クラウドソリューションエンジニア　宮本拓弥

LLMでの業務データ活用をテーマに、若手だけで協力してユースケース検討や実装を進め、**僅か3カ月でプロジェクトを無事完了**しました！

内容は、Part 1、Part 2、Part 3という、３部構成となっています。
この記事では、Part 2として、Oracle Database構築について説明します。

## めんどうなベクトル化処理不要！今ある業務DBに活用したいテキストデータを組み合わせるだけのOracle Database構築

まずは今回検証で使用したスキーマについて説明します。
`IM_TEST`というスキーマを作成し、以下のような表を作成しました。実際のユースケースを想定しているため、ある程度正規化しています。

- PRODUCTS表: 製品表。家電製品のID、製品名、価格、カテゴリIDを含む。
- CATEGORY表: カテゴリ表。家電のカテゴリID、カテゴリ名を含む。
- SITE表: 拠点表。生産拠点のID、拠点名を含む。
- STOCK表: 在庫表。拠点ID、製品IDで一意に決定され、在庫数、予約済み在庫数、引当可能な在庫数を含む。
- PRODUCTS_DESC表: 製品マニュアル表。製品ID、製品マニュアル(非構造化テキストデータ)を各チャンクに分けたチャンクID、チャンクテキスト、チャンクテキストをベクトル化したデータ

今回の検証では、製品マニュアル(PDF)のデータをOCRしたテキストデータをチャンキングして、PRODUCTS_DESC表に格納し、エンベッディングを行いました。
DB外部で非構造化データをエンベッディングして、DBにロードすることもできますが、今回はOracle Database 23aiのAI Vector Searchのベクトルデータを生成するためのPL/SQLパッケージである`DBMS_VECTOR_CHAIN`パッケージを使用してDB内部でエンベッディング処理を行いました。

なお、それ以外の表データは個別にINSERTしました。

Autonomous DatabaseではOS領域を使えないので、オブジェクトストレージやファイルストレージからの非構造化ファイルのロードが必要になります。

まずはオブジェクトストレージにOCR済みのテキストファイルをアップロードします。

![oss_upload.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3898980/7f4cd7f0-e969-d26a-2dc6-22be74aa9970.png)

続いて一時的にこれらの.txtファイルをblobとして格納するための表PRODUCTS_TEXT_TABを作成し、その表にロードします。ここでは`DBMS_CLOUD.GET_OBJECT`ファンクションを使用します。

```sql
-- 一時的に格納する表PRODUCTS_TEXT_TABの作成
CREATE TABLE PRODUCTS_TEXT_TAB (PRODUCT_NAME VARCHAR2(100), data blob);

-- オブジェクトストレージへアクセスするためのクレデンシャル作成
BEGIN  
    DBMS_CLOUD.CREATE_CREDENTIAL (
        credential_name => 'IM_TEST_OSS_CRED',
        user_ocid => 'ocid1.user.oc1..aaaaaaaaaaaa',
        tenancy_ocid => 'ocid1.tenancy.oc1..aaaaaaaaaaa',
        private_key => 'MIIEvwI...',
        fingerprint => 'xx:xx:xx');
END;
/

-- テキストファイルを格納したバケット内のオブジェクトのファイル名と中身のデータを抽出しロード
DECLARE
    bucket_uri	   VARCHAR2(200) := 'https://objectstorage.ap-tokyo-1.oraclecloud.com/n/orasejapan/b/bucket-product-text/o/';
    object_uri     VARCHAR2(200);
    l_blob BLOB := NULL;
BEGIN
    FOR rec IN (SELECT REPLACE(object_name, '.txt', '') AS product_name
                FROM DBMS_CLOUD.LIST_OBJECTS(
                    'IM_TEST_OSS_CRED',
                    bucket_uri)
               )
    LOOP
        object_uri := bucket_uri||rec.product_name||'.txt';
        l_blob := DBMS_CLOUD.GET_OBJECT(
            credential_name => 'IM_TEST_OSS_CRED',
            object_uri => object_uri);
        INSERT INTO PRODUCTS_TEXT_TAB values(rec.product_name, l_blob);      
    END LOOP;
    commit;
END;
/
```

これでDB内の非構造化データを格納できたので、続いてこの表に対してチャンキング、エンベッディングを行っていきます。

```sql
-- エンベッディングにはOCI GenAIのCohereのエンベッディングモデルを使用するため、OCI GenAIのAPIキーの設定
declare
  jo json_object_t;
  begin
  jo := json_object_t();
  jo.put('user_ocid', 'ocid1.user.oc1..aaaaa');
  jo.put('tenancy_ocid', 'ocid1.tenancy.oc1..aaaaaaaaaaaaa');
  jo.put('compartment_ocid', 'ocid1.compartment.oc1..aaaaaaaaaaaa');
  jo.put('private_key', 'MIIEvwIBxxxxx');
  jo.put('fingerprint', 'xx:xx:xx');
  dbms_output.put_line(jo.to_string);
  dbms_vector.create_credential(
    credential_name => 'IM_TEST_VECTOR_CRED',
    params          => json(jo.to_string));
end;
/

-- バインド変数の設定
var embed_genai_params clob;
exec :embed_genai_params := '{"provider": "ocigenai", "credential_name": "IM_TEST_VECTOR_CRED", "url": "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/embedText", "model": "cohere.embed-multilingual-v3.0"}';

--PRODUCTS_TEXT_TABのデータをそれぞれテキスト変換→チャンキングして、DESC_VEC以外の列にINSERT
INSERT INTO PRODUCTS_DESC(PRODUCT_ID, DESC_CHUNK_ID, DESC_CHUNK)
select p.PRODUCT_ID, et.chunk_id, et.chunk_data
from
PRODUCT_TEXT_TAB dt
join PRODUCTS p on dt.PRODUCT_NAME = p.PRODUCT_NAME, 
dbms_vector_chain.utl_to_chunks(dbms_vector_chain.utl_to_text(dt.data), json('{"max": "400", "overlap": "20", "language": "JAPANESE", "normalize": "all"}')) t, JSON_TABLE(t.column_value, '$[*]' COLUMNS (chunk_id NUMBER PATH '$.chunk_id', chunk_data VARCHAR2(4000) PATH '$.chunk_data')) et;

-- utl_to_embeddingを使って、PRODUCTS_DESC表の全行に対してembedding
UPDATE PRODUCTS_DESC PD
SET DESC_VEC = dbms_vector_chain.utl_to_embedding(PD.DESC_CHUNK, json(:embed_genai_params));

commit;
```

ベクトルデータを確認してみます。
```sql
select desc_vec from products_desc where rownum<6;

DESC_VEC
---------------------------------------------------------
[3.67736816E-002,2.59094238E-002,-7.87353516E-002,5.57861328E-002,...
[5.69152832E-003,3.04870605E-002,-1.91192627E-002,1.5625E-002,1.57623291E-002,...
[2.66723633E-002,2.24456787E-002,-2.02636719E-002,5.16891479E-003,...
[1.93481445E-002,3.28674316E-002,-5.69458008E-002,3.60412598E-002,...
[3.98864746E-002,3.52478027E-002,-6.2286377E-002,8.75091553E-003,...
```

20個の製品に対して、全部で何チャンクに分けられたか確認します。
```sql
select count(*) from products_desc;

COUNT(*)
----------
      2317
```

以上でサンプルデータの投入ができました。

このように**Oracle Database**や**Autonomous Database**には、非構造化データをベクトルデータに変換するための`DBMS_VECTOR_CHAIN`パッケージが用意されているので、DB内部でRAGアーキテクチャを組むまでのデータ準備を簡素化することができます。

また今回はエンベッディングにOCI GenAIで利用できるCohereのembed-multilingual-v3.0モデル（3rd partyモデル）を使用しましたが、DBにエンベッディングモデル自体をインポートし、DB内部でエンベッディングを行うこともできます。

## おわりに

いかがでしたでしょうか？

Part 2である今回は、Oracle Database構築についてご紹介しました。

Part 3の記事では、自然言語からSQL文を生成するためのSelect AI機能とAI Vector Search機能について解説します。

最後まで読んでくださり、ありがとうございました。