Oracle Database 23ai のベクトル検索を試す

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410176/3efc3b16-7679-74bd-368f-955e6bc266f6.png)

昨年末から待っていた、ベクトル検索機能を搭載した Oracle Database 23c のアップデート 23.4.0（どういうわけか 23c ではなく **23ai** になったらしい…）がようやくリリースされました。

ベクトル検索のドキュメント「[Oracle AI Vector Search User's Guide](https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/index.html)」を見てみると、言語モデルに問い合わせてベクトル表現を取得したり、モデル自体をデータベースに格納したり、SQL でプロンプトを投げてみたりと、SQL 世界で RAG を実現しているようです。

とはいえ、RAG のフローをアプリ側で実装している多くのエンジニアの方にとって、**データベースに求めていること**はスケーラブルなベクトル検索（と SQL 処理との組み合わせや、確立されたデータベース運用）なのではないかと思います。

そこで、この記事では、ベクトル検索に的を絞って、最速で試してみる手順を紹介したいと思います。最後まで読んでいただくと、こちらの SQL 謎記法が出てきます。

```sql
SELECT g2.embedding <=> g1.embedding AS distance
```

ベクトル検索の索引については [こちらの記事](https://qiita.com/ryotayamanaka/items/d1e665ceaa7152b22279) を参照してください。

# Docker 環境の用意

今回は Oracle Cloud 上に作成した AMD の Oracle Linux 8 のインスタンスを使いました。その場合、以下のように Podman をインストールしますが、お手元に Docker 環境がある場合にはここはスキップしてください。

```
sudo yum install podman podman-docker
```

1521 ポートを使うので、ファイアーウォールで許可しておきます。クラウド環境の場合には、仮想ネットワークでも 1521 ポートを開けておいてください。

```
sudo firewall-cmd --list-all
sudo firewall-cmd --zone=public --add-port=1521/tcp --permanent
sudo firewall-cmd --reload
```

# Database 23ai の起動

Docker または Podman を使用してコンテナをダウンロードして起動します。このコンテナの詳細は[こちら](https://container-registry.oracle.com/ords/f?p=113:4:115684569830975:::4:P4_REPOSITORY,AI_REPOSITORY,AI_REPOSITORY_NAME,P4_REPOSITORY_NAME,P4_EULA_ID,P4_BUSINESS_AREA_ID:1863,1863,Oracle%20Database%20Free,Oracle%20Database%20Free,1,0&cs=3gr78NummVdiYLvstxVU5afz8QyB__ZdygEW7oyF1MRTOH_DkEFDNyBJF1I6qS19npnLJEEl6GXbphzCmZVl7WA)に書かれている通りで、amd64 アーキテクチャ対応で解凍後のサイズで 10 GB 程度あることに注意してください。

```
docker run -p 1521:1521 --name database-23ai \
container-registry.oracle.com/database/free:23.4.0.0
```

このメッセージが出てきたらデータベースの起動成功です。

```
...
#########################
DATABASE IS READY TO USE!
#########################
...
```

Ctrl + C などでコンテナを停止させた場合には再度起動します。

```
docker start database-23ai
```

管理者ユーザーのパスワードを設定します。

```
docker exec -it database-23ai ./setPassword.sh password123
```

データベースに管理者ユーザー（sys）でログインします。

```
docker exec -it database-23ai sqlplus sys/password123@freepdb1 as sysdba
```

# ユーザーの作成

通常のデータベースユーザー **myuser** を作成して、CONNECT ロールと RESOURCE ロールを与えます。

```sql
CREATE USER myuser
IDENTIFIED BY password123
DEFAULT TABLESPACE users
TEMPORARY TABLESPACE temp
QUOTA UNLIMITED ON users;

GRANT CONNECT, RESOURCE TO myuser;
```

一度、ログアウトします。

```sql
exit
```

新しいユーザーで接続できることを確認します。

```
docker exec -it database-23ai sqlplus myuser/password123@freepdb1
```

# VECTOR 型データを格納

VECTOR 型の **embedding** 列を持つ表を作成します。この表は、複数の銀河の名前と短い説明文、そして説明文を言語モデルによってベクトル表現に変換したものを格納します。

```sql
CREATE TABLE galaxies (
  id NUMBER
, name VARCHAR2(50)
, doc VARCHAR2(500)
, embedding VECTOR
);
```

9 つの銀河のデータを格納します。上の表定義と元データは公式ドキュメントの[このページ](https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/insert-vectors-database-table-using-insert-statement.html)から拝借したものですが、ベクトルデータだけは簡略化されすぎていたため、実際に言語モデルを使って生成したベクトルに置き換えました。その手順は記事の最後に参考として記載しています。

**変更前のベクトルデータ**

```
M31: [0,2,2,0,0]
M33: [0,0,1,0,0]
M58: [1,1,1,0,0]
```

**変更後のベクトルデータ**（5 次元に合わせましたが、もっと高次元でも格納できます）

```
M31: [0.26833928, 0.012467232, -0.48890606, 0.61341953, 0.5590402]
M33: [0.43291375, -0.06379729, -0.40366283, 0.77222455, 0.2219036]
M58: [-0.07266286, 0.0802545, -0.34327424, 0.8917586, 0.27424216]
```

次のように、ベクトルを文字列として INSERT 文に渡せるそうです。

```sql
INSERT INTO galaxies VALUES (1, 'M31', 'Messier 31 is a barred spiral galaxy in the Andromeda constellation which has a lot of barred spiral galaxies.', '[0.26833928, 0.012467232, -0.48890606, 0.61341953, 0.5590402]');
INSERT INTO galaxies VALUES (2, 'M33', 'Messier 33 is a spiral galaxy in the Triangulum constellation.', '[0.43291375, -0.06379729, -0.40366283, 0.77222455, 0.2219036]');
INSERT INTO galaxies VALUES (3, 'M58', 'Messier 58 is an intermediate barred spiral galaxy in the Virgo constellation.', '[-0.07266286, 0.0802545, -0.34327424, 0.8917586, 0.27424216]');
INSERT INTO galaxies VALUES (4, 'M63', 'Messier 63 is a spiral galaxy in the Canes Venatici constellation.', '[0.16099164, -0.28416803, -0.32071748, 0.7423811, 0.4892247]');
INSERT INTO galaxies VALUES (5, 'M77', 'Messier 77 is a barred spiral galaxy in the Cetus constellation.', '[0.43336692, -0.20248333, -0.38971466, 0.6521541, 0.44046697]');
INSERT INTO galaxies VALUES (6, 'M91', 'Messier 91 is a barred spiral galaxy in the Coma Berenices constellation.', '[0.41154075, 0.14537011, -0.42996794, 0.33680823, 0.7149753]');
INSERT INTO galaxies VALUES (7, 'M49', 'Messier 49 is a giant elliptical galaxy in the Virgo constellation.', '[0.45378348, 0.37351337, -0.33156702, 0.7252909, 0.13632572]');
INSERT INTO galaxies VALUES (8, 'M60', 'Messier 60 is an elliptical galaxy in the Virgo constellation.', '[0.124404885, 0.1522709, -0.27538168, 0.9206997, 0.19445813]');
INSERT INTO galaxies VALUES (9, 'NGC1073', 'NGC 1073 is a barred spiral galaxy in Cetus constellation.', '[-0.11507724, -0.3145153, -0.42180404, 0.50861514, 0.67173606]');
COMMIT;
```

# ベクトルの近傍検索

説明文を基にして ID = 1 の銀河と他の銀河の距離を算出し、距離が近い順（＝説明文が似ている順）に並べてみます。

この 3 行の `col ...` は、上の流れで sqlplus を使っている場合に結果を整形するためのものなので、他の SQL クライアントをお使いの場合には削除してください。

```sql
col galaxy_1 for a10
col galaxy_2 for a10
col distance for 0.00000000000000000
```

こちらが 23ai で新しく追加された `VECTOR_DISTANCE` 関数です。

```sql
SELECT
  g1.name AS galaxy_1
, g2.name AS galaxy_2
, VECTOR_DISTANCE(g2.embedding, g1.embedding) AS distance
FROM galaxies g1, galaxies g2
WHERE g1.id = 1
ORDER BY distance ASC;
```

この関数は、2 つのベクトルの距離を次のような数値で返します。

```
GALAXY_1   GALAXY_2   DISTANCE
---------- ---------- --------------------
M31	       M31	      -0.00000011920928955
M31	       M77	       0.04941833019256592
M31	       M91	       0.07123643159866333
M31	       M63	       0.07465422153472900
M31	       M33	       0.08952367305755615
M31	       NGC1073     0.14105635881423950
M31	       M58	       0.15033429861068726
M31	       M60	       0.15659791231155396
M31	       M49	       0.19035106897354126
```

# 距離の計算方法の指定

上の場合は関数の第三引数である距離の計算方法（distance metric）が指定されていないため、デフォルトの「コサイン距離」が計算されています。次のように `COSINE` が指定されている場合と同様です。

```sql
SELECT
  g1.name AS galaxy_1
, g2.name AS galaxy_2
, VECTOR_DISTANCE(g2.embedding, g1.embedding, COSINE) AS distance
FROM galaxies g1, galaxies g2
WHERE g1.id = 1
ORDER BY distance ASC;
```

[SQL リファレンス](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/vector_distance.html)によるとコサイン距離以外に指定できる計算方法は `DOT` / `EUCLIDEAN` / `EUCLIDEAN_SQUARED` / `HAMMING` / `MANHATTAN` ということですが、ここでは計算方法の詳細に踏み込まず `DOT` / `EUCLIDEAN` の実行結果を掲載します。今回のデータでは結果の順序は共にコサイン距離のときと同じになりました。

```sql
SELECT
  g1.name AS galaxy_1
, g2.name AS galaxy_2
, VECTOR_DISTANCE(g2.embedding, g1.embedding, DOT) AS distance
FROM galaxies g1, galaxies g2
WHERE g1.id = 1
ORDER BY distance ASC;
```
```
GALAXY_1   GALAXY_2   DISTANCE
---------- ---------- --------------------
M31	   	   M31	      -0.99999994039535522
M31	   	   M77	      -0.95058161020278931
M31	   	   M91	      -0.92876350879669189
M31	   	   M63	      -0.92534565925598145
M31	   	   M33	      -0.91047626733779907
M31	   	   NGC1073    -0.85894358158111572
M31	   	   M58	      -0.84966564178466797
M31	   	   M60	      -0.84340202808380127
M31	   	   M49	      -0.80964887142181396
```

ユークリッド距離の場合は M31 同士の距離はゼロになっています。

```sql
SELECT
  g1.name AS galaxy_1
, g2.name AS galaxy_2
, VECTOR_DISTANCE(g2.embedding, g1.embedding, EUCLIDEAN) AS distance
FROM galaxies g1, galaxies g2
WHERE g1.id = 1
ORDER BY distance ASC;
```
```
GALAXY_1   GALAXY_2   DISTANCE
---------- ---------- --------------------
M31	   	   M31	       0.00000000000000000
M31	   	   M77	       0.31438317894935608
M31	   	   M91	       0.37745609879493713
M31	   	   M63	       0.38640478253364563
M31	   	   M33	       0.42314010858535767
M31	   	   NGC1073     0.53114295005798340
M31	   	   M58	       0.54833269119262695
M31	   	   M60	       0.55963915586471558
M31	   	   M49	       0.61701065301895142
```

# 謎のベクトル距離記法！

先の [SQL リファレンス](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/vector_distance.html)を見ていて、えっ？てなったのがこちらです。上のコサイン距離を求める `VECTOR_DISTANCE` 関数の代替として `<=>` という記法があります、と書いてあります。実際に次のようなクエリを試してみると：

```sql

SELECT
  g1.name AS galaxy_1
, g2.name AS galaxy_2
, g2.embedding <=> g1.embedding AS distance
FROM galaxies g1, galaxies g2
WHERE g1.id = 1
ORDER BY distance ASC;

```
```
GALAXY_1   GALAXY_2   DISTANCE
---------- ---------- --------------------
M31	       M31	      -0.00000011920928955
M31	       M77	       0.04941833019256592
M31	       M91	       0.07123643159866333
M31	       M63	       0.07465422153472900
M31	       M33	       0.08952367305755615
M31	       NGC1073     0.14105635881423950
M31	       M58	       0.15033429861068726
M31	       M60	       0.15659791231155396
M31	       M49	       0.19035106897354126
```

確かに同じ結果が返ってきました。正直、この記事を書いてしまった動機なのですが、（法律的に）**こんなに勝手な SQL 文法**を作ってよいものでしょうか？

ちなみにユークリッド距離は `<->`：

```sql
SELECT
  g1.name AS galaxy_1
, g2.name AS galaxy_2
, g2.embedding <-> g1.embedding AS distance
FROM galaxies g1, galaxies g2
WHERE g1.id = 1
ORDER BY distance ASC;
```

ドット積は `<#>`：

```sql
SELECT
  g1.name AS galaxy_1
, g2.name AS galaxy_2
, g2.embedding <#> g1.embedding AS distance
FROM galaxies g1, galaxies g2
WHERE g1.id = 1
ORDER BY distance ASC;
```

だそうです。確かに見やすい気もするのですが、ちょっと戸惑いますね！

# 参考）ベクトルの取得

この記事で用いたベクトル表現は OpenAI API を使って以下のように取得しました。INSERT 文をテキストで取得していますが、実際には Python から直接 Oracle Database に接続してクエリを実行することもあるかと思います。

```python
from openai import OpenAI
client = OpenAI()
galaxies = [
    {"id":1, "name":'M31', "doc":'Messier 31 is a barred spiral galaxy in the Andromeda constellation which has a lot of barred spiral galaxies.'},
    {"id":2, "name":'M33', "doc":'Messier 33 is a spiral galaxy in the Triangulum constellation.'},
    {"id":3, "name":'M58', "doc":'Messier 58 is an intermediate barred spiral galaxy in the Virgo constellation.'},
    {"id":4, "name":'M63', "doc":'Messier 63 is a spiral galaxy in the Canes Venatici constellation.'},
    {"id":5, "name":'M77', "doc":'Messier 77 is a barred spiral galaxy in the Cetus constellation.'},
    {"id":6, "name":'M91', "doc":'Messier 91 is a barred spiral galaxy in the Coma Berenices constellation.'},
    {"id":7, "name":'M49', "doc":'Messier 49 is a giant elliptical galaxy in the Virgo constellation.'},
    {"id":8, "name":'M60', "doc":'Messier 60 is an elliptical galaxy in the Virgo constellation.'},
    {"id":9, "name":'NGC1073', "doc":'NGC 1073 is a barred spiral galaxy in Cetus constellation.'}
]
for galaxy in galaxies:
    embedding_response = client.embeddings.create(
        model = "text-embedding-3-large",
        input = galaxy["doc"],
        dimensions = 5
    )
    str_embedding = str(embedding_response.data[0].embedding)
    str_insert = "INSERT INTO galaxies VALUES (" + str(galaxy["id"]) + ", '" + galaxy["name"] + "', '" + galaxy["doc"] + "', '" + str_embedding + "');"
    print(str_insert)
```
```
INSERT INTO galaxies VALUES (1, 'M31', 'Messier 31 is a barred spiral galaxy in the Andromeda constellation which has a lot of barred spiral galaxies.', '[0.26833928, 0.012467232, -0.48890606, 0.61341953, 0.5590402]');
INSERT INTO galaxies VALUES (2, 'M33', 'Messier 33 is a spiral galaxy in the Triangulum constellation.', '[0.43291375, -0.06379729, -0.40366283, 0.77222455, 0.2219036]');
INSERT INTO galaxies VALUES (3, 'M58', 'Messier 58 is an intermediate barred spiral galaxy in the Virgo constellation.', '[-0.07266286, 0.0802545, -0.34327424, 0.8917586, 0.27424216]');
INSERT INTO galaxies VALUES (4, 'M63', 'Messier 63 is a spiral galaxy in the Canes Venatici constellation.', '[0.16099164, -0.28416803, -0.32071748, 0.7423811, 0.4892247]');
INSERT INTO galaxies VALUES (5, 'M77', 'Messier 77 is a barred spiral galaxy in the Cetus constellation.', '[0.43336692, -0.20248333, -0.38971466, 0.6521541, 0.44046697]');
INSERT INTO galaxies VALUES (6, 'M91', 'Messier 91 is a barred spiral galaxy in the Coma Berenices constellation.', '[0.41154075, 0.14537011, -0.42996794, 0.33680823, 0.7149753]');
INSERT INTO galaxies VALUES (7, 'M49', 'Messier 49 is a giant elliptical galaxy in the Virgo constellation.', '[0.45378348, 0.37351337, -0.33156702, 0.7252909, 0.13632572]');
INSERT INTO galaxies VALUES (8, 'M60', 'Messier 60 is an elliptical galaxy in the Virgo constellation.', '[0.124404885, 0.1522709, -0.27538168, 0.9206997, 0.19445813]');
INSERT INTO galaxies VALUES (9, 'NGC1073', 'NGC 1073 is a barred spiral galaxy in Cetus constellation.', '[-0.11507724, -0.3145153, -0.42180404, 0.50861514, 0.67173606]');
```

# 次は性能

とりあえずは SQL の新しい文法を試してみましたが、大量のベクトルをデータベースに格納するのであれば、その検索性能が重要です。次回はベクトル検索のために[新しく導入された索引](https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/perform-approximate-similarity-search-using-vector-indexes.html)について試していきたいと思います。

* 次の記事：[Oracle 23ai のベクトル索引で近傍検索を高速化](https://qiita.com/ryotayamanaka/items/d1e665ceaa7152b22279)