[SQL編] 新リリース Oracle Database 23ai で実装するエンタープライズRAG

本記事は日本オラクルが運営する下記Meetupで発表予定の内容になります。発表までに今後、内容は予告なく変更される可能性があることをあらかじめご了承ください。

https://oracle-code-tokyo-dev.connpass.com/

# RAG
LLM周辺のソリューションとして検索拡張生成、通称RAG(Retrieval Augumented Generation)と呼んでいる手法があります。現在、様々な企業がLLMのユースケースを模索する中、このソリューションが導入検討の筆頭となるケースが非常に多く、まず最初にこのRAGがどのようなコンセプトなのかを簡単にご紹介します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109260/36c5cf88-377b-0f4e-7594-afca03e38130.png)

LLMプロバイダーが提供しているLLMはインターネットに公開されているテキストデータを学習して作られたモデルです。従って学習時点で、インターネットに公開されていないデータについてテキスト生成を行うことができません。(上図の左)

特に、社内には沢山のドキュメントデータが日々作られており、これらのドキュメントをAIに活用できないというのは非常にもったいない。ということでこれらのドキュメントデータをベクトルデータベースに入れておいて、LLMが知らない社内ドキュメントについてはベクトルデータベースから情報を取り出し、最終的なテキスト生成を行うというソリューションがRAGです。(上図の右)



# RAGの構成パターン

現在、RAGを構成パターンとして主流の方法は下記3つのパターンです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109260/597d2003-236a-2e17-61cc-5847f69243a5.png)


- パターン1) フル・スクラッチ開発
主にLLMプロバーダーから提供されているAPIのみでRAGのロジックを実装するパターンです。抽象化されていないAPIを使うためコーディング量は多めです。関数の入出力のフォーマット変換など、その他もろもろ、自動的にはされないのでその部分は自分で頑張ってコードを書いていく必要があります。全て自分でコードをゼロから書くため自由度は非常に高いです。※フル・スクラッチパターンのサンプル実装記事は[こちら](https://qiita.com/ksonoda/items/c300e734a5b1bef7b872)。



- パターン2) オーケストレーションツール(LangChain、LlamaIndexなどオープンソースのツール)
RAGのロジックを自分で実装するのは少々面倒だ、そんなお決まりのコードはAPIに任せたい、という場合にその部分を抽象化したAPIとしてLangChain、LlamaIndexなどオープンソースのオーケストレーションツールを使うバターンです。これらのAPIではお決まりのRAGパイプラインを非常にシンプルなコードで実装できます。お決まりのRAGパイプラインといっても、これらのツールは実にバラエティに富んだ構成を実装できるようになっており広く使われています。コーディング量は非常に少なくて済みますし、新しい技術も比較的早めにサポートされます。またコーディングの自由度も比較的高いです。※ LangChainパターンのサンプル実装記事は[こちら](https://qiita.com/ksonoda/items/d434aca84d6e6dacb3f1)。



- オーケストレーションツール(クラウドサービスのオーケストレーションツール)
LangChain、LlamaIndexなどはAPIなので、簡単とは言えコーディングの知識、工数は必須です。また、沢山のオープンソースライブラリを利用するためそれらに精通している必要があり、かつ、それらのバージョンの互換性を常に担保しなければならず運用負荷は決して低くはありません。
そこで、オーケストレーションツールの部分にクラウドサービスを利用するというパターンがあります。主要な総合クラウドベンダーは下記のようにRAGを構成するためのクラウドサービスをリリースしています。どのベンダーのクラウドサービスもWebUIベース(コーディングなし)でRAGのパイプラインが実装でき、クラウドサービスなのでもちろん運用はクラウドベンダーに丸投げというところがメリットになります。逆に、これらのクラウドーサービスでは最新技術が実装されるタイミングは遅い(もしくは実装されない)ということがありますのでその点がデメリットになります。

    - AWS : Bedrock Agent
    - Google : GCP Vertex AI Agent Builder
    - Microsoft : Azure OpenAI Service On your data
    - Oracle : OCI Generative AI Agents Service(Beta)
    
主要なパターンは上述した3つなのですが、本記事でご紹介するサンプル実装は第4のパターン、「SQLでRAGパイプラインを実装する」という新しいパターンです。Oracle Databaseの新しいバージョン Oracle Database 23ai では、SQLでRAGパイプラインを構成することができます。


# SQLで実装するRAG

下図がOracle Database 23ai のSQLでRAGを実装する際のワークフローです。図に示すSQLを左から順番に実行してゆけばRAGパイプラインが実装できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109260/8947e6fc-6060-965c-b320-16f5bff09765.png)

