MySQL 9.7のVECTOR型とVector Functionsを徹底解説(後編) 〜ベクトル検索の仕組みをSQLだけで実装してみよう〜

目次

はじめに

前編では、MySQL9.7のVECTOR型がどのようなデータ構造なのか、TO_VECTOR()/FROM_VECTOR()による相互変換、そして「次元に意味を持たせる」ためのVECTOR_ELEMENT()関数とflavor_dimensionテーブルの組み合わせまでを紹介しました。

後編となる今回は、以下2つのテーマについて紹介します。

・VECTOR_DIM() ─ ベクトルの次元数を確認する
・ベクトル検索との関係 ─ DISTANCE()関数と、Community版での代替アプローチ

データ準備

VECTOR型の検証用に、カレー屋さんの「辛さ」「コク」「酸味」「甘み」の4つの指標(各0.0〜10.0のスコア)を1本のベクトルとして持つテーブルを作成します。

なお、「クセスゴYAMITSUKIYA 本店」のメニューに載せてみたい…
栃木県産の「とちおとめ」を使用した『いちごカレー』、京都の名産である宇治抹茶をルーに合わせた『宇治抹茶カレー』、見た目がクリームシチュー、まろやかな甘みがありつつカレーの辛みが広がる北海道の『白い伽哩』などの『ご当地カレー』や『至高の一品』がある場合は、是非検証時にデータを追加してみてください!

もちろん、味の指標だけでなく、「独創性」、「中毒度」、「映え度」、「温冷感」、「ボリューム」などの指標も追加して、より深い検証を行っていくのも良さそうです!

flavor列が[辛さ, コク, 酸味, 甘み]の順で4つの数値を持つベクトルです。
まずはSELECT *で中身を見てみましょう

このように、VECTOR型は内部的にバイナリ形式で格納され、SELECTするとそのバイナリ表現が表示されます。

VECTOR_DIM() ─ ベクトルの次元数を確認する

VECTOR_DIM()は、VECTOR値が何個のエントリ(次元)を持っているかを返す関数です。
「このベクトル、本当に想定した次元数で入っているんだっけ?」を確認したいときに便利です。

データ量が少なければ、一覧を表示する形で確認することができます。
ただし何万行ものデータが登録されていた場合は…

WITH句やWindow関数を使うことで、
このように「謎肉入り焼き鳥カレー」だけ3次元であることがわかります。

ベクトル検索と利用する関数の概要

さて、ここまでは「VECTOR型に値を出し入れする」「意味を持たせて確認する」という話でしたが、本題とも言える「VECTOR型を使って類似度検索(ベクトル検索)ができるのか?」を見ていきます。

DISTANCE()関数について

MySQLには2つのベクトルの距離(=似ている度合い)を計算するDISTANCE()関数(エイリアス:VECTOR_DISTANCE())が用意されています。第3引数にCOSINE(コサイン距離)、DOT(内積)、EUCLIDEAN(ユークリッド距離)のいずれかを指定して計算方法を選びます。

もしこの関数が使えれば、「辛さ8、コク5、酸味6、甘み1くらいのカレーが食べたい気分」を1本のベクトルにして、それに近いカレーを1発のSQLで検索する、といったことが実現できそうです。

**ただし、DISTANCE()関数はMySQL HeatWave on OCIおよびMySQL AIのユーザー向けの機能**であり、MySQL CommunityやMySQL Enterprise Edition(Commercial Server)のディストリビューションには含まれていません。
手元のMySQL Community Serverで上記SQLを実行すると、以下のようなエラーになります。

DISTANCE()の3つの指標を整理する ─ EUCLIDEAN/COSINE/DOT

DISTANCE()(および自作のVECTOR_DISTANCE())は、第3引数で計算方法を切り替えられます。実際にSQLを動かす前に、この3つがそれぞれ何を計算しているのかを整理しておきます。

EUCLIDEAN(ユークリッド距離)

いわゆる普通の「直線距離」を表しており、各次元の差を2乗して足し合わせ、平方根を取ったもので算出します。

√Σ(v1ᵢ - v2ᵢ)²

  • 値が小さいほど近い(距離なので0が最小、上限なし)
  • ベクトルの「大きさ(長さ)」の違いにも敏感に反応する

例えば、カレーの「辛さ・コク」だけを取り出して考えると、「辛さ8, コク8」と「辛さ4, コク4」は方向(比率)としては全く同じでも、ユークリッド距離的には結構離れていると判定されます。今回の記事内のDOTのランキングで「追いバター追いチーズ追い背徳カリー」のような”全体的に数値が大きい”メニューが上位に来ていたのは、まさにこの性質が影響しています。

COSINE(コサイン類似度/距離)

2つのベクトルが指している”方向“がどれだけ近いかを見る指標です。ベクトルの長さ(大きさ)は無視して、角度だけで比較します。

cos θ = (v1・v2) / (|v1| × |v2|)

  • HeatwaveのDISTANCE()が返す”コサイン距離”は1 - cos θという実装になっているため、その場合は0に近いほど似ている
  • ベクトルの大きさに影響されないのが最大の特徴。「辛さ8, コク8」と「辛さ4, コク4」は方向が同じなので、コサイン的にはほぼ同じ(=非常に近い)と判定される

DOT(内積)

2つのベクトルの内積そのものです。

v1・v2 = Σ(v1ᵢ × v2ᵢ)

  • コサインの計算式から「ベクトルの長さで割る」部分を省いたもの、と考えると分かりやすい
  • 値が大きいほど似ている(=「近い」の意味がCOSINE/EUCLIDEANと逆転する点に注意)

