Oracle Database RU23.7で機能追加されたDB内マルチモーダルEmbeddingを試してみた

2025年1月にリリースされたRU23.7において、ONNXフォーマットを使ったDB内Embeddingで画像を扱えるようになりました。
一方で、画像のEmbeddingに対応したモデルを23ai向けにONNXエクスポートするには、OML4Py 2.1以上が必要となっています。
そのOML4Py 2.1はRU23.7がリリースされた時点で未だ公開されていなかったのですが、やっと先日使えるようになりました。
という訳で早速、画像のDB内Embeddingを試すべく、せっかくなのでマルチモーダルEmbeddingモデルの `clip-vit-large-patch14` を使って動作確認してみました。

なお、今回は OCI BaseDB の RU23.7 を使いました。
またマルチモーダル検索の動作確認にはAPEXで作った簡易アプリを利用しています。
以下のようにテキストで画像を検索するように実装しました。

![WS000004.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/076c48bb-624f-4f3e-a667-fc488a8fa6d2.jpeg)

また画像で画像を検索する機能も試してみました。

![WS000005.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/b1b67569-dc6f-4f31-9879-1b135cd0470e.jpeg)

本検証用アプリは[こちら](https://github.com/mago1chi/apex/blob/main/aivector_multimodal.sql)からエクスポートしたファイルを取得できます。

検証用の画像は kaggle にて `CC0: Public Domain` で公開されている以下データセットを使用しました。
https://www.kaggle.com/datasets/pavansanagapati/images-dataset

以降は検証手順のメモです。

## 検証手順

### OML4Py 2.1のインストール

OML4Pyに必要なOSパッケージをインストール

```bash
$ sudo su -
## 不足パッケージの確認
$ rpm -qa perl-Env
$ rpm -qa libffi-devel
$ rpm -qa openssl 
$ rpm -qa openssl-devel
$ rpm -qa tk-devel
$ rpm -qa xz-devel
$ rpm -qa zlib-devel
$ rpm -qa bzip2-devel
$ rpm -qa readline-devel
$ rpm -qa libuuid-devel
$ rpm -qa ncurses-devel

## 不足分をインストール
$ yum install perl-Env libffi-devel tk-devel bzip2-devel readline-devel libuuid-devel ncurses-devel
```

Python仮想環境の構築にあたりminicondaをインストール

```bash
## Anacondaインストール
$ mkdir -p ~/miniconda3
$ wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda3/miniconda.sh
$ bash ~/miniconda3/miniconda.sh -b -u -p ~/miniconda3

## 不要ファイル削除
$ rm -rf ~/miniconda3/miniconda.sh

## Anaconda初期セットアップ
$ ~/miniconda3/bin/conda init bash
```

Python仮想環境を作成

```bash
$ conda create -n mltebd python=3.12
## 仮想環境のアクティベート
$ conda activate mltebd
```

pipを最新化

```bash
$ python3 -m pip install --upgrade pip
```

OML4Pyに必要なPythonパッケージをインストール

```bash
$ pip install pandas
$ pip install setuptools
$ pip install scipy
$ pip install matplotlib
$ pip install oracledb
$ pip install joblib
$ pip install scikit-learn
$ pip install numpy
$ pip install onnxruntime
$ pip install onnxruntime-extensions
$ pip install onnx
$ pip install --extra-index-url "https://download.pytorch.org/whl/cpu" torch
$ pip install transformers
$ pip install sentencepiece
```

以下サイトからOML4Py 2.1 以上をダウンロードし、サーバに配置
https://www.oracle.com/database/technologies/oml4py-downloads.html

zipファイルを解凍し、Perlスクリプトを使ってOML4Pyをインストール

```bash
$ mkdir oml4py
$ cd oml4py
$ unzip V1048628-01.zip
$ perl -Iclient client/client.pl

Oracle Machine Learning for Python 2.1 Client.

Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved.
Checking platform .................. Pass
Checking Python .................... Pass
Checking dependencies .............. Pass
Checking OML4P version ............. Pass
Current configuration
  Python Version ................... 3.12.9
  PYTHONHOME ....................... /home/oracle/miniconda3/envs/mltebd
  Existing OML4P module version .... None

  Operation ........................ Install/Upgrade

Proceed? [yes]

Processing ./client/oml-2.1-cp312-cp312-linux_x86_64.whl
Installing collected packages: oml
Successfully installed oml-2.1

Done
```

### ONNXのエクスポート

以下マニュアルに沿って作業を進めます。
https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/onnx-pipeline-models-multi-modal-embedding.html
> 1. Exporting a pre-configured image model to a file

`clip-vit-large-patch14` のONNXファイル作成 (テキスト向けと画像向けの2種類を生成)

```python
from oml.utils import ONNXPipeline
pipeline = ONNXPipeline("openai/clip-vit-large-patch14")
pipeline.export2file("clip")
```

これで `clip_txt.onnx` と `clip_img.onnx` の2つが作成されます。

### 検証用DBユーザ作成、権限付与

```sql
GRANT DB_DEVELOPER_ROLE TO dmuser identified by your_password;
GRANT CREATE MINING MODEL TO dmuser;
ALTER USER dmuser QUOTA UNLIMITED ON USERS;
```

### ONNX用のディレクトリオブジェクト作成

```sql
CREATE OR REPLACE DIRECTORY dm_dump AS '/u01/app/mltebd';
GRANT READ ON DIRECTORY dm_dump TO dmuser;
GRANT WRITE ON DIRECTORY dm_dump TO dmuser;
```

### ONNXをインポート

`clip_txt.onnx` と `clip_img.onnx` をDBにインポート

```sql
EXECUTE DBMS_VECTOR.LOAD_ONNX_MODEL( 'dm_dump', 'clip_txt.onnx', 'clip_txt');
EXECUTE DBMS_VECTOR.LOAD_ONNX_MODEL( 'dm_dump', 'clip_img.onnx', 'clip_img');
```

インポートされたことの確認

```sql
col model_name format a20
col algorithm_name format a20
col algorithm format a20
col attribute_name format a20
col attribute_type format a20
col data_type format a20 

SELECT model_name, attribute_name, attribute_type, data_type, vector_info
FROM user_mining_model_attributes
WHERE model_name IN ('CLIP_TXT', 'CLIP_IMG')
ORDER BY ATTRIBUTE_NAME;

MODEL_NAME	     ATTRIBUTE_NAME	  ATTRIBUTE_TYPE       DATA_TYPE	    VECTOR_INFO
-------------------- -------------------- -------------------- -------------------- --------------------------------------------------------
CLIP_TXT	     DATA		  TEXT		       VARCHAR2
CLIP_IMG	     DATA		  UNSTRUCTURED	       BLOB
CLIP_IMG	     ORA$ONNXTARGET	  VECTOR	       VECTOR		    VECTOR(768,FLOAT32)
CLIP_TXT	     ORA$ONNXTARGET	  VECTOR	       VECTOR		    VECTOR(768,FLOAT32)
```

### データセット用のディレクトリオブジェクト作成

```sql
CREATE OR REPLACE DIRECTORY dataset_dir AS '/u01/app/dataset';
GRANT READ ON DIRECTORY dataset_dir TO dmuser;
GRANT WRITE ON DIRECTORY dataset_dir TO dmuser;
```

### データセットを入れるための表を作成-

```sql
CREATE TABLE my_table (
    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,
    file_name VARCHAR2(255),
    mime VARCHAR2(100),
    image BLOB,
    embedding VECTOR
);
```

### 表に各データセットの ファイル名、画像バイナリ、画像のベクトルデータ を格納

画像をベクトル化しつつ、表にデータを入れるPythonスクリプトを作成して実行
(なぜかjpgしかベクトル化されなかった)

```python
import os
import oracledb
from oracledb import DB_TYPE_BLOB

# Oracle DB接続情報
user = 'dmuser'
password = 'your_password'
dsn = 'localhost:1521/pdb1'

# BaseDBはNNEによる暗号化を使っているため、
# thick modeで利用しないとDB接続できない
oracledb.init_oracle_client()

connection = oracledb.connect(user=user, password=password, dsn=dsn)

# ディレクトリ内のファイルを取得
directory_path = '/u01/app/dataset'
files = os.listdir(directory_path)

# 各ファイルの内容を読み取り、DBにINSERT
for file_name in files:
    file_path = os.path.join(directory_path, file_name)
    with open(file_path, 'rb') as file:
        content = file.read()
        cr = connection.cursor()
        
        # 画像の読み込み
        blob = cr.var(DB_TYPE_BLOB)
        blob.setvalue(0, content)
        
        # 画像をベクトル化しつつINSERT
        insert_query = "INSERT INTO my_table (file_name, mime, image, embedding) VALUES (:1, 'image/jpeg', :2, VECTOR_EMBEDDING(CLIP_IMG USING TO_BLOB(:2) as DATA))"
        cr.execute(insert_query, [file_name, blob])
        cr.close()

# コミットして接続を閉じる
connection.commit()
connection.close()
```

## APEX、ORDSのインストール

APEXの最新版を取得

```bash
$ curl -OL https://download.oracle.com/otn_software/apex/apex-latest.zip
$ unzip apex-latest.zip 
```

APEXの静的ファイルの格納先を用意

```bash
$ mkdir i
$ cp -r -p apex/images i/24.2.0
```

PDBにログインしてインストール

```bash
$ cd apex
$ export NLS_LANG=American_America.AL32UTF8
$ sqlplus / as sysdba
```

```sql
SQL> alter session set container=pdb1;
SQL> @apexins SYSAUX SYSAUX TEMP /i/24.2.0/
```

APEXの日本語対応

```sql
SQL> @load_trans JAPANESE
```

APEX管理ユーザのパスワード変更

```sql
SQL> @apxchpwd
```

APEX管理ユーザのアンロック

```sql
SQL> ALTER USER APEX_PUBLIC_USER NO AUTHENTICATION ACCOUNT UNLOCK;
```

ORDSインストールのため、JDK 21をインストール

```bash
$ curl -OL https://download.oracle.com/java/21/latest/jdk-21_linux-x64_bin.rpm
$ dnf -y localinstall jdk-21_linux-x64_bin.rpm
$ java -version
java version "21.0.7" 2025-04-15 LTS
Java(TM) SE Runtime Environment (build 21.0.7+8-LTS-245)
Java HotSpot(TM) 64-Bit Server VM (build 21.0.7+8-LTS-245, mixed mode, sharing)
```

ORDSインストール

```bash
dnf -y --repofrompath ol8_oracle_software,http://yum.oracle.com/repo/OracleLinux/OL8/oracle/software/x86_64 install ords
```

ORDSの構成

```bash
$ su - oracle
$ export PATH=/usr/local/bin:$PATH
$ cd /etc/ords/config
$ ords install

Oracle REST Data Services - Interactive Install

  Enter a number to select the TNS net service name to use from /u01/app/oracle/product/23.0.0/dbhome_1/network/admin/tnsnames.ora or specify the database connection
    [1] ORCL_QZZ_KIX SERVICE_NAME=orcl_qzz_kix.pubsub1.tstosaka.oraclevcn.com    
    [2] PDB1         SERVICE_NAME=pdb1                                           
    [S] Specify the database connection
  Choose [1]: 2

Connecting to administrator user: SYS AS SYSDBA for container: PDB1 using bequeath connection
  Provide database user name with administrator privileges.
    Enter the administrator username: sys
  Enter the database password for SYS AS SYSDBA: 

Retrieving information.
ORDS is not installed in the database. ORDS installation is required.

  Enter a number to update the value or select option A to Accept and Continue
    [1] Connection Type: TNS
    [2] TNS Connection: TNS_NAME=PDB1 TNS_FOLDER=/u01/app/oracle/product/23.0.0/dbhome_1/network/admin
           Administrator User: SYS AS SYSDBA
    [3] Database password for ORDS runtime user (ORDS_PUBLIC_USER): <generate>
    [4] ORDS runtime user and schema tablespaces:  Default: SYSAUX Temporary TEMP
    [5] Additional Feature: Database Actions
    [6] Configure and start ORDS in Standalone Mode: Yes
    [7]    Protocol: HTTP
    [8]       HTTP Port: 8080
    [9]   APEX static resources location: 
    [A] Accept and Continue - Create configuration and Install ORDS in the database
    [Q] Quit - Do not proceed. No changes
  Choose [A]: 9
  Enter the APEX static resources location: /u01/app/apex/i
  Enter a number to update the value or select option A to Accept and Continue
    [1] Connection Type: TNS
    [2] TNS Connection: TNS_NAME=PDB1 TNS_FOLDER=/u01/app/oracle/product/23.0.0/dbhome_1/network/admin
           Administrator User: SYS AS SYSDBA
    [3] Database password for ORDS runtime user (ORDS_PUBLIC_USER): <generate>
    [4] ORDS runtime user and schema tablespaces:  Default: SYSAUX Temporary TEMP
    [5] Additional Feature: Database Actions
    [6] Configure and start ORDS in Standalone Mode: Yes
    [7]    Protocol: HTTP
    [8]       HTTP Port: 8080
    [9]   APEX static resources location: /u01/app/apex/i
    [A] Accept and Continue - Create configuration and Install ORDS in the database
    [Q] Quit - Do not proceed. No changes
  Choose [A]: 
...(略)...
2025-04-21T15:17:55.801Z INFO        Oracle REST Data Services initialized
Oracle REST Data Services version : 25.1.0.r1001652
Oracle REST Data Services server info: jetty/12.0.13
Oracle REST Data Services java info: Java HotSpot(TM) 64-Bit Server VM  (build: 21.0.7+8-LTS-245 mixed mode, sharing)
```

ORDSの自動起動を有効化

```bash
$ sudo su -
$ systemctl enable ords
$ systemctl stop ords
$ systemctl start ords
```

APEX管理画面へアクセスできるよう、BaseDBのポートを開放

```bash
$ sudo iptables -I INPUT 8 -p tcp -m state --state NEW -m tcp --dport 8080 -j ACCEPT -m comment --comment "Required for APEX"

$ sudo /sbin/service iptables save
iptables: Saving firewall rules to /etc/sysconfig/iptables: [  OK  ]
```

以下よりAPEX管理画面へアクセスできることを確認
http://your_public_ip:8080/apex/apex_admin

### 表内の画像をテキストでベクトル検索するAPEXアプリを作成

画面作成は省略しますが、テキスト入力のフォームとボタンを用意します。
検索結果の表示には対話モード・レポートを使いました。

![WS000006.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/61816a82-e3bb-48cb-965b-f7b4f0e7b3eb.jpeg)

対話モード・レポートのSQL問い合わせに以下SQLを入力するだけで、テキストから画像を検索するマルチモーダル検索が実装できちゃいます。

```sql
SELECT "ROWID","ID","FILE_NAME","MIME",SYS.DBMS_LOB.GETLENGTH("IMAGE")"IMAGE","EMBEDDING" FROM "MY_TABLE"
ORDER BY VECTOR_DISTANCE(embedding, 
    TO_VECTOR(VECTOR_EMBEDDING(CLIP_TXT USING :P5_SEARCH as DATA)), COSINE)
FETCH EXACT FIRST 3 ROWS ONLY
```

色々と試してみます。
`cat` で検索
![WS000000.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/f03dbaf1-42d6-41d0-81d3-b15c1d8aefdc.jpeg)

`dog` で検索
![WS000003.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/0a39b1a5-a5f5-4353-ba7b-2974d11e2d15.jpeg)

`tree` で検索
![WS000001.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/563b9262-0e9f-4a1c-99d3-9df890b89ae7.jpeg)

`water` で検索
![WS000002.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/16f5a137-e69e-4868-a815-e2e802283afc.jpeg)

## 表内の画像を画像でベクトル検索するAPEXアプリを作成

ベクトル検索結果を一時格納する表を作成

```sql
CREATE TABLE res_table (
    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,
    file_name VARCHAR2(255),
    mime VARCHAR2(100),
    image BLOB
);
```

検索に使う画像をアップロードするアイテムを追加し、
検索結果の表示用に対話モード・レポートを使用します。

![WS000007.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/6dfc493e-dd06-4fd2-9da4-e5775fa81dda.jpeg)

対話モード・レポートのSQL問合せに以下を入力

```sql
SELECT "ROWID","ID","FILE_NAME","MIME",SYS.DBMS_LOB.GETLENGTH("IMAGE")"IMAGE" FROM "RES_TABLE" ORDER BY "ID"
```

以下ベクトル検索処理をAPEXのプロセスとして定義し、
検索ボタンを押すと実行されるように設定します。

```sql
DECLARE
    l_file apex_application_temp_files%rowtype;
BEGIN
    -- 一時結果格納表をリセット
    DELETE FROM res_table;
    
    -- 検索用にアップロードされた画像の情報を抽出
    SELECT * INTO l_file
        FROM APEX_APPLICATION_TEMP_FILES
        WHERE name IN 
        (
            SELECT column_value
            FROM APEX_STRING.SPLIT(:P6_SEARCH, ':')
        );
    
    -- 一時結果格納表へデータ保存
    INSERT INTO res_table (file_name, mime, image)
        SELECT file_name, mime, image 
            FROM my_table
            ORDER BY VECTOR_DISTANCE(embedding,
                TO_VECTOR(VECTOR_EMBEDDING(CLIP_IMG USING l_file.BLOB_CONTENT as DATA)), COSINE)
        FETCH EXACT FIRST 3 ROWS ONLY;
    
    COMMIT;
END;
```

動作確認します。
犬の画像で検索
![WS000008.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/7ffec39f-1374-4cc0-bf49-fc7dd9a58453.jpeg)

馬の画像で検索
![WS000009.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/a184c676-77fc-4827-8085-ea292c4f5d5f.jpeg)

以上、RU23.7 + OML4Py 2.1 でマルチモーダル検索を実装できました。
テキストからの画像検索はSQL一行で実装できてしまい、とても簡単です。
一方で現段階はインポート可能なONNXファイルのサイズ上限が1GBと厳しく、多言語対応のマルチモーダルEmbeddingモデルを扱うのが難しい仕様となっています。
検索ワードに日本語を使う上で、この点については今後の改善が待たれます。