図の左①から⑤まで各プロシージャの処理の流れは下記のようなものになります。

- ステップ1 : UTL_TO_TEXT() 
まずはデータをベクトルデータベースにロードします。データはPDF、Word、PPT、HTMLなど様々なフォーマットがありますので、それをドキュメントローダーで構造解析し、テキストデータに変換する必要があります。この処理を UTL_TO_TEXT() で実行します。

- ステップ2 : UTL_TO_CHUNKS()
次に、上述のテキストデータをテキストプリッターでチャンクテキストに分割します。たくさんの長文を短文に区切るようなイメージです。この処理を UTL_TO_CHUNKS()で実行します。

- ステップ3 : UTL_TO_EMBEDDINGS()
次に、上述のチャンクテキストを埋め込みモデルでベクトルに変換し、データベースの表にロードします。この処理をUTL_TO_EMBEDDINGS()で実行します。これにより、チャンクテキストとそれに対応するベクトルデータから構成された表ができあがります。

- ステップ4 : VECTOR_DISTANCE
ここからが検索です。入力プロンプトのテキストを埋め込みモデルでベクトルに変換し、ベクトルデータベースの中にあるベクトルデータに対して類似検索を行い、入力プロンプトと類似度の高いチャンクテキストを検索します。(下記のサンプルコードでは類似度の高い5件のチャンクテキストを取り出しています。)この類似度を計算するためのSQLとしてVECTOR_DISTANCEを使います。(こちらはプロシージャではありません)

- ステップ5 : UTL_TO_GENERATE_TEXT()
ステップ4で検索した類似度の高いチャンクテキストと、プロンプトをテキスト生成モデルに入力し、最終的な返答となるテキストを生成します。この処理をUTL_TO_GENERATE_TEXT()で実行します。

このようにOracle Database 23ai にはRAGのパイプラインとなる処理を実行するためのプロシージャが実装されており、SQLだけでRAGパイプラインが構築できるようになっています。


また、構成に利用するサービスは以下の通り。

- ベクトルデータベース
    - Oracle Database 23ai Free(AI Vector Search)
- ドキュメントデータのベクトル化に利用するモデル
    - Oracle Cloud Generative AI Service(embed-multilingual-v3.0)
- テキスト生成モデルは下記5つを全て試してみようと思います。
    - Cohere(Command-R-Plus)
    - OpenAI(GPT3-Turbo, GPT4)
    - GoogleAI(Gamini)
    - OCI Generative AI Service(Command)



### ドキュメントデータ
ベクトルデータベースにロードするドキュメントデータは下図のような内容のPDFファイルです。テキストの内容としては架空の製品であるロケットエンジンOraBoosterの説明文章です。企業内のデータを想定し、テキストの内容としては、存在しない製品かつ完全な創作文章、ということでLLMが学習しているはずのないデータということになります。後の手順でこちらのPDFファイルをベクトルデータベースにロードします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109260/76dc0197-cd00-ac65-5f4d-0f4a59a18391.png)

### シナリオ
本記事では下記のようなシナリオでテキスト生成を行い、LLMが学習していないデータ(上述のPDFの内容)に関する質問に対してRAG構成でうまく回答できることを確認します。また、非RAG構成とのテキスト生成結果を比較し、RAGの有効性を確認します。

