はじめに
前編では、MySQL9.7のVECTOR型がどのようなデータ構造なのか、TO_VECTOR()/FROM_VECTOR()による相互変換、そして「次元に意味を持たせる」ためのVECTOR_ELEMENT()関数とflavor_dimensionテーブルの組み合わせまでを紹介しました。
後編となる今回は、以下2つのテーマについて紹介します。
・VECTOR_DIM() ─ ベクトルの次元数を確認する
・ベクトル検索との関係 ─ DISTANCE()関数と、Community版での代替アプローチ
データ準備
VECTOR型の検証用に、カレー屋さんの「辛さ」「コク」「酸味」「甘み」の4つの指標(各0.0〜10.0のスコア)を1本のベクトルとして持つテーブルを作成します。
なお、「クセスゴYAMITSUKIYA 本店」のメニューに載せてみたい…
栃木県産の「とちおとめ」を使用した『いちごカレー』、京都の名産である宇治抹茶をルーに合わせた『宇治抹茶カレー』、見た目がクリームシチュー、まろやかな甘みがありつつカレーの辛みが広がる北海道の『白い伽哩』などの『ご当地カレー』や『至高の一品』がある場合は、是非検証時にデータを追加してみてください!
もちろん、味の指標だけでなく、「独創性」、「中毒度」、「映え度」、「温冷感」、「ボリューム」などの指標も追加して、より深い検証を行っていくのも良さそうです!
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#Create Table CREATE TABLE curry_shop ( id INT AUTO_INCREMENT PRIMARY KEY, shop_name VARCHAR(50), curry_name VARCHAR(50), flavor VECTOR(4) ); Query OK, 0 rows affected (0.028 sec) CREATE TABLE flavor_dimension ( idx INT PRIMARY KEY, dimension_name VARCHAR(20) ); #Data Insert INSERT INTO curry_shop(shop_name, curry_name, flavor) VALUES ('こくうまランド', 'バターチキンカリー', TO_VECTOR('[3.5, 8.0, 2.0, 6.5]')), ('黄金スパイス', '大盛りロースカツカレー', TO_VECTOR('[6.0, 7.5, 1.0, 3.0]')), ('南インド食堂', 'キーマカリー', TO_VECTOR('[8.5, 5.0, 4.5, 2.0]')), ('ネパール食堂', 'ダルバート', TO_VECTOR('[5.5, 4.0, 3.5, 1.5]')), ('タイ料理店', 'グリーンカレー', TO_VECTOR('[7.0, 6.0, 5.5, 4.0]')), ('クセスゴYAMITSUKIYA 本店', '豚角煮バターカリー', TO_VECTOR('[5.5, 10.0, 1.0, 6.0]')), ('クセスゴYAMITSUKIYA 本店', 'ラム肉とパクチー爆盛りカリー', TO_VECTOR('[8.5, 7.5, 4.5, 2.5]')), ('クセスゴYAMITSUKIYA 本店', '謎肉入り焼き鳥カレー', TO_VECTOR('[6.5, 9.5, 1.5]')), ('クセスゴYAMITSUKIYA 本店', '思ひ出の熟成カリー(二日目)', TO_VECTOR('[4.5, 9.0, 2.0, 5.0]')), ('クセスゴYAMITSUKIYA 本店', '店長の気まぐれスパイス暴走カリー', TO_VECTOR('[10.0, 8.0, 3.5, 1.5]')), ('クセスゴYAMITSUKIYA 本店', 'カロリーは気にしたら負けカリー', TO_VECTOR('[4.0, 10.0, 1.0, 8.0]')), ('クセスゴYAMITSUKIYA 本店', '追いバター追いチーズ追い背徳カリー', TO_VECTOR('[4.0, 10.0, 1.0, 9.0]')), ('クセスゴYAMITSUKIYA 本店', 'スプーンが止まらない罪深きカリーDX', TO_VECTOR('[6.0, 9.5, 2.0, 7.5]')) ; Query OK, 13 rows affected (0.017 sec) Records: 13 Duplicates: 0 Warnings: 0 INSERT INTO flavor_dimension(idx, dimension_name) VALUES (1, '辛さ'), (2, 'コク'), (3, '酸味'), (4, '甘み'); Query OK, 4 rows affected (0.008 sec) |
flavor列が[辛さ, コク, 酸味, 甘み]の順で4つの数値を持つベクトルです。
まずはSELECT *で中身を見てみましょう
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
mysql> SELECT * FROM curry_shop; +----+--------------------------------+-----------------------------------------------------+------------------------------------+ | id | shop_name | curry_name | flavor | +----+--------------------------------+-----------------------------------------------------+------------------------------------+ | 25 | こくうまランド | バターチキンカリー | 0x0000604000000041000000400000D040 | | 26 | 黄金スパイス | 大盛りロースカツカレー | 0x0000C0400000F0400000803F00004040 | | 27 | 南インド食堂 | キーマカリー | 0x000008410000A0400000904000000040 | | 28 | ネパール食堂 | ダルバート | 0x0000B04000008040000060400000C03F | | 29 | タイ料理店 | グリーンカレー | 0x0000E0400000C0400000B04000008040 | | 30 | クセスゴYAMITSUKIYA 本店 | 豚角煮バターカリー | 0x0000B040000020410000803F0000C040 | | 31 | クセスゴYAMITSUKIYA 本店 | ラム肉とパクチー爆盛りカリー | 0x000008410000F0400000904000002040 | | 32 | クセスゴYAMITSUKIYA 本店 | 謎肉入り焼き鳥カレー | 0x0000D040000018410000C03F | | 33 | クセスゴYAMITSUKIYA 本店 | 思ひ出の熟成カリー(二日目) | 0x0000904000001041000000400000A040 | | 34 | クセスゴYAMITSUKIYA 本店 | 店長の気まぐれスパイス暴走カリー | 0x0000204100000041000060400000C03F | | 35 | クセスゴYAMITSUKIYA 本店 | カロリーは気にしたら負けカリー | 0x00008040000020410000803F00000041 | | 36 | クセスゴYAMITSUKIYA 本店 | 追いバター追いチーズ追い背徳カリー | 0x00008040000020410000803F00001041 | | 37 | クセスゴYAMITSUKIYA 本店 | スプーンが止まらない罪深きカリーDX | 0x0000C04000001841000000400000F040 | +----+--------------------------------+-----------------------------------------------------+------------------------------------+ 13 rows in set (0.003 sec) |
このように、VECTOR型は内部的にバイナリ形式で格納され、SELECTするとそのバイナリ表現が表示されます。
VECTOR_DIM() ─ ベクトルの次元数を確認する
VECTOR_DIM()は、VECTOR値が何個のエントリ(次元)を持っているかを返す関数です。
「このベクトル、本当に想定した次元数で入っているんだっけ?」を確認したいときに便利です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
SELECT shop_name , curry_name , VECTOR_DIM(flavor) AS dim FROM curry_shop ORDER BY id ; +--------------------------------+-----------------------------------------------------+------+ | shop_name | curry_name | dim | +--------------------------------+-----------------------------------------------------+------+ | こくうまランド | バターチキンカリー | 4 | | 黄金スパイス | 大盛りロースカツカレー | 4 | | 南インド食堂 | キーマカリー | 4 | | ネパール食堂 | ダルバート | 4 | | タイ料理店 | グリーンカレー | 4 | | クセスゴYAMITSUKIYA 本店 | 豚角煮バターカリー | 4 | | クセスゴYAMITSUKIYA 本店 | ラム肉とパクチー爆盛りカリー | 4 | | クセスゴYAMITSUKIYA 本店 | 謎肉入り焼き鳥カレー | 3 | | クセスゴYAMITSUKIYA 本店 | 思ひ出の熟成カリー(二日目) | 4 | | クセスゴYAMITSUKIYA 本店 | 店長の気まぐれスパイス暴走カリー | 4 | | クセスゴYAMITSUKIYA 本店 | カロリーは気にしたら負けカリー | 4 | | クセスゴYAMITSUKIYA 本店 | 追いバター追いチーズ追い背徳カリー | 4 | | クセスゴYAMITSUKIYA 本店 | スプーンが止まらない罪深きカリーDX | 4 | +--------------------------------+-----------------------------------------------------+------+ 13 rows in set (0.005 sec) |
データ量が少なければ、一覧を表示する形で確認することができます。
ただし何万行ものデータが登録されていた場合は…
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
WITH dim_ranking AS ( SELECT VECTOR_DIM(flavor) AS dim , COUNT(*) AS cnt , RANK() OVER (ORDER BY COUNT(*) DESC) AS rnk FROM curry_shop GROUP BY VECTOR_DIM(flavor) ) SELECT c.shop_name , c.curry_name , VECTOR_DIM(c.flavor) AS dim , VECTOR_TO_STRING(c.flavor) AS flavor_text , c.flavor FROM curry_shop c WHERE VECTOR_DIM(c.flavor) <> (SELECT dim FROM dim_ranking WHERE rnk = 1 LIMIT 1) ; +--------------------------------+--------------------------------+------+---------------------------------------+----------------------------+ | shop_name | curry_name | dim | flavor_text | flavor | +--------------------------------+--------------------------------+------+---------------------------------------+----------------------------+ | クセスゴYAMITSUKIYA 本店 | 謎肉入り焼き鳥カレー | 3 | [6.50000e+00,9.50000e+00,1.50000e+00] | 0x0000D040000018410000C03F | +--------------------------------+--------------------------------+------+---------------------------------------+----------------------------+ 1 row in set (0.019 sec) |
WITH句やWindow関数を使うことで、
このように「謎肉入り焼き鳥カレー」だけ3次元であることがわかります。
ベクトル検索と利用する関数の概要
さて、ここまでは「VECTOR型に値を出し入れする」「意味を持たせて確認する」という話でしたが、本題とも言える「VECTOR型を使って類似度検索(ベクトル検索)ができるのか?」を見ていきます。
DISTANCE()関数について
MySQLには2つのベクトルの距離(=似ている度合い)を計算するDISTANCE()関数(エイリアス:VECTOR_DISTANCE())が用意されています。第3引数にCOSINE(コサイン距離)、DOT(内積)、EUCLIDEAN(ユークリッド距離)のいずれかを指定して計算方法を選びます。
|
1 2 3 4 5 6 |
SELECT DISTANCE( (SELECT flavor FROM curry_shop WHERE curry_name = 'バターチキンカリー'), (SELECT flavor FROM curry_shop WHERE curry_name = 'グリーンカレー'), 'EUCLIDEAN' ) AS flavor_distance; |
もしこの関数が使えれば、「辛さ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を実行すると、以下のようなエラーになります。
|
1 2 3 4 5 6 7 |
SELECT DISTANCE( (SELECT flavor FROM curry_shop WHERE curry_name = 'バターチキンカリー'), (SELECT flavor FROM curry_shop WHERE curry_name = 'グリーンカレー'), 'EUCLIDEAN' ) AS flavor_distance; ERROR 1305 (42000): FUNCTION test.DISTANCE does not exist |
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で丸める
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
DELIMITER $$ DROP FUNCTION IF EXISTS VECTOR_DISTANCE$$ CREATE FUNCTION VECTOR_DISTANCE( vec1 VECTOR, vec2 VECTOR, metric VARCHAR(20) ) RETURNS DOUBLE DETERMINISTIC BEGIN DECLARE i INT DEFAULT 1; DECLARE dim INT; DECLARE v1 DOUBLE; DECLARE v2 DOUBLE; DECLARE sum_sq DOUBLE DEFAULT 0; DECLARE dot_product DOUBLE DEFAULT 0; DECLARE norm1 DOUBLE DEFAULT 0; DECLARE norm2 DOUBLE DEFAULT 0; DECLARE cosine_distance DOUBLE; SET metric = UPPER(metric); -- ベクトルの次元数を取得 SET dim = VECTOR_DIM(vec1); -- HeatWave互換 -- 次元数が異なる場合はNULLを返す IF dim <> VECTOR_DIM(vec2) THEN RETURN NULL; END IF; -- 各次元を走査 WHILE i <= dim DO SET v1 = VECTOR_ELEMENT(vec1, i); SET v2 = VECTOR_ELEMENT(vec2, i); -- ユークリッド距離 SET sum_sq = sum_sq + POW(v1 - v2, 2); -- 内積 SET dot_product = dot_product + (v1 * v2); -- ノルム SET norm1 = norm1 + POW(v1, 2); SET norm2 = norm2 + POW(v2, 2); SET i = i + 1; END WHILE; CASE metric WHEN 'EUCLIDEAN' THEN RETURN SQRT(sum_sq); WHEN 'DOT' THEN RETURN dot_product; WHEN 'COSINE' THEN -- HeatWave互換 -- ゼロベクトルはNULL IF norm1 = 0 OR norm2 = 0 THEN RETURN NULL; END IF; -- Cosine Distance = 1 - Cosine Similarity SET cosine_distance = 1 - ( dot_product / (SQRT(norm1) * SQRT(norm2)) ); -- 浮動小数点誤差対策 -- 理論上0となる値は0へ丸める IF ABS(cosine_distance) < 1e-12 THEN RETURN 0; END IF; RETURN cosine_distance; ELSE SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Unknown metric. Use EUCLIDEAN, COSINE or DOT.'; END CASE; END$$ DELIMITER ; |
実際にHeatWaveとCommunity版で比較する
「バターチキンカリー」を基準に、全メニューとの距離を3つの指標でそれぞれ計算してみます。まずはHeatWave側です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
■HeatWave SELECT curry_name, DISTANCE( flavor, ( SELECT flavor FROM curry_shop WHERE curry_name='バターチキンカリー' ), 'EUCLIDEAN' ) AS distance FROM curry_shop ORDER BY distance; +-----------------------------------------------------+--------------------+ | curry_name | distance | +-----------------------------------------------------+--------------------+ | 謎肉入り焼き鳥カレー | NULL | | バターチキンカリー | 0 | | 思ひ出の熟成カリー(二日目) | 2.0615527629852295 | | カロリーは気にしたら負けカリー | 2.7386128902435303 | | 豚角煮バターカリー | 3.041381359100342 | | スプーンが止まらない罪深きカリーDX | 3.082206964492798 | | 追いバター追いチーズ追い背徳カリー | 3.391165018081665 | | 大盛りロースカツカレー | 4.44409704208374 | | グリーンカレー | 5.894913196563721 | | ダルバート | 6.873863697052002 | | ラム肉とパクチー爆盛りカリー | 6.892024517059326 | | キーマカリー | 7.77817440032959 | | 店長の気まぐれスパイス暴走カリー | 8.336666107177734 | +-----------------------------------------------------+--------------------+ 13 rows in set (0.00 sec) SELECT curry_name, DISTANCE( flavor, ( SELECT flavor FROM curry_shop WHERE curry_name='バターチキンカリー' ), 'DOT' ) AS dot FROM curry_shop ORDER BY dot DESC; +-----------------------------------------------------+--------+ | curry_name | dot | +-----------------------------------------------------+--------+ | 追いバター追いチーズ追い背徳カリー | 154.5 | | スプーンが止まらない罪深きカリーDX | 149.75 | | カロリーは気にしたら負けカリー | 148 | | 豚角煮バターカリー | 140.25 | | 思ひ出の熟成カリー(二日目) | 124.25 | | バターチキンカリー | 122.5 | | 店長の気まぐれスパイス暴走カリー | 115.75 | | ラム肉とパクチー爆盛りカリー | 115 | | グリーンカレー | 109.5 | | 大盛りロースカツカレー | 102.5 | | キーマカリー | 91.75 | | ダルバート | 68 | | 謎肉入り焼き鳥カレー | NULL | +-----------------------------------------------------+--------+ 13 rows in set (0.01 sec) SELECT curry_name, DISTANCE( flavor, ( SELECT flavor FROM curry_shop WHERE curry_name='バターチキンカリー' ), 'COSINE' ) AS cosine FROM curry_shop ORDER BY cosine; +-----------------------------------------------------+---------------------------+ | curry_name | cosine | +-----------------------------------------------------+---------------------------+ | 謎肉入り焼き鳥カレー | NULL | | バターチキンカリー | 0.00000005960464477539063 | | カロリーは気にしたら負けカリー | 0.0060732364654541016 | | 追いバター追いチーズ追い背徳カリー | 0.007963120937347412 | | スプーンが止まらない罪深きカリーDX | 0.009260773658752441 | | 思ひ出の熟成カリー(二日目) | 0.016352534294128418 | | 豚角煮バターカリー | 0.020167648792266846 | | 大盛りロースカツカレー | 0.08415043354034424 | | グリーンカレー | 0.1364322304725647 | | ラム肉とパクチー爆盛りカリー | 0.1654278039932251 | | ダルバート | 0.21174323558807373 | | 店長の気まぐれスパイス暴走カリー | 0.21723055839538574 | | キーマカリー | 0.24794459342956543 | +-----------------------------------------------------+---------------------------+ 13 rows in set (0.00 sec) |
続いてCommunity側、DISTANCE()の代わりに自作したVECTOR_DISTANCE()を使って全く同じ内容を実行してみます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
■Community SELECT curry_name, VECTOR_DISTANCE( flavor, ( SELECT flavor FROM curry_shop WHERE curry_name='バターチキンカリー' ), 'EUCLIDEAN' ) AS distance FROM curry_shop ORDER BY distance; +-----------------------------------------------------+--------------------+ | curry_name | distance | +-----------------------------------------------------+--------------------+ | 謎肉入り焼き鳥カレー | NULL | | バターチキンカリー | 0 | | 思ひ出の熟成カリー(二日目) | 2.0615528128088303 | | カロリーは気にしたら負けカリー | 2.7386127875258306 | | 豚角煮バターカリー | 3.0413812651491097 | | スプーンが止まらない罪深きカリーDX | 3.082207001484488 | | 追いバター追いチーズ追い背徳カリー | 3.391164991562634 | | 大盛りロースカツカレー | 4.444097208657794 | | グリーンカレー | 5.894913061275798 | | ダルバート | 6.87386354243376 | | ラム肉とパクチー爆盛りカリー | 6.892024376045111 | | キーマカリー | 7.7781745930520225 | | 店長の気まぐれスパイス暴走カリー | 8.336666000266533 | +-----------------------------------------------------+--------------------+ 13 rows in set (0.005 sec) SELECT curry_name, VECTOR_DISTANCE( flavor, ( SELECT flavor FROM curry_shop WHERE curry_name='バターチキンカリー' ), 'DOT' ) AS dot FROM curry_shop ORDER BY dot DESC; +-----------------------------------------------------+--------+ | curry_name | dot | +-----------------------------------------------------+--------+ | 追いバター追いチーズ追い背徳カリー | 154.5 | | スプーンが止まらない罪深きカリーDX | 149.75 | | カロリーは気にしたら負けカリー | 148 | | 豚角煮バターカリー | 140.25 | | 思ひ出の熟成カリー(二日目) | 124.25 | | バターチキンカリー | 122.5 | | 店長の気まぐれスパイス暴走カリー | 115.75 | | ラム肉とパクチー爆盛りカリー | 115 | | グリーンカレー | 109.5 | | 大盛りロースカツカレー | 102.5 | | キーマカリー | 91.75 | | ダルバート | 68 | | 謎肉入り焼き鳥カレー | NULL | +-----------------------------------------------------+--------+ 13 rows in set (0.004 sec) SELECT curry_name, VECTOR_DISTANCE( flavor, ( SELECT flavor FROM curry_shop WHERE curry_name='バターチキンカリー' ), 'COSINE' ) AS cosine FROM curry_shop ORDER BY cosine; +-----------------------------------------------------+----------------------+ | curry_name | cosine | +-----------------------------------------------------+----------------------+ | 謎肉入り焼き鳥カレー | NULL | | バターチキンカリー | 0 | | カロリーは気にしたら負けカリー | 0.00607323973006102 | | 追いバター追いチーズ追い背徳カリー | 0.007963059518372173 | | スプーンが止まらない罪深きカリーDX | 0.009260750200549839 | | 思ひ出の熟成カリー(二日目) | 0.01635251231023427 | | 豚角煮バターカリー | 0.020167676174532967 | | 大盛りロースカツカレー | 0.0841503944560269 | | グリーンカレー | 0.13643224288013944 | | ラム肉とパクチー爆盛りカリー | 0.16542773685589107 | | ダルバート | 0.21174319766452288 | | 店長の気まぐれスパイス暴走カリー | 0.2172305905809897 | | キーマカリー | 0.2479445036729513 | +-----------------------------------------------------+----------------------+ 13 rows in set (0.004 sec) |
いくつか結果を見比べてみます。
■ 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が生み出す「意味」を数値として受け止めるための器となっています。しかしその器の中身をどう解釈し、どう比較して活用するかという部分も重要な要素に思います。
…今回のカレー屋さんの例が、そのことを少しでも “美味く” 伝えられていたら幸いです。


