はじめに
MySQL9.0で満を持して登場したVECTOR型ですが、バージョンが進むごとに周辺機能が少しずつ整理され、
MySQL9.7時点でもVECTOR関連の関数がいくつか用意されています。
「機械学習の埋め込み(embedding)を保存するための型」と聞くと難しそうに聞こえますが、
中身を覗いてみると「数値の配列をバイナリで持っているだけ」というシンプルな構造です。
この記事では、以下の内容を実際にSQLを実行しながら紹介していきます。
・VECTOR型とは何か
・STRING_TO_VECTOR()(TO_VECTOR())
・VECTOR_TO_STRING()(FROM_VECTOR())
今回は「架空のカレー屋さんのフレーバープロファイル」を題材にして、
実際にVECTOR型を触りながら解説していきます。
AIはカレーをどう分析しているのか?
具体的なSQLに入る前に、そもそも「VECTOR型に何を保存するのか」を少しだけイメージしておきましょう。
例えば人間なら、
「バターチキンカリーが好きなら、グリーンカレーも好きそう。」
というように、料理名やジャンル、辛さのイメージなどから直感的に「似ている料理」を連想できます。
しかしAIは、料理名を”文字列”としてそのまま理解しているわけではありません。
AI(正確にはEmbeddingモデル)は、入力データから特徴を抽出し、数百〜数千次元の数値ベクトルとして表現します。
バターチキンカリー ⇒ [0.82, 0.31, 0.11, 0.75, ...]
この数値列のことを Embedding(埋め込みベクトル)と呼びます。
一つひとつの数値が具体的に何を表しているかは人間には解釈しづらいのですが、料理のレシピ・食材・レビューコメントといった情報から、AIが学習の過程で”味わい”や”雰囲気”を数値に圧縮したものだとイメージしてもらえればOKです。
つまりEmbeddingとは、
「意味(この場合は”味わい”)」を数値で表現したもの
です。
似たような風味・雰囲気を持つ料理同士は、このベクトル空間上で近い場所(=似たような数値の並び)に配置されます。だからこそAIは、「バターチキンカリー」と「グリーンカレー」のEmbedding同士の距離が近いことを手がかりに、「この2つは似ている」と判断できるわけです。
そして、このEmbeddingをアプリケーション側で毎回計算し直すのではなく、そのままデータベースに保存しておけるようになったのが、今回紹介するMySQLのVECTOR型です。テキストや画像から一度Embeddingを生成してVECTOR型のカラムに保存しておけば、あとはそのベクトル同士を比較することで、「意味(味わい)が近いデータ」を検索できるようになります。
今回のサンプルでは、AIが自動生成する数百〜数千次元のEmbeddingの代わりに、「辛さ・コク・酸味・甘み」という人間が自分で定義した4次元のベクトルを使って、実際の使い方を見ていきます。次の章で、この2つの違いについてもう少し掘り下げます。
データ準備
VECTOR型の検証用に、カレー屋さんの「辛さ」「コク」「酸味」「甘み」の4つの指標(各0.0〜10.0のスコア)を1本のベクトルとして持つテーブルを作成します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#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) #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]')) ; Query OK, 5 rows affected (0.021 sec) Records: 5 Duplicates: 0 Warnings: 0 |
flavor列が[辛さ, コク, 酸味, 甘み]の順で4つの数値を持つベクトルです。
まずはSELECT *で中身を見てみましょう
|
1 2 3 4 5 6 7 8 9 10 11 12 |
SELECT * FROM curry_shop; +----+-----------------------+-----------------------------------+------------------------------------+ | id | shop_name | curry_name | flavor | +----+-----------------------+-----------------------------------+------------------------------------+ | 1 | こくうまランド | バターチキンカリー | 0x0000604000000041000000400000D040 | | 2 | 黄金スパイス | 大盛りロースカツカレー | 0x0000C0400000F0400000803F00004040 | | 3 | 南インド食堂 | キーマカリー | 0x000008410000A0400000904000000040 | | 4 | ネパール食堂 | ダルバート | 0x0000B04000008040000060400000C03F | | 5 | タイ料理店 | グリーンカレー | 0x0000E0400000C0400000B04000008040 | +----+-----------------------+-----------------------------------+------------------------------------+ 5 rows in set (0.001 sec) |
このように、VECTOR型はテーブルの中ではバイナリ文字列として保持されています。
これが人間には全く読めないので、あとで紹介するVECTOR_TO_STRING()(FROM_VECTOR())で人間が読める形式に戻す必要があります。
補足として、「カレー」と「カリー」は基本的に同じ料理を指しますが、
「カレー」は日本で一般的な呼び方、「カリー」は英語の発音に近く、本格的なインド風・スパイス料理をイメージさせる表現です。
実際には明確な定義の違いはなく、
お店のコンセプトやイメージによって使い分けられることが多いですが…
専門店に入店した際にお店の人に聞く機会があれば、『なぜこのお店では「カリー」(もしくは「カレー」)とメニューに書いているのですか?』と質問をすると興味深い答えが聞けるかもしれません!!
VECTOR型とは何か
MySQL9.7のVECTOR型は、ざっくり言うと「4バイトの単精度浮動小数点数(float)を、指定した個数だけ並べて持てる型」です。定義方法は以下の通りです。
VECTOR(N)
・1エントリあたり4バイト(単精度浮動小数点数)
・指定できる最大エントリ数(N)は16383
・Nを省略した場合のデフォルトは2048
・VECTORと括弧なしで定義OKですが、VECTOR()と空括弧付きで定義するとシンタックスエラー
今回のサンプルでは4次元(辛さ・コク・酸味・甘み)の小さなベクトルを扱っていますが、実際のAI/機械学習のユースケースでは、テキストや画像から生成した埋め込み(embedding)を数百〜数千次元のベクトルとして保存するケースが多くなります。
VECTOR型ならではの制約
VECTOR型は普通のカラムと違って、扱いにやや癖があります。代表的な制約は以下の通りです。
・他の型とは比較できない。VECTOR同士の等価比較(=)のみ可能で、大小比較(<, >など)は不可
・主キー・外部キー・ユニークキー・パーティショニングキーとして使用不可
・ヒストグラムの対象にもできない
・使える文字列関数はBIT_LENGTH()、CHAR_LENGTH()、HEX()、LENGTH()、TO_BASE64()のみで、それ以外の文字列関数(CONCAT()など)は使用不可
・数値関数/日時関数/全文検索関数/XML関数/ビット演算子/JSON関数の引数としても使用不可
・集約関数やウィンドウ関数の引数としてはCOUNT [DISTINCT]のみ使用可能
・CAST(expression AS BINARY)の対象にはできるが、CAST(expression AS VECTOR)のようにVECTOR型へキャストすることは不可(後述のSTRING_TO_VECTOR()を使う)
…というように「何でも入れられる万能な型」ではなく、
「ベクトル値を保持・変換・距離計算するための専用の型」と割り切って考えるのが良さそうです。
VECTOR型の各次元に「意味」を持たせることはできる?
ここで気になるのが、「VECTOR(4)の1番目の要素は辛さ、2番目はコク……というように、
各次元に意味を持たせられるのか?」という点です。
結論から言うと、MySQL自体にはそのための仕組みはありません。
VECTOR型はあくまで「float値がN個並んでいるだけ」のデータ構造であり、
「何番目の要素が何を表すか」というメタデータをカラム側で保持することはできません。
…とはいえ、これは2つのケースに分けて考えると理解しやすくなります。
① 自分で設計した特徴を表すベクトルの場合
今回のカレーの例のように、「1番目=辛さ、2番目=コク、3番目=酸味、4番目=甘み」と、開発者自身が意味を決めて設計するベクトルであれば、各次元に明確な意味を持たせること自体は何の問題もありません。
ただし、コメントやドキュメントに書いておくだけでは「SELECTした結果を見ても、どの数値が何を表しているか一目で分からない」という問題が残ります。そこで、**「次元の意味」を管理する専用テーブルを別途用意し、VECTOR型から特定の次元だけを取り出す自作関数と組み合わせてJOINする、というアプローチを取ってみます。
まずは、VECTOR型からidx番目の要素を1つだけ取り出す関数VECTOR_ELEMENT()を作成します。内部ではVECTOR_TO_STRING()で一度文字列に戻し、SUBSTRING_INDEX()を2回使うことで、カンマ区切りの中からidx番目の値だけを取り出しています。
|
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 |
DELIMITER $$ DROP FUNCTION IF EXISTS VECTOR_ELEMENT$$ Query OK, 0 rows affected (0.013 sec) CREATE FUNCTION VECTOR_ELEMENT( vec VECTOR(4), idx INT ) RETURNS DOUBLE DETERMINISTIC BEGIN DECLARE vec_str LONGTEXT; DECLARE value_str TEXT; -- VECTOR → 文字列 SET vec_str = VECTOR_TO_STRING(vec); -- [] を削除 SET vec_str = REPLACE(vec_str, '[', ''); SET vec_str = REPLACE(vec_str, ']', ''); -- idx番目の要素を取得(1始まり) SET value_str = SUBSTRING_INDEX( SUBSTRING_INDEX(vec_str, ',', idx), ',', -1 ); RETURN CAST(TRIM(value_str) AS DOUBLE); END$$ Query OK, 0 rows affected (0.013 sec) DELIMITER ; |
次に、「何番目の要素が何を表すか」を管理するflavor_dimensionテーブルを作成します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
CREATE TABLE flavor_dimension ( idx INT PRIMARY KEY, dimension_name VARCHAR(20) ); Query OK, 0 rows affected (0.022 sec) INSERT INTO flavor_dimension(idx, dimension_name) VALUES (1, '辛さ'), (2, 'コク'), (3, '酸味'), (4, '甘み'); Query OK, 4 rows affected (0.008 sec) |
これで準備は完了です。
curry_shopとflavor_dimensionをCROSS JOINし、VECTOR_ELEMENT()で該当する次元の値を取り出せば、VECTOR型の中身を「意味付きの行」としてSELECTだけで確認できるようになります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
SELECT c.shop_name , c.curry_name , d.dimension_name , VECTOR_ELEMENT(c.flavor, d.idx) AS score FROM curry_shop c CROSS JOIN flavor_dimension d WHERE c.shop_name = 'こくうまランド' ORDER BY d.idx ; +-----------------------+-----------------------------+----------------+-------+ | shop_name | curry_name | dimension_name | score | +-----------------------+-----------------------------+----------------+-------+ | こくうまランド | バターチキンカリー | 辛さ | 3.5 | | こくうまランド | バターチキンカリー | コク | 8 | | こくうまランド | バターチキンカリー | 酸味 | 2 | | こくうまランド | バターチキンカリー | 甘み | 6.5 | +-----------------------+-----------------------------+----------------+-------+ 4 rows in set (0.001 sec) |
またSQLを工夫すれば以下のように1行で表示することも可能です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
SELECT c.shop_name, c.curry_name, MAX(CASE WHEN d.idx = 1 THEN VECTOR_ELEMENT(c.flavor, d.idx) END) AS 辛さ, MAX(CASE WHEN d.idx = 2 THEN VECTOR_ELEMENT(c.flavor, d.idx) END) AS コク, MAX(CASE WHEN d.idx = 3 THEN VECTOR_ELEMENT(c.flavor, d.idx) END) AS 酸味, MAX(CASE WHEN d.idx = 4 THEN VECTOR_ELEMENT(c.flavor, d.idx) END) AS 甘み FROM curry_shop c CROSS JOIN flavor_dimension d WHERE c.shop_name = 'こくうまランド' GROUP BY c.shop_name, c.curry_name; +-----------------------+-----------------------------+--------+--------+--------+--------+ | shop_name | curry_name | 辛さ | コク | 酸味 | 甘み | +-----------------------+-----------------------------+--------+--------+--------+--------+ | こくうまランド | バターチキンカリー | 3.5 | 8 | 2 | 6.5 | +-----------------------+-----------------------------+--------+--------+--------+--------+ 1 row in set (0.004 sec) |
WHEREを外せば、全店舗分をまとめて「縦持ち」の形式で取得することもできますし、JSON形式でも表示することが可能です。
|
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 |
SELECT c.shop_name, c.curry_name, MAX(CASE WHEN d.idx = 1 THEN VECTOR_ELEMENT(c.flavor, d.idx) END) AS 辛さ, MAX(CASE WHEN d.idx = 2 THEN VECTOR_ELEMENT(c.flavor, d.idx) END) AS コク, MAX(CASE WHEN d.idx = 3 THEN VECTOR_ELEMENT(c.flavor, d.idx) END) AS 酸味, MAX(CASE WHEN d.idx = 4 THEN VECTOR_ELEMENT(c.flavor, d.idx) END) AS 甘み FROM curry_shop c CROSS JOIN flavor_dimension d GROUP BY c.shop_name, c.curry_name; +-----------------------+-----------------------------------+--------+--------+--------+--------+ | shop_name | curry_name | 辛さ | コク | 酸味 | 甘み | +-----------------------+-----------------------------------+--------+--------+--------+--------+ | こくうまランド | バターチキンカリー | 3.5 | 8 | 2 | 6.5 | | 黄金スパイス | 大盛りロースカツカレー | 6 | 7.5 | 1 | 3 | | 南インド食堂 | キーマカリー | 8.5 | 5 | 4.5 | 2 | | ネパール食堂 | ダルバート | 5.5 | 4 | 3.5 | 1.5 | | タイ料理店 | グリーンカレー | 7 | 6 | 5.5 | 4 | +-----------------------+-----------------------------------+--------+--------+--------+--------+ 5 rows in set (0.002 sec) #JSON SELECT c.shop_name, c.curry_name, JSON_OBJECTAGG( d.dimension_name, VECTOR_ELEMENT(c.flavor, d.idx) ) AS flavor FROM curry_shop c JOIN flavor_dimension d GROUP BY c.shop_name, c.curry_name; +-----------------------+-----------------------------------+--------------------------------------------------------------+ | shop_name | curry_name | flavor | +-----------------------+-----------------------------------+--------------------------------------------------------------+ | こくうまランド | バターチキンカリー | {"コク": 8.0, "甘み": 6.5, "辛さ": 3.5, "酸味": 2.0} | | タイ料理店 | グリーンカレー | {"コク": 6.0, "甘み": 4.0, "辛さ": 7.0, "酸味": 5.5} | | ネパール食堂 | ダルバート | {"コク": 4.0, "甘み": 1.5, "辛さ": 5.5, "酸味": 3.5} | | 南インド食堂 | キーマカリー | {"コク": 5.0, "甘み": 2.0, "辛さ": 8.5, "酸味": 4.5} | | 黄金スパイス | 大盛りロースカツカレー | {"コク": 7.5, "甘み": 3.0, "辛さ": 6.0, "酸味": 1.0} | +-----------------------+-----------------------------------+--------------------------------------------------------------+ 5 rows in set (0.003 sec) |
他の方法としては、動的SQLを利用することもできます。
|
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 |
SELECT GROUP_CONCAT( CONCAT( 'MAX(CASE WHEN d.idx = ', idx, ' THEN VECTOR_ELEMENT(c.flavor, ', idx, ') END) AS `', dimension_name, '`' ) ORDER BY idx ) INTO @cols FROM flavor_dimension; Query OK, 1 row affected (0.001 sec) SET @sql = CONCAT( 'SELECT c.shop_name, c.curry_name, ', @cols, ' FROM curry_shop c CROSS JOIN flavor_dimension d GROUP BY c.shop_name, c.curry_name' ); Query OK, 0 rows affected (0.000 sec) PREPARE stmt FROM @sql; Query OK, 0 rows affected (0.001 sec) Statement prepared EXECUTE stmt; +-----------------------+-----------------------------------+--------+--------+--------+--------+ | shop_name | curry_name | 辛さ | コク | 酸味 | 甘み | +-----------------------+-----------------------------------+--------+--------+--------+--------+ | こくうまランド | バターチキンカリー | 3.5 | 8 | 2 | 6.5 | | 黄金スパイス | 大盛りロースカツカレー | 6 | 7.5 | 1 | 3 | | 南インド食堂 | キーマカリー | 8.5 | 5 | 4.5 | 2 | | ネパール食堂 | ダルバート | 5.5 | 4 | 3.5 | 1.5 | | タイ料理店 | グリーンカレー | 7 | 6 | 5.5 | 4 | +-----------------------+-----------------------------------+--------+--------+--------+--------+ 5 rows in set (0.001 sec) DEALLOCATE PREPARE stmt; Query OK, 0 rows affected (0.000 sec) |
このような方法であれば、VECTOR_TO_STRING()の結果を目でカンマ区切りの何番目か数えたり、次元の意味をコメントで探しに行ったりする必要がなくなり、「意味の分かるデータ」としてそのまま集計やグラフ化に回せるようになります。次元数が増えてもflavor_dimensionテーブルに行を追加するだけで対応できるので、拡張性の面でも扱いやすい設計です。
なお、このVECTOR_ELEMENT()は内部で文字列変換とSUBSTRING_INDEX()を都度実行しているため、次元数が大きいベクトル(数百〜数千次元のEmbeddingなど)や大量の行に対して多用すると、パフォーマンス面ではやや不利になります。
今回のような数次元程度の「人間が意味を定義した特徴量ベクトル」を確認・分析する用途にはちょうど良い方法だと言えそうです。
② AIモデルが生成したEmbeddingの場合
一方、前章で紹介したような、AIモデルが自動生成するEmbeddingの場合は事情が異なります。各次元は学習の過程でモデルが自動的に獲得した”潜在的な特徴”であり、「1番目の数値が辛さに対応している」というような、人間が直感的に理解できる意味づけは基本的にできません。
1つ1つの次元を個別に解釈するのではなく、あくまで ベクトル全体としての距離(近い/遠い)に意味がある、という点がポイントです。そのため、上記のようなflavor_dimensionテーブルによる意味付けは、基本的には自分で設計した特徴量ベクトル向けのテクニックだと考えてください。
つまり、「各次元に自分の言葉で意味を持たせたい」場合は、今回のカレーの例のようにあらかじめ人間が定義した特徴量ベクトルを使うのが向いていて、「大量のテキストや画像から自動で似ているものを探したい」場合は、AIモデルによるEmbeddingを使うのが向いている、という住み分けになります。どちらの場合も保存先としては同じVECTOR型を使えるので、目的に応じて使い分けるとよさそうです。
STRING_TO_VECTOR()/TO_VECTOR()─ 文字列からVECTORへ変換する
[数値, 数値, ...]という形式の文字列を、VECTOR型のバイナリ表現に変換するのがSTRING_TO_VECTOR()です。TO_VECTOR()はこの関数のエイリアスなので、どちらを使っても結果は同じです。
先ほどのINSERT文でも実際に使っていましたが、単体で実行してみます。
|
1 2 3 4 5 6 7 8 |
SELECT TO_VECTOR('[1.05, -17.8, 32, 123.456]'); +----------------------------------------------------------------------------------+ | TO_VECTOR('[1.05, -17.8, 32, 123.456]') | +----------------------------------------------------------------------------------+ | 0x6666863F66668EC10000004279E9F642 | +----------------------------------------------------------------------------------+ 1 row in set (0.010 sec) |
数値はカンマ区切りで、角括弧[ ]で囲む必要があります。小数(1.05)だけでなく、指数表記(1.05e2のような形式)にも対応しています。
なお、変換できない文字列を渡すとエラーになります。
|
1 2 3 |
SELECT TO_VECTOR('辛さ8, コク5'); ERROR 6138 (HY000): Data cannot be converted to a valid vector: '辛さ8, コク5' |
「文字列をそのままベクトルにできるわけではなく、あくまで数値の配列である」という点は覚えておく必要がありそうです。
VECTOR_TO_STRING() /FROM_VECTOR()─ VECTORから文字列へ戻す
前述の通り、テーブルに保存されたVECTOR型の値をそのままSELECTすると、意味の分からないバイナリが返ってきます。これを人間が読める文字列に戻すのがVECTOR_TO_STRING()(エイリアス:FROM_VECTOR())です。
実際に、カレー屋さんテーブルのflavor列を文字列に戻して見てみましょう。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
SELECT shop_name , curry_name , FROM_VECTOR(flavor) AS flavor_text FROM curry_shop ORDER BY id ; +-----------------------+-----------------------------------+---------------------------------------------------+ | shop_name | curry_name | flavor_text | +-----------------------+-----------------------------------+---------------------------------------------------+ | こくうまランド | バターチキンカリー | [3.50000e+00,8.00000e+00,2.00000e+00,6.50000e+00] | | 黄金スパイス | 大盛りロースカツカレー | [6.00000e+00,7.50000e+00,1.00000e+00,3.00000e+00] | | 南インド食堂 | キーマカリー | [8.50000e+00,5.00000e+00,4.50000e+00,2.00000e+00] | | ネパール食堂 | ダルバート | [5.50000e+00,4.00000e+00,3.50000e+00,1.50000e+00] | | タイ料理店 | グリーンカレー | [7.00000e+00,6.00000e+00,5.50000e+00,4.00000e+00] | +-----------------------+-----------------------------------+---------------------------------------------------+ 5 rows in set (0.014 sec) |
無事に、INSERT時に指定した[辛さ, コク, 酸味, 甘み]の数値が復元できました。出力は指数表記(e+00など)になる点だけ注意が必要です。
VECTOR_TO_STRING()とSTRING_TO_VECTOR()は、ちょうど逆の変換をする関数同士になっているので、以下のようにセットで使うと元の文字列に戻ることも確認できます
|
1 2 3 4 5 6 7 8 |
SELECT VECTOR_TO_STRING(STRING_TO_VECTOR('[1.05, -17.8, 32]')); +---------------------------------------------------------+ | VECTOR_TO_STRING(STRING_TO_VECTOR('[1.05, -17.8, 32]')) | +---------------------------------------------------------+ | [1.05000e+00,-1.78000e+01,3.20000e+01] | +---------------------------------------------------------+ 1 row in set (0.001 sec) |
まとめ
MySQL 9.0で登場した導入されたVECTOR型は、
9.x系で関連機能が拡充されている新しいデータ型です。
そしてこれまでアプリケーションでしか扱えなかった
「意味の数値化(Embedding)」をデータベース内部で直接操作できるようにした新しい概念です。
従来の「文字列」や「数値」に加えて、「意味空間を表現するベクトル」というデータ型が登場したことで、SQLもAI時代に向けて進化を遂げています。
特に、STRING_TO_VECTOR()/VECTOR_TO_STRING()などの変換関数を通じて、
AIで生成したベクトルをMySQL上で格納・比較・検索できる点は、
今後大きな転換点となる可能性がありそうですので、引き続き注視していきましょう!!