1. 架空の製品OraBoosterについて質問をします。
2. LLMはこの製品の知識を持ち合わせていないため、ベクトルデータベースに類似検索を行い、ヒントとなるテキストデータ(チャンクテキスト)を複数検索します。下記のコードでは類似度の高いチャンクテキストを3つ検索するコードにしています。
4. 類似検索で取り出したチャンクテキストともともとのプロンプトをテキスト生成モデルに入力し、OraBoosterの説明テキストが生成されていることを確認します。
5. ベクトルデータベースへの類似検索処理をスキップした状態(つまり、RAGではなく単にLLMにクエリしている状態)でテキスト生成を行い、上記「4」で生成されたテキストと比較しRAGの有効性を確認します。


# 実装
下記の流れで実装してゆきます。

1. データベースインスタンスとデータベースの作成
2. ドキュメントファイル(今回はPDF)をデータベースにロードし、チャンク作成、ベクトル化 
3. プロンプトから類似検索を実行し、プロンプトと関連度の高いチャンクテキストを取り出す
4. 類似検索結果とプロンプトをテキスト生成モデルに入力


### 1. データベースインスタンスとデータベースの作成
まずは、ベクトルデータベースとなるOracle Databaseを準備します。下記のチュートリアルに沿ってComputeインスタンスにOracle Database 23ai Freeをインストールします。

https://oracle-japan.github.io/ocitutorials/ai-vector-search/ai-vector102-23aifree-install/

データベースインストール後、下記の流れでユーザー作成、その他の設定を行います。

日本語表示のため、下記環境変数を定義します。

```sql
export NLS_LANG=Japanese_Japan.AL32UTF8
```

コンテナデータベース(CDB)内のプラガブル・データベース(PDB)にsysユーザーでsysdbaとして接続します。

```sql
sqlplus /nolog
CONN sys@freepdb1 as sysdba
```

DBユーザー(docuser)を作成し、必要な権限を付与します。

```sql
drop user docuser cascade; 
grant connect, ctxapp, unlimited tablespace, create credential, create procedure, create table to docuser identified by docuser;
grant execute on sys.dmutil_lib to docuser; 
grant create mining model to docuser; 

BEGIN
DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
    host => '*', 
    ace => xs$ace_type(
        privilege_list => xs$name_list('connect'),
        principal_name => 'docuser', 
        principal_type => xs_acl.ptype_db)); 
END; 
/
```

インスタンスに配置したPDFドキュメントを読み込むためにディレクトリオブジェクトを作成します。こちらのOSのディレクトリに上述したPDFファイル(rocket.pdf)を配置してあります。


```sql
create directory VEC_DUMP as '/home/oracle/data/vec_dump';
grant read, write on directory VEC_DUMP to docuser; 
commit;
```

Oracle Databaseに接続します。

```sql
sqlplus /nolog
CONN docuser@freepdb1
```

SQL*Plusの表示を調整します。

```sql
SET ECHO ON 
SET FEEDBACK 1 
SET NUMWIDTH 10
SET LINESIZE 80 
SET TRIMSPOOL ON 
SET TAB OFF 
SET PAGESIZE 10000 
SET LONG 10000
```

### 2. ドキュメントデータをデータベースにロードし、チャンク化し、ベクトルデータに変換

以降の処理で、上述したRAGパイプラインの図の左下の赤枠部分を実行してゆきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109260/8ee29c27-b94d-0190-dc8d-04b91bcc9126.png)

ここで登場するSQLは上述した5つのうち、下記3つです。

①UTL_TO_TEXT
②UTL_TO_CHUNK
③UTL_TO_EMBEDDING

まず、表(documentation_tab)を作成し、その表にPDFドキュメントを格納します。

```sql
drop table documentation_tab purge; 
CREATE TABLE documentation_tab (id number, data blob);
INSERT INTO documentation_tab values(1, to_blob(bfilename('VEC_DUMP', 'rocket.pdf'))); 
commit;
```