DOTを「近さ」の指標として正しく使うためには、あらかじめすべてのベクトルの大きさ(ノルム)を1に正規化してある場合です。

ベクトルの長さを1に揃えてしまえば、内積とコサイン類似度は数式上一致する(|v1| = |v2| = 1ならv1・v2 = cos θになる)ので、DOTでもCOSINEと同じ結果が得られる想定となります。

これらの結果を表に纏めると以下のようになります。

指標 何を見ているか 「近い」の判定 ベクトルの大きさの影響
EUCLIDEAN 直線距離 値が小さいほど近い 受ける
COSINE 方向(角度) 値が1に近い(距離なら0に近い)ほど近い 受けない
DOT 方向×大きさ 値が大きいほど近い 受ける(要注意)

Community版で類似度検索をする ─ VECTOR_DISTANCE()を自作する

前述の通り、Community版ではDISTANCE()関数自体が存在しないため、
前編で作ったVECTOR_ELEMENT()を土台に、DISTANCE()と同じインターフェース(第3引数でEUCLIDEAN/DOT/COSINEを切り替え)を持つVECTOR_DISTANCE()を自作します。

今回は単に距離を計算するだけでなく、HeatWaveの**DISTANCE()**の挙動にできるだけ寄せるように、以下の2点を意識して作成しています。

  • 次元数が一致しない場合はSIGNALでエラーにするのではなく、DISTANCE()と同じくNULLを返す
  • COSINEは「類似度」ではなくDISTANCE()が返す「コサイン距離(1 - コサイン類似度)」に合わせ、浮動小数点誤差で理論上0になるはずの値も明示的に0で丸める

実際にHeatWaveとCommunity版で比較する

「バターチキンカリー」を基準に、全メニューとの距離を3つの指標でそれぞれ計算してみます。まずはHeatWave側です。

続いてCommunity側、DISTANCE()の代わりに自作したVECTOR_DISTANCE()を使って全く同じ内容を実行してみます。

いくつか結果を見比べてみます。

■ EUCLIDEAN

curry_name HeatWave (DISTANCE) Community (VECTOR_DISTANCE)
バターチキンカリー 0 0
思ひ出の熟成カリー(二日目) 2.0615527629852295 2.0615528128088303
カロリーは気にしたら負けカリー 2.7386128902435303 2.7386127875258306
店長の気まぐれスパイス暴走カリー 8.336666107177734 8.336666000266533
謎肉入り焼き鳥カレー(3次元) NULL NULL

■ COSINE

curry_name HeatWave Community
バターチキンカリー 0.00000005960464477539063 0(丸め処理により)
カロリーは気にしたら負けカリー 0.0060732364654541016 0.00607323973006102
キーマカリー 0.24794459342956543 0.2479445036729513
謎肉入り焼き鳥カレー(3次元) NULL NULL

■ DOT

curry_name HeatWave Community
追いバター追いチーズ追い背徳カリー 154.5 154.5
バターチキンカリー 122.5 122.5
ダルバート 68 68
謎肉入り焼き鳥カレー(3次元) NULL NULL

小数点以下の細かい誤差はあるものの、ほぼ完全に一致していることが分かります。

この誤差は、VECTOR型自体が内部的に単精度浮動小数点数(float、4バイト)で保持しているのに対し、自作関数側はDOUBLE(倍精度)で計算しているために生じるものと考えられます。

バターチキンカリー同士(自分自身との距離)のコサイン距離が、HeatWaveでは0ではなくごく小さな値(0.00000005960...)になっているのはその表れで、自作関数側は丸め処理によってきっちり0になっています。

そして、3次元しかない「謎肉入り焼き鳥カレー」がHeatWave・Community双方で**NULL**として同じように扱われている点も重要です。
今回の検証結果から、DISTANCE()/VECTOR_DISTANCE()側で**NULL**を弾いてくれているため同じ挙動になっていることが確認できました。

ちょっとした注意点
ORDER BY distance(EUCLIDEAN・COSINEは昇順、DOTは降順)で結果を見ると、NULLの行の位置が指標によって先頭だったり末尾だったりしています。
これはMySQLがNULLを「最小値」として扱うためで、昇順ソートではNULLが先頭に、降順ソートでは末尾に来る、というだけの挙動です。次元不一致のデータが紛れていると、LIMITで上位N件だけ取得するようなクエリでNULL行を思わぬ形で拾ってしまう可能性があるので、実務ではWHERE distance IS NOT NULLを添えておくと安心です。

まとめ(前編・後編を通して)

今回、VECTOR_ELEMENT()とVECTOR_DISTANCE()という2つの自作関数を通して、HeatWave専用機能であるDISTANCE()の挙動を、Community版でもある程度再現できることを確認できました。

「HeatWaveでしかベクトル検索はできない」わけではなく、「MySQL CommunityでもSQLとストアド関数の組み合わせ次第で、ベクトル検索できる」というのが2回にわたる検証の結論です。
ただし大規模データに対する検索性能については、詳細な検証が必要となりますのでご注意ください。

VECTOR型は、AIが生み出す「意味」を数値として受け止めるための器となっています。しかしその器の中身をどう解釈し、どう比較して活用するかという部分も重要な要素に思います。
…今回のカレー屋さんの例が、そのことを少しでも “美味く” 伝えられていたら幸いです。

参考

スマートスタイルTECHブログについて

スマートスタイルTECHブログでは、日頃MySQLのサポート業務に従事している有資格者で構成された技術サポートチームがMySQLに関する技術情報を発信しています。データベースのお困りごとはお気軽にご相談下さい。

よかったらシェアしてね!
  • URLをコピーしました!
目次