昨年末から待っていた、ベクトル検索機能を搭載した Oracle Database 23c のアップデート 23.4.0(どういうわけか 23c ではなく 23ai になったらしい…)がようやくリリースされました。
ベクトル検索のドキュメント「Oracle AI Vector Search User's Guide」を見てみると、言語モデルに問い合わせてベクトル表現を取得したり、モデル自体をデータベースに格納したり、SQL でプロンプトを投げてみたりと、SQL 世界で RAG を実現しているようです。
とはいえ、RAG のフローをアプリ側で実装している多くのエンジニアの方にとって、データベースに求めていることはスケーラブルなベクトル検索(と SQL 処理との組み合わせや、確立されたデータベース運用)なのではないかと思います。
そこで、この記事では、ベクトル検索に的を絞って、最速で試してみる手順を紹介したいと思います。最後まで読んでいただくと、こちらの SQL 謎記法が出てきます。
SELECT g2.embedding <=> g1.embedding AS distance
ベクトル検索の索引については こちらの記事 を参照してください。
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 を使用してコンテナをダウンロードして起動します。このコンテナの詳細はこちらに書かれている通りで、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 ロールを与えます。
CREATE USER myuser
IDENTIFIED BY password123
DEFAULT TABLESPACE users
TEMPORARY TABLESPACE temp
QUOTA UNLIMITED ON users;
GRANT CONNECT, RESOURCE TO myuser;
一度、ログアウトします。
exit
新しいユーザーで接続できることを確認します。
docker exec -it database-23ai sqlplus myuser/password123@freepdb1
VECTOR 型データを格納
VECTOR 型の embedding 列を持つ表を作成します。この表は、複数の銀河の名前と短い説明文、そして説明文を言語モデルによってベクトル表現に変換したものを格納します。
CREATE TABLE galaxies (
id NUMBER
, name VARCHAR2(50)
, doc VARCHAR2(500)
, embedding VECTOR
);
9 つの銀河のデータを格納します。上の表定義と元データは公式ドキュメントのこのページから拝借したものですが、ベクトルデータだけは簡略化されすぎていたため、実際に言語モデルを使って生成したベクトルに置き換えました。その手順は記事の最後に参考として記載しています。
変更前のベクトルデータ
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 文に渡せるそうです。
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 クライアントをお使いの場合には削除してください。
col galaxy_1 for a10
col galaxy_2 for a10
col distance for 0.00000000000000000
こちらが 23ai で新しく追加された VECTOR_DISTANCE
関数です。
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
が指定されている場合と同様です。
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 リファレンスによるとコサイン距離以外に指定できる計算方法は DOT
/ EUCLIDEAN
/ EUCLIDEAN_SQUARED
/ HAMMING
/ MANHATTAN
ということですが、ここでは計算方法の詳細に踏み込まず DOT
/ EUCLIDEAN
の実行結果を掲載します。今回のデータでは結果の順序は共にコサイン距離のときと同じになりました。
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 同士の距離はゼロになっています。
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 リファレンスを見ていて、えっ?てなったのがこちらです。上のコサイン距離を求める VECTOR_DISTANCE
関数の代替として <=>
という記法があります、と書いてあります。実際に次のようなクエリを試してみると:
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 文法を作ってよいものでしょうか?
ちなみにユークリッド距離は <->
:
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;
ドット積は <#>
:
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 に接続してクエリを実行することもあるかと思います。
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 の新しい文法を試してみましたが、大量のベクトルをデータベースに格納するのであれば、その検索性能が重要です。次回はベクトル検索のために新しく導入された索引について試していきたいと思います。