UTL_TO_TEXTを実行してPDFをテキスト形式に変換します。

```sql
SELECT dbms_vector_chain.utl_to_text(dt.data) 
from documentation_tab dt;
```

UTL_TO_CHUNKSを実行して、テキストの全文がどのようにチャンクに分割されるかを下記SQLで確認します。今回は固定長で分割するので下記SQLでチャンクテキストを確認しながら max と overlap の値を調整します。

```sql
SELECT ct.* 
from documentation_tab dt,
    dbms_vector_chain.utl_to_chunks(
        dbms_vector_chain.utl_to_text(dt.data), 
        json('{"max": "100", "overlap": "10%", "language": "JAPANESE", "normalize": "all", "split": "sentence"}')
    ) ct;
```

最終的にチャンクテキストをベクトルに変換するためにOracle CloudのGenerative AI Serviceの埋め込みモデルを利用します。このサービスを利用するにはAPIキーでの認証が必要になりますから、APIキーをCredentialとしてデータベースに登録する処理を下記手順で行います。

まず、下記シェルコマンドを実行して、APIキーの生成で取得したkey_fileの文字列を取得します。

```bash
awk '/^--/{ next } { printf "%s", $0 } END { printf "\n" }' ~/.oci/oci_api_key.pem | tr -d '\n'
```

上記で取得した文字列をprivate_keyに入力し、APIキーの生成で取得したuser_ocid、tenancy_ocid、fingerprintおよびcompartment_ocidを入力して実行します。

```sql
exec dbms_vector.drop_credential('OCI_CRED'); 

declare 
jo json_object_t; 
begin 

jo := json_object_t(); 
jo.put('user_ocid', 'user ocid value'); 
jo.put('tenancy_ocid', 'tenancy ocid value'); 
jo.put('compartment_ocid', 'compartment ocid value'); 
jo.put('private_key', 'private key value'); 
jo.put('fingerprint', 'fingerprint value'); dbms_output.put_line(jo.to_string); 
dbms_vector.create_credential(
    credential_name => 'OCI_CRED', 
    params => json(jo.to_string)); 
end; 
/
```

OCI GenAIサービスにアクセスするためのパラメータを設定します。

```sql
var embed_genai_params clob; 
exec :embed_genai_params := '{"provider": "ocigenai", "credential_name": "OCI_CRED", "url": "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/embedText", "model": "cohere.embed-multilingual-v3.0"}';
```

上記の設定が動作するかOCI GenAIサービスへのアクセスして確認してみます。

```sql
select et.* from dbms_vector_chain.utl_to_embeddings(
    'こんにちは', 
    json(:embed_genai_params)
) et;
```

下記のように、入力したテキスト「こんにちは」のベクトル値が取得できていれば成功です。

```sql
COLUMN_VALUE 
--------------------------------------------------------
{"embed_id":"1","embed_data":"こんにちは","embed_vector":"[0.0035 934448,0.028701782,0.031051636,-0.001415
... 
1行が選択されました。
```

ここまでの手順でチャンク作成とベクトル化の準備ができました。
実際にUTL_TO_EMBEDDINGSを実行して、チャンクテキストをベクトルに変換します。

下記のSQLでは上述したUTL_TO_CHUNKSで分割したチャンクのテキストデータをUTL_TO_EMBEDDINGSでベクトル化しその値を保存する表を作成しています。

