ベクトル値をバインド変数として持たせたい

# Oracle Database 23ai 新機能: ベクトル検索
Oracle Database 23aiの新機能として、ベクトル検索が実装されています。
検索する内容に合わせて適切な事前トレーニング済基礎モデルを呼び出し、テーブル内の検索対象となる列をベクトル値へと変換 (エンベディング) します。
エンベディングによって得られたベクトル値をVECTOR型として、テーブルに格納することが可能となっています。
このテーブル内を検索する目的で作成した質問文を同じ方法でエンベディングし、関数を使用することでテーブルの各行とのベクトル距離を計算し、近い順に出力する近傍探索ができます。
[Oracleで使用可能な事前トレーニング済基礎モデル](https://docs.oracle.com/ja-jp/iaas/Content/generative-ai/pretrained-models.htm)

# 背景
実際にベクトル検索をしたことがなかったので、サンプルとなる適当なテキストデータをテーブルとして格納して試してみました。
その時に思ったのがこの記事のタイトルになっている**ベクトル値をバインド変数として持たせたい**ということでした。

## 検証環境
Oracle Autonomous Database 23ai 
* Oracle Autonomous Transaction Processing
* バージョン 23.8.0.25.05
* 8 ECPU

:::note warn
ここからの情報は、2025年5月検証時の情報です。
:::

# ベクトル検索の実装
検証する前にこんな感じでベクトル検索できないかなーと考えていました。

:::note
1\. PL/SQL: 検索対象テーブルのエンベディング
2\. PL/SQL: 質問文のエンベディング
3\. SQL: ベクトル検索 (テーブル内と質問文のベクトル値の比較)
:::

上記内容を1つのPL/SQLで実装しようとしたのですが、検索対象テーブルが大きすぎて(30万行ほど) 実行時間が長くなってしまうため、1つにまとめることはできませんでした。
それぞれバラバラのPL/SQLとSQLを作成して実行する方法を考えました。

**3\. SQL: ベクトル検索 (テーブル内と質問文のベクトル値の比較)**
をする時に
**2\. PL/SQL: 質問文のエンベディング**
で得られた質問文のベクトル値をSQL内で渡す必要があります。
このときの渡し方として、バインド変数を使用した実装を考えました。

## 2\. PL/SQL: 質問文のエンベディング
検索対象テーブル内のデータを探すような質問文をエンベディングして、質問文のベクトル値を取得します。
そしてそのベクトル値をバインド変数として持たせます。

まず、今回使用するモデルを呼び出すファンクション`embed_question`を作成します。

```
CREATE OR REPLACE FUNCTION embed_question(question_text IN CLOB)
RETURN vector IS
    v vector;
BEGIN
    SELECT embed_vector
    INTO v
    FROM TABLE(
        dbms_vector.utl_to_embeddings(
            question_text,
            json('{
                    "provider": "ocigenai", 
                    "credential_name": "OCI_CRED", 
                    "url": "https://inference.generativeai.ap-osaka-1.oci.oraclecloud.com/20231130/actions/embedText", 
                    "model": "cohere.embed-multilingual-v3.0"
                }')
        )
    ) 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;

    RETURN v;
END;
/
```

ファンクション`embed_question`を用いて質問文をエンベディングし、
得られたベクトル値をVECTOR型のバインド変数`question_vector`に格納します。
```
VARIABLE question_text CLOB
VARIABLE question_vector VECTOR
BEGIN
  :question_text := '<質問文>';
  :question_vector := embed_question(:question_text);
END;
/
```

これを実行したところ、以下のようなエラーが発生しました。
```
BEGIN
*
行1でエラーが発生しました。:
ORA-00600: 内部エラー・コード, 引数: [kollAdjustDuration - compile time VBL,
run time RBL], [], [], [], [], [], [], [], [], [], [], []
ヘルプ: https://docs.oracle.com/error-help/db/ora-00600/
```
このPL/SQLを切り分けて実行してみると、このエラーは
`VECTOR`型と宣言したバインド変数に対して
`:question_vector := embed_question(:question_text);`で質問文のベクトル値を格納していることによって発生していることが原因のようです。
すなわち、**VECTOR型をバインド変数として格納できない**ということがここでわかりました。

質問文のベクトル値をバインド変数としてSQLに記述したい・・
でもVECTOR型は使えない・・
そこで以下の方法によって、SQLに質問文のベクトル値を渡すことができました。

:::note
**1\. TO_VECTOR関数の使用**
    質問文のベクトル値をCLOB型で取得して、TO_VECTOR関数を用いてVECTOR型に変換
    
**2\. 一時表への格納**
    質問文のベクトル値を一時表に格納し、IDから参照
:::

### 1\. TO_VECTOR関数の使用
文字列をVECTOR型に変換する関数として、TO_VECTOR関数があります。

> TO_VECTORは、VARCHAR2、CLOB、BLOBまたはJSON型の文字列を入力として受け取り、それをベクトルに変換し、ベクトルを出力として返すコンストラクタです。
> [TO_VECTOR](https://docs.oracle.com/cd/G11854_01/sqlrf/to_vector.html)

質問文のベクトル値をCLOB型として格納します。
TO_VECTOR関数を活用してベクトル検索を行うSQL内でCLOB型からVECTOR型に変換した後、ベクトル検索をさせることができます。

先ほど作成したファンクション`embed_question`を使用してエンベディングを行うとベクトル値はVECTOR型として返ってきてしまうので、全く同じ内容でCLOB型を返してもらえるようなファンクション`embed_question_clob`を作成します。

```
CREATE OR REPLACE FUNCTION embed_question_clob(question_text IN CLOB)
RETURN clob IS -- VECTORではなくCLOBに変更
    v clob;    -- この行も同様
BEGIN
    SELECT embed_vector
    INTO v
    FROM TABLE(
        dbms_vector.utl_to_embeddings(
            question_text,
            json('{
                    "provider": "ocigenai", 
                    "credential_name": "OCI_CRED", 
                    "url": "https://inference.generativeai.ap-osaka-1.oci.oraclecloud.com/20231130/actions/embedText", 
                    "model": "cohere.embed-multilingual-v3.0"
                }')
        )
    ) 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;

    RETURN v;
END;
/
```
このファンクション`embed_question_clob`を使って、質問文のベクトル値をCLOB型のバインド変数`embedding`に格納します。
```
VARIABLE embedding CLOB
BEGIN
  :embedding := embed_question_clob('<質問文>');
END;
/
```
CLOB型ならバインド変数に格納できますので、エラーなく実行できました！


### 2\. 一時表への格納

:::note warn
こちらは、TO_VECTOR関数の存在を知る前に試した方法です。
1\. TO_VECTOR関数の使用 の方がシンプルな実装になるので、もしやり方を知れれば充分という方はskipしていただいて構いません。
:::

質問文のベクトル値はVECTOR型で取得して、ベクトル値をVECTOR型以外のデータ型をバインド変数に格納する方法です。
ベクトル値をVECTOR型以外のデータ型をバインド変数に格納する方法として考えたのが、一時表の利用です。

IDとベクトル値を格納できるような一時表を用意します。
```
CREATE GLOBAL TEMPORARY TABLE tmp_embedding( id NUMBER, embedding VECTOR ) ON COMMIT PRESERVE ROWS;
```

質問文を先ほど作成した`embed_question`ファンクションを使用してエンベディングします。
このとき、バインド変数`:question_id`として質問文のベクトル値に紐づくIDを格納します。
```
VARIABLE question_id NUMBER
BEGIN
  DECLARE
    v_question CLOB := '<質問文>';
    v_vector VECTOR;
    v_new_id NUMBER;
  BEGIN
    SELECT NVL(MAX(id), 0) + 1 INTO v_new_id FROM tmp_embedding;
    v_vector := embed_question(v_question);

    INSERT INTO tmp_embedding (id, embedding)
    VALUES (v_new_id, v_vector);

    :question_id := v_new_id;
  END;
END;
/
```
こちらのPL/SQLも、NUMBER型をバインド変数としたためエラーなく実行することができました！

## 3\. SQL: ベクトル検索
質問文へのベクトル値の渡し方によって、SQLが異なります。それぞれの渡し方に分けて紹介します。

### 1\. TO_VECTOR関数の使用
質問文のCLOB型であるベクトル値が格納されているバインド変数`:embedding`を
TO_VECTOR関数を用いてCLOB型からVECTOR型に変換します。
検索対象テーブルのベクトル値と質問文のベクトル値の2点に対して、VECTOR_DISTANCE関数を用いてベクトル距離を計算し、近い順に出力させます。

[VECTOR_DISTANCE関数](https://docs.oracle.com/cd/G11854_01/sqlrf/vector_distance.html)
> VECTOR_DISTANCEは、2つのベクトル間の距離を計算するために使用できるメインのファンクションです。

```
SELECT w.text
FROM <検索対象テーブル> w
ORDER BY VECTOR_DISTANCE ( w.embedding, TO_VECTOR(:embedding) )
FETCH APPROX FIRST 3 ROWS ONLY;
```

今回は、`サヴォワ地方はどこの国？`という質問をして以下の結果が返ってきました。
```
TEXT
--------------------------------------------------------------------------------
サヴォイ(Savoy)
フランス、サンタヴォルド
セルビア・モンテネグロ
```
問題なくベクトル検索できていることが確認できました！

:::note
ベクトル検索は自然言語で回答を生成してくれるわけではありません。
テーブル内のある列をエンベディングして得られたベクトル値 (今回はTEXT列をエンベディング) と質問文のベクトル値を比較して、ベクトル距離の近い行を出力します。
:::
サヴォワ地方はフランス南東の地域を指します。
それに関連する行が返ってきているので、問題なくベクトル検索できていると判断できます。


### 2\. 一時表への格納

一時表のIDである`question_id`をもとに一時表から質問文のベクトル値を引っ張ってきて、VECTOR_DISTANCE関数にそのベクトル値を渡して、検索対象テーブルに対してベクトル検索を実行します。
```
SELECT w.text
FROM <検索対象テーブル> w
WHERE 1=1
ORDER BY VECTOR_DISTANCE(
         w.embedding,
         (SELECT embedding FROM tmp_embedding WHERE id = :question_id)
       )
FETCH APPROX FIRST 3 ROWS ONLY;
```

こちらの方法でも、TO_VECTOR関数を使用した場合と同じ結果が得られました。
すなわち、問題なくベクトル検索ができています。

# まとめ
質問文のベクトル値をVECTOR型でバインド変数として持たせることができないので、以下の2つの方法でバインド変数を使用することができました。

:::note
**1\. TO_VECTOR関数の使用**
    質問文のベクトル値をCLOB型で取得して、TO_VECTOR関数を用いてVECTOR型に変換
    
**2\. 一時表への格納**
    質問文のベクトル値を一時表に格納し、IDから参照
:::

いつかVECTOR型もバインド変数にとして格納できればいいのですが・・
もし同じようなエラーに直面した時はこのような回避方法があるので活用してみてください！