```sql
CREATE TABLE doc_chunks AS
WITH t_chunk AS (
  SELECT 
    dt.id AS doc_id, 
    et.chunk_id AS embed_id, 
    et.chunk_data AS embed_data
  FROM
    documentation_tab dt,
    dbms_vector_chain.utl_to_chunks(
      dbms_vector_chain.utl_to_text(dt.data), 
      JSON('{"max": "100", "overlap": "10%", "language": "JAPANESE", "normalize": "all", "split": "sentence"}')
    ) t, 
    JSON_TABLE(
      t.column_value, 
      '$[*]' COLUMNS (
        chunk_id NUMBER PATH '$.chunk_id', 
        chunk_data VARCHAR2(4000) PATH '$.chunk_data'
      )
    ) et
  WHERE 
    dt.id = 1
),
t_embed AS (
  SELECT 
    dt.id AS doc_id, 
    ROWNUM AS embed_id, 
    to_vector(t.column_value) AS embed_vector
  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('{"max": "100", "overlap": "10%", "language": "JAPANESE", "normalize": "all", "split": "sentence"}')
      ), 
      JSON('{
        "provider": "ocigenai", 
        "credential_name": "OCI_CRED", 
        "url": "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/embedText", 
        "model": "cohere.embed-multilingual-v3.0"
      }')
    ) t
  WHERE 
    dt.id = 1
)
SELECT 
  t_chunk.doc_id AS doc_id, 
  t_chunk.embed_id AS embed_id, 
  t_chunk.embed_data AS embed_data, 
  t_embed.embed_vector AS embed_vector
FROM 
  t_chunk
JOIN 
  t_embed 
ON 
  t_chunk.doc_id = t_embed.doc_id 
  AND t_chunk.embed_id = t_embed.embed_id;
```

ここまでの手順で、PDFファイルのテキストデータをベクトルデータベースにロードし、チャンクテキストの作成、それに対応したベクトルデータを作成するところまでが完了しました。








### 3. 類似検索を実行し、プロンプトと関連度の高いチャンクテキストを取り出す

ここからがベクトル類似検索の処理になります。入力されたプロンプトと関連性の高いチャンクテキストを取り出します。上述したRAGパイプライン図でいうと下図の赤枠の部分です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109260/2c122b51-32d8-96a9-fca5-37d8aa97cfe5.png)

ここで類似検索に使用するSQLは④vector_distanceです。

入力プロンプトをベクトル化するためにembeddingモデルを定義します。ベクトルデータベースにデータをロードしてベクトル化する際に利用したモデル定義と同じなので続けて処理する場合は設定不要です。

```sql
SET SERVEROUTPUT ON;
VAR prompt CLOB;
VAR user_question CLOB;
VAR context CLOB;

var embed_genai_params clob;
exec :embed_genai_params := '{"provider": "ocigenai", "credential_name": "OCI_CRED", "url": "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/embedText", "model": "cohere.embed-multilingual-v3.0"}';
```

定義した埋め込みモデルを使ってベクトルデータベースに類似検索を実行します。

```sql
BEGIN
-- 類似検索の結果を入力する変数を定義
:context := '';

-- 質問(プロンプト)を定義
:user_question := 'OraBoosterとは何ですか？';

-- 質問のテキストで類似検索を実行(vector_distanceで検索を実行)
FOR rec IN (
   SELECT EMBED_DATA
   FROM doc_chunks
   WHERE DOC_ID = '1'
   ORDER BY vector_distance(embed_vector , (SELECT to_vector(et.embed_vector) embed_vector FROM dbms_vector_chain.utl_to_embeddings(:user_question, JSON(:embed_genai_params)) t, JSON_TABLE ( t.column_value, '$[*]' COLUMNS ( embed_id NUMBER PATH '$.embed_id', embed_data VARCHAR2 ( 4000 ) PATH '$.embed_data', embed_vector CLOB PATH '$.embed_vector' ) ) et), COSINE)
   FETCH FIRST 3 ROWS ONLY
)
LOOP
    :context := :context || rec.embed_data;
END LOOP;

 -- 質問と類似検索結果を含めた新しいプロンプトを定義(この後のテキスト生成の工程で使います。)
:prompt := 'あなたは専門家であることを前提として、提供されたコンテキストを使用して次の質問に答えてください。Question: ' || :user_question || ' Context: ' || :context;
DBMS_OUTPUT.PUT_LINE('Generated prompt: ' || :prompt);
END;
/
```

上記の処理で、バインド変数promptには元の質問('OraBoosterとは何ですか？')と類似検索結果から得られた類似度の高いtop 3のチャンクテキストが入った状態になっています。


### 4. テキスト生成の実行

前の処理で作成した prompt をテキスト生成モデルに入力し、応答テキストを生成します。RAGパイプラインの図の赤枠の部分の処理です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109260/3ea9df9c-f559-a2ad-80a8-e01b3fb18d6e.png)

以降、下記5パターンのモデルでテキスト生成をしてみます。いずれも⑤UTL_TO_GENERATE_TEXTのSQLの中でモデルを定義しテキスト生成に利用します。

- Cohere Command R Plus
- OpenAI GPT3 Turbo
- OpenAI GPT4
- Google Gemini
- OCI Generative AI Service(Command)

#### テキスト生成 (Cohere Command R Plus)
テキスト生成モデルを定義します。一つ目は最近リリースされたCohere Command R Plusを使ってみましょう。cohere社のサイトで取得したapiキーを使って認証の定義(credentialの作成)を行います。

```sql
exec dbms_vector.drop_credential('COHERE_CRED');

declare
 jo json_object_t;
begin
 jo := json_object_t();
 jo.put('access_token', '<cohereのapi keyを入力>');
 dbms_vector.create_credential(
 credential_name => 'COHERE_CRED',
 params => json(jo.to_string));
end;
/
```

次に、テキスト生成です。類似検索の処理で定義したバインド変数prompt(質問テキストと類似検索結果のチャンクテキスト)をLLMに入力するSQL(UTL_TO_GENERATE_TEXT)を実行します。

```sql
DECLARE
 input CLOB;
 params CLOB;
 output CLOB;

BEGIN
input := :prompt;

params := '{"provider": "cohere", "credential_name": "COHERE_CRED", "url": "https://api.cohere.com/v1/generate", "model": "command-r-plus"}';

utl_http.set_body_charset('UTF-8');

output := DBMS_VECTOR_CHAIN.UTL_TO_GENERATE_TEXT(input, json(params));
 DBMS_OUTPUT.PUT_LINE(output);
 IF output IS NOT NULL THEN
 DBMS_LOB.FREETEMPORARY(output);
 END IF;
EXCEPTION
 WHEN OTHERS THEN
 DBMS_OUTPUT.PUT_LINE(SQLERRM);
 DBMS_OUTPUT.PUT_LINE(SQLCODE);
END;
/
```

すると出力としては下記のような感じです。

```sql
OraBooster
は、次世代の宇宙探査を可能にするために開発された革新的なロケットエンジンです。そ
の高性能と革新的な設計により、宇宙進出の新たな時代を切り拓く革命的な技術となって
います。

OraBooster
の最大の特徴は、その比類ない推力と先進的な推進システムです。従来のエンジンよりも
はるかに高い推力を発生させることで、より遠くの惑星や星系への探索が可能となります
。この高い推力は、宇宙探査ミッションの範囲と可能性を大きく広げます。

さらに、OraBooster
には超軽量かつ超強度の素材が使われています。エンジンの外殻にナノファイバー製の素
材を使用することで、強度と軽量化を両立させています。これにより、ロケットの全体的
な重量が軽減され、より多くのペイロードを搭載したり、より遠くまで飛行したりするこ
とが可能となります。

このエンジンのもう一つの重要な機能は、その適応性と追跡能力
です。ナノファイバー製の素材は、目標を追跡して調整できるように設計されており、宇
宙飛行中の安定した飛行軌道の維持に役立ちます。この適応能力により、ミッション中に
予期せぬ事態が発生した場合でも、軌道を修正して目的地に到達することができます。


OraBooster
は、宇宙探査の未来を形作るための重要な技術であり、その高性能と革新的な設計により
、宇宙ミッションの成功と人間の宇宙進出の加速に貢献します。
```

類似検索結果を使ってちゃんとテキストが生成されていることが分かります。

ここからいくつかのテキスト生成モデルを試してみようと思います。

#### テキスト生成(OpenAI gpt-3.5-turbo)

OpenAI gpt-3.5-turboの場合も同様に、まずはOpenAIのapiキーを使ってcredentialを定義します。

```sql
exec dbms_vector.drop_credential('OPENAI_CRED');

declare
 jo json_object_t;
begin
 jo := json_object_t();
 jo.put('access_token', '<OpenAIのapi keyを入力>');
 dbms_vector.create_credential(
 credential_name => 'OPENAI_CRED',
 params => json(jo.to_string));
end;
/
```

次にテキスト生成です。


```sql
DECLARE
 input CLOB;
 params CLOB;
 output CLOB;
BEGIN
input := :prompt;

params := '{"provider": "openai", "credential_name": "OPENAI_CRED", "url": "https://api.openai.com/v1/chat/completions", "model": "gpt-3.5-turbo"}';

utl_http.set_body_charset('UTF-8');

output := DBMS_VECTOR_CHAIN.UTL_TO_GENERATE_TEXT(input, json(params));
 DBMS_OUTPUT.PUT_LINE(output);
 IF output IS NOT NULL THEN
 DBMS_LOB.FREETEMPORARY(output);
 END IF;
EXCEPTION
 WHEN OTHERS THEN
 DBMS_OUTPUT.PUT_LINE(SQLERRM);
 DBMS_OUTPUT.PUT_LINE(SQLCODE);
END;
/
```

こちらが出力。

```sql
OraBoosterは、当社が開発したロケットエンジンで、先進的な推進技術を採用した次世代
の宇宙探査を支える象徴的な存在です。このエンジンは高性能と革新性を融合させており
、人類の宇宙進出を加速させる革命的な一歩として位置づけられています。OraBoosterは
非常に高い推力を発生し、遠く離れた惑星や星系への探査を可能にすることができます。
また、エンジンの外殻にはナノファイバー製の超軽量かつ超強度の素材が使用されており
、これにより長時間にわたる宇宙飛行中でも安定した飛行軌道を維持し、ミッションの成
功を確保します。

```

#### テキスト生成(OpenAI gpt-4)

お次は gpt-4

```sql
DECLARE
 input CLOB;
 params CLOB;

output CLOB;
BEGIN
input := :prompt;

params := '{"provider": "openai", "credential_name": "OPENAI_CRED", "url": "https://api.openai.com/v1/chat/completions", "model": "gpt-4"}';

utl_http.set_body_charset('UTF-8');

output := DBMS_VECTOR_CHAIN.UTL_TO_GENERATE_TEXT(input, json(params));
 DBMS_OUTPUT.PUT_LINE(output);
 IF output IS NOT NULL THEN
 DBMS_LOB.FREETEMPORARY(output);
 END IF;
EXCEPTION
 WHEN OTHERS THEN
 DBMS_OUTPUT.PUT_LINE(SQLERRM);
 DBMS_OUTPUT.PUT_LINE(SQLCODE);
END;
/
```

こちらが出力。

```sql
OraBoosterは、開発元の会社が作ったロケットエンジンで、次世代の宇宙探査を支える先
進的な推進技術を示しています。このユニークな設計は高性能と革新性を組み合わせ、宇
宙進出を加速させる能力を備えています。遠くの星や惑星への探査を可能にするために、
非常に高い推力を発生します。さらに、エンジンの外側は、超軽量かつ超強度のナノファ
イバー素材で作られています。これにより、飛行軌道を安定させ、長時間の宇宙飛行中に
ミッションの成功を確保します。
```

#### テキスト生成(Google gemini-pro)

次は、GoogleAIのモデルGemini。

```sql
exec dbms_vector.drop_credential('GOOGLEAI_CRED');

declare
 jo json_object_t;
begin
 jo := json_object_t();
 jo.put('access_token', '<GoogleAIのapi key>');
 dbms_vector.create_credential(
 credential_name => 'GOOGLEAI_CRED',
 params => json(jo.to_string));
end;
/
```

次にテキスト生成です。

```sql
DECLARE
 input CLOB;
 params CLOB;
 output CLOB;

BEGIN
 input := :prompt;

 params := '{"provider": "googleai", "credential_name": "GOOGLEAI_CRED", "url": "https://generativelanguage.googleapis.com/v1beta/models/", "model": "gemini-pro:generateContent"}';

 utl_http.set_body_charset('UTF-8');

 output := DBMS_VECTOR_CHAIN.UTL_TO_GENERATE_TEXT(input, json(params));
 DBMS_OUTPUT.PUT_LINE(output);
 IF output IS NOT NULL THEN
  DBMS_LOB.FREETEMPORARY(output);
 END IF;
EXCEPTION
 WHEN OTHERS THEN
 DBMS_OUTPUT.PUT_LINE(SQLERRM);
 DBMS_OUTPUT.PUT_LINE(SQLCODE);
END;
/
```

こちらが出力。

```sql
OraBoosterは、次世代の宇宙探査をサポートするよう設計された、革新的な推進技術を採
用したロケットエンジンです。
```





#### テキスト生成(OCI Generative AI ServiceのCommandモデル)

本記事では、埋め込みモデルでOCI Generative AI Serviceのembeddingモデルを使っています。どうせなら同じサービスのテキスト生成モデルであるCommand-R、Command-R-Plusを使いたいのですが、今日現在、UTL_TO_TEXT_EMBEDDINGSプロシージャが上述のモデルに対応していませんので、あえてmultilingual対応していない commandモデルを使ってテキスト生成をしてみたいと思います。

credentialは既に作成済のOCI_CREDを使ってテキスト生成してみます。

```sql
DECLARE
 input CLOB;
 params CLOB;

output CLOB;
BEGIN
input := :prompt;

params := '{"provider": "ocigenai", "credential_name": "OCI_CRED", "url": "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/generateText", "model": "cohere.command"}';

output := DBMS_VECTOR_CHAIN.UTL_TO_GENERATE_TEXT(input, json(params));
 DBMS_OUTPUT.PUT_LINE(output);
 IF output IS NOT NULL THEN
 DBMS_LOB.FREETEMPORARY(output);
 END IF;
EXCEPTION
 WHEN OTHERS THEN
 DBMS_OUTPUT.PUT_LINE(SQLERRM);
 DBMS_OUTPUT.PUT_LINE(SQLCODE);
END;
/

```

出力はもちろん英語ですが内容としては外れていないように見えますね。

```sql
The OraBooster is a next-generation space probe designed to support advanced,
progressive technologies for exploring
```



# おわりに
言うまでもなく、LLMやRAG周辺のソリューションはPythonで実装する方法が主流であり、SQLで実装するやり方はユニークです。機械学習やLLM関連のコードをSQLで実装することに抵抗を感じる方がいらっしゃるのではないかと想像しますが(実は筆者もその一人でした。)、実際にやってみるとそのシンプルさに非常に新鮮な感覚を覚えます。とかく実装言語については指向が大きく分かれるところでもあると思いますが、普段データベース技術者の方にこのような実装を説明していると、意外とSQLで実装するパターンに好評を得ることが多いということを感じます。そのようなユーザーの声としては、とにかくわかりやすい、PythonよりSQLでできるほうが嬉しい、なんでもSQLで実装できるほうが嬉しい、既にデータベースの中にあるデータもSQLで簡単に統合できてメリットを感じる、というようなものです。(データベース技術者の方なので当たり前といえば当たり前ではあるのですが。)特にベクトルデータベースとしてRDB系の製品を考えられている方にとっては選択の一つとして検討していただきたい実装です。




# 関連記事

https://qiita.com/ksonoda/items/c300e734a5b1bef7b872

https://qiita.com/ksonoda/items/d434aca84d6e6dacb3f1

https://qiita.com/ksonoda/items/ba6d7b913fc744db3d79

https://www.youtube.com/watch?v=8-7HOHJygeU