CreateField Blog

オープンソースを使って個人でWebサービスを開発・運営していたブログ

MroongaでGroongaの機能を使いこなす高度なテーブル設計をする方法

はじめに

MySQL/MariaDBで高速に全文検索するためのオープンソースのストレージエンジンMroongaは、以下のように、Engine=MroongaFULLTEXT INDEX (${source_column})と書くだけで非常に簡単に全文検索を使い始めることができます。

CREATE TABLE memos (
id INT NOT NULL PRIMARY KEY,
content TEXT NOT NULL,
FULLTEXT INDEX (content)
) Engine=Mroonga DEFAULT CHARSET=utf8;

検索するときも以下のようにMATCH ... AGAINSTを使うだけです。

mysql> INSERT INTO memos VALUES (1, "1日の消費㌍は約2000㌔㌍");
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM memos WHERE MATCH (content) AGAINST ("+消費" IN BOOLEAN MODE);
+----+----------------------------------+
| id | content                          |
+----+----------------------------------+
|  1 | 1日の消費㌍は約2000㌔㌍ |
+----+----------------------------------+
1 row in set (0.00 sec)

なお、IN BOOLEAN MODEをつけないと、自然文検索IN NATURAL LANGUAGE MODEとなり、検索クエリを所定のルールで分割したトークンの並び順を考慮しなかったりして、一見、不自然と思われる結果が得られるため注意が必要です。LIKE '%word%のような挙動(まったく同じではありませんが)を望むのであれば、IN BOOLEAN MODEを指定してください。

このように、非常に簡単に使えるのは、MroongaがGroonga全文検索用の語彙表テーブルインデックスカラムをいい感じに自動的に作成してくれるからです。

上記のように簡単な使い方だけでいいのであればそれでいいですが、よりGroongaの機能を使いこなしたい場合は、Groonga側の構成を理解して、それをMroongaから設定する方法を理解する必要があります。

GroongaとMroongaはほぼ毎月29日にリリースされており、10/29にはGroonga4.0.7Mroonga4.07がリリースされました。

Mroonga4.07からは、Groongaのテーブルオプション、カラムオプションの大半はMroongaでもできるようになったと思います。

この記事では、MroongaでGroongaの機能を使いこなすテーブル設計をする方法について紹介します。

FULLTEXT INDEXのCOMMENTによるオプション

FULLTEXT INDEXをつけてテーブルを作ることにより、MySQLからは見えない形で、${テーブル名}-${インデックス名}というGroonga全文検索用の語彙表テーブルが自動的に作成されます。

これは、Groongatable_listコマンドを使うことにより、以下のように確認することができます。

mysql> SELECT mroonga_command('table_list');
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| mroonga_command('table_list')                                                                                                                                                                                                                                                                                                                                                                                                |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| [[["id","UInt32"],["name","ShortText"],["path","ShortText"],["flags","ShortText"],["domain","ShortText"],["range","ShortText"],["default_tokenizer","ShortText"],["normalizer","ShortText"]],
[259,"memos","mrn_test600.mrn.0000103","TABLE_PAT_KEY|PERSISTENT","Int32",null,null,null],
[262,"memos-content","mrn_test600.mrn.0000106","TABLE_PAT_KEY|PERSISTENT","ShortText",null,"TokenBigram","NormalizerMySQLGeneralCI"]] |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

TokenBigramトークナイザー、NormalizerMySQLGeneralCIノーマライザーが設定されたキーの種類TABLE_PAT_KEYShortText型の memos-contentというテーブルが作成されています。これがGroongaでいう全文検索用の語彙表テーブルです。

あまり一般的ではないかもしれませんが、Groongaではインデックスのキー管理構造も普通のデータ用のテーブルと同様にして作ります。

全文検索用のパーサー(トークナイザー)の指定方法

FULLTEXT INDEXCOMMENTparserを指定することで、全文検索用のパーサー(トークナイザー)を変更することできます。(ドキュメント)

以下は、TokenBigramSplitSymbolAlphaトークナイザーを使用する例です。

CREATE TABLE memos (
id INT NOT NULL PRIMARY KEY,
content TEXT NOT NULL,
FULLTEXT INDEX (content) COMMENT 'parser "TokenBigramSplitSymbolAlpha"'
) Engine=Mroonga DEFAULT CHARSET=utf8;

トークナイズされ方は、以下のようにGroongatokenizeコマンドで確認することができます。

mysql>  SELECT mroonga_command('tokenize TokenBigramSplitSymbolAlpha "This is a pen" NormalizerMySQLGeneralCI');
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| mroonga_command('tokenize TokenBigramSplitSymbolAlpha "This is a pen" NormalizerMySQLGeneralCI')                                                                                                                                                                                      |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| [{"value":"TH","position":0},{"value":"HI","position":1},
{"value":"IS","position":2},{"value":"S","position":3},
{"value":"IS","position":4},{"value":"S","position":5},
{"value":"A","position":6},{"value":"PE","position":7},
{"value":"EN","position":8},{"value":"N","position":9}] |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

TokenBigramSplitSymbolAlphaは、アルファベットと記号をバイグラムで分割させるトークナイザーです。このトークナイザーは、英単語をより細かい粒度で検索にヒットさせたい場合に有用です。ただし、アルファベットの異なり字数は26種しかないため、英語のみの文章において、転置索引方式で2文字ごとのトークナイズは、検索性能が劣化しやすいので注意してください。とはいっても、Groongaはかなり高速なので数GiB程度なら問題ないと思います。ただ、10GiB以上になってくると、かなり検索速度が劣化してくると思います。

ちなみにInnoDB FTSinnodb_ft_min_token_sizeのデフォルト値は3でそれ以下の文字数の単語は無視されます。やはり、転置索引方式では、アルファベットのトークンサイズは3文字以上はないと、データベースが有る程度大きくなってくると苦しくなるのでしょう。

ノーマライザーの指定方法

FULLTEXT INDEXCOMMENTnormalizerを指定することで、ノーマライザーを変更することできます。(ドキュメント)

以下は、NormalizerAutoノーマライザーを使用する例です。

CREATE TABLE memos (
id INT NOT NULL PRIMARY KEY,
content TEXT NOT NULL,
FULLTEXT INDEX (content) COMMENT 'normalizer "NormalizerAuto"'
) Engine=Mroonga DEFAULT CHARSET=utf8;

ノーマライズされ方は、以下のようにGroonganormalizeコマンドで確認することができます。

mysql> SELECT mroonga_command('normalize NormalizerAuto "T1日の消費㌍は約2000㌔ ㌍"');
+------------------------------------------------------------------------------------------------+
| mroonga_command('normalize NormalizerAuto "T1日の消費㌍は約2000㌔㌍"')                |
+------------------------------------------------------------------------------------------------+
| {"normalized":"t1日の消費カロリーは約2000キロカロリー","types":[],"checks":[]} |
+------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

NormalizerAutoは、NFKCに独自のノーマライズを加えたもので合成文字が分解されたり、"がカタカナと合成されたりします。

トークンフィルターの指定方法

Groonga 4.0.7からはTokenFilterStopWordTokenFilterStemのトークンフィルターが追加されました。

FULLTEXT INDEXCOMMENTtoken_filtersを指定することで、トークンフィルターを設定することできます。(ドキュメント)

以下は、TokenFilterStemトークンフィルターを使用する例です。

mysql> SELECT mroonga_command('register token_filters/stem');
CREATE TABLE memos (
id INT NOT NULL PRIMARY KEY,
content TEXT NOT NULL,
FULLTEXT INDEX (content) COMMENT 'token_filters "TokenFilterStem"'
) Engine=Mroonga DEFAULT CHARSET=utf8;

初回だけSELECT mroonga_command('register token_filters/stem');を実行してGroongaのデータベースに対してプラグインを登録する必要があります。

トークナイズされ方は、以下のようにGroongatokenizeコマンドで確認することができます。

mysql> SELECT mroonga_command('tokenize TokenBigram "There are cars" NormalizerAuto --token_filters TokenFilterStem');
+---------------------------------------------------------------------------------------------------------+
| mroonga_command('tokenize TokenBigram "There are cars" NormalizerAuto --token_filters TokenFilterStem') |
+---------------------------------------------------------------------------------------------------------+
| [{"value":"there","position":0},{"value":"are","position":1},{"value":"car","position":2}]              |
+---------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

carsがステミングされてcarとトークナイズされています。ステミングというのは、語句の語幹を抽出する処理のことで、複数形や過去形などの活用形の語尾が所定のルールで切除されます。 これにより、TokenBigram(デフォルト)で検索クエリにcarと入力してもcarsがヒットするようになります。なお、TokenFilterStemは、libstemmerがインストールされている必要があるため、注意が必要です。また、MySQL互換のノーマライザーではアルファベットが大文字にノーマライズされるため、ステミング対象にならないので注意してください。

追記
Groonga4.0.9以降ではMySQL互換のノーマライザーでもTokenFilterStemを利用することができるようになりました。 Groonga - Groonga 4.0.9リリース

カラムのCOMMENTによるオプション

圧縮カラムの指定方法

Groongaの圧縮カラム自体は以前からあったのですが、Groonga 4.0.7からは圧縮カラムで発生していたメモリリークが解消され、非常に高速な圧縮伸長が可能なLZ4のライブラリを利用できるようになっています。

以下は、COMPRESS_LZ4を使用する例です。

CREATE TABLE entries (
  id INT UNSIGNED PRIMARY KEY,
  content TEXT COMMENT 'flags "COLUMN_SCALAR|COMPRESS_LZ4"'
) Engine=Mroonga DEFAULT CHARSET=utf8;

この他、COMPRESS_ZLIBを使用することができます。zlibの圧縮率は高いですが、圧縮、伸長にかかる時間が長いです。

以下のプルリクエストにWikipediaを使った実験結果をのせています。LZ4の場合は、普通に圧縮カラムを出力させるだけであれば、ほとんどqpsに影響がないことがわかります。

https://github.com/groonga/groonga/pull/221

https://github.com/groonga/groonga/pull/223

テーブル参照の指定方法

Groongaには、テーブル参照という機能があります。Groongaselectコマンドでは、テーブル参照のカラムを作っておけば、そのカラムの値をキーとする別テーブルのカラムを参照したり、検索したりできます。

このテーブル参照も一応Mroongaでも作れるようになっています。

CREATE TABLE refs (
   tag VARCHAR(255) PRIMARY KEY,
   description TEXT
) Engine=Mroonga DEFAULT CHARSET=utf8;
CREATE TABLE entries (
  id INT UNSIGNED PRIMARY KEY,
  tag VARCHAR(255) COMMENT 'type "refs"'
) Engine=Mroonga DEFAULT CHARSET=utf8;

ただ、MroongaでSQLを使うだけなら使う必要はないと思います。SQLでは、Groongaのselectコマンドのように、参照先テーブルrefsの他のカラムを参照したりできません。参照できるのはカラムに追加した値だけです。

mysql> INSERT INTO refs VALUES ("Mroonga", "I found Mroonga that is a MySQL storage engine to use Groonga!");
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO entries VALUES (1, "Mroonga");
Query OK, 1 row affected (0.01 sec)

mysql> SELECT id,tag,tag.description FROM entries;
ERROR 1054 (42S22): Unknown column 'tag.description' in 'field list'

mysql> SELECT id,tag FROM entries;
+----+---------+
| id | tag     |
+----+---------+
|  1 | MROONGA |
+----+---------+
1 row in set (0.00 sec)
mysql> SELECT mroonga_command('select entries --output_columns id,tag,tag.description');
+--------------------------------------------------------------------------------------------------------------------------------------------------------+
| mroonga_command('select entries --output_columns id,tag,tag.description')                                                                              |
+--------------------------------------------------------------------------------------------------------------------------------------------------------+
| [[[1],[["id","UInt32"],["tag","refs"],["tag.description","LongText"]],
[1,"MROONGA","I found Mroonga that is a MySQL storage engine to use Groonga!"]]] |
+--------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

テーブルのCOMMENTによるオプション

ここからは、Groongaと同じように全文検索用の語彙表テーブルを明示的に作る方法です。Groongaのtable_createコマンドを使ってテーブル定義を作る方法と同じような感じになります*1。MySQLのテーブル定義を作る方法のみが頭にあると混乱する可能性があります。興味のある方のみご参照ください。

これによるメリットは語彙表を可視化できて、SQLで語彙表のキーやストップワードのフラグを管理できるということです。

ただし、この方法は、テーブル間の依存関係ができ、mysqldumpで出力された順番によってはそのままではリストアができなくなる可能性があるので注意してください。名前の付け方やダンプファイルのテーブル定義の出力を調整したりする必要があります。

トークナイザーの指定方法

FULLTEXT INDEXのCOMMENTにtable "${テーブル名}"を指定することにより、${テーブル名}のテーブルが全文検索用の語彙表テーブルとして用いられます。${テーブル名}のテーブルCOMMENTのdefault_tokenizerにトークナイザーを指定することができます。

CREATE TABLE terms (
term VARCHAR(255) NOT NULL PRIMARY KEY
) Engine=Mroonga COMMENT='default_tokenizer "TokenBigram"' DEFAULT CHARSET=utf8;

CREATE TABLE memos (
id INT NOT NULL PRIMARY KEY,
memo TEXT NOT NULL,
FULLTEXT INDEX (memo) COMMENT 'table "terms"'
) Engine=Mroonga DEFAULT CHARSET=utf8;

これにより、以下のように全文検索用の語彙テーブルをSQLで参照することができるようになりますん。なりませんでした。4.08ではSQLのSELECTで参照できるようになるかもしれません。

12/30追記
4.09で参照できるようになりました。 Mroonga - Mroonga 4.09リリース!

mysql> INSERT INTO memos VALUES (1, "今日は雨だなぁ。");
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM terms;
+------+
| term |
+------+
|      |
|      |
|      |
|      |
|      |
|      |
|      |
|      |
+------+
8 rows in set (0.01 sec)
mysql> SELECT mroonga_command('select terms');
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| mroonga_command('select terms')                                                                                                                                                                                                 |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| [[[8],[["_id","UInt32"],["_key","ShortText"],["memo","memos"],["term","ShortText"]],
[8,"。",1,""],[7,"ぁ",1,""],[5,"だな",1,""],[6,"なぁ",1,""],
[3,"は雨",1,""],[1,"今日",1,""],[2,"日は",1,""],[4,"雨だ",1,""]]] |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

ノーマライザーの指定方法

テーブルCOMMENTでのノーマライザーの指定方法は以下のようになります。

CREATE TABLE terms (
term VARCHAR(64) NOT NULL PRIMARY KEY
) Engine=Mroonga COMMENT='default_tokenizer "TokenBigram", normalizer "NormalizerAuto"' DEFAULT CHARSET=utf8;
CREATE TABLE memos (
id INT NOT NULL PRIMARY KEY,
content TEXT NOT NULL,
FULLTEXT INDEX (content) COMMENT 'table "terms"'
) Engine=Mroonga DEFAULT CHARSET=utf8;

トークンフィルターの指定方法

テーブルCOMMENTでのトークンフィルターの指定方法は以下のようになります。

以下は、TokenFilterStopWordトークンフィルターを使用する例です。

SELECT mroonga_command('register token_filters/stop_word');
CREATE TABLE terms (
term VARCHAR(64) NOT NULL PRIMARY KEY,
is_stop_word BOOL NOT NULL
) Engine=Mroonga COMMENT='default_tokenizer "TokenBigram", normalizer "NormalizerAuto", token_filters "TokenFilterStopWord"' DEFAULT CHARSET=utf8;
CREATE TABLE memos (
id INT NOT NULL PRIMARY KEY,
content TEXT NOT NULL,
FULLTEXT INDEX (content) COMMENT 'table "terms"'
) Engine=Mroonga DEFAULT CHARSET=utf8;

語彙表テーブルtermsis_stop_wordカラムがtrueのトークンが検索対象から除外されます。atheなど、どの文書にも現れており、検索精度に影響が小さく、検索速度に影響が大きいワードを検索対象から除外させることができます。TokenMecabなどを使って単語ごとにトークナイズしていれば、意図的に検索させたくないキーを設定することもできるでしょう。検索時のみ除外されるため、データ挿入後、運用中に適宜ストップワードを変更することができます。

mysql> INSERT INTO memos VALUES (1, "Hello");
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO memos VALUES (2, "Hello and Good-bye");
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO memos VALUES (3, "Good-bye");
Query OK, 1 row affected (0.00 sec)

mysql> REPLACE INTO terms VALUES ("and", true);
Query OK, 2 rows affected (0.00 sec)

mysql> SELECT * FROM memos
    -> WHERE MATCH (content) AGAINST ("+\"Hello and\"" IN BOOLEAN MODE);
+----+--------------------+
| id | content            |
+----+--------------------+
|  1 | Hello              |
|  2 | Hello and Good-bye |
+----+--------------------+
2 rows in set (0.01 sec)

この例では、andが除外されて、Helloのみで検索されています。

本来は、UPDATE terms SET is_stop_word = true WHERE term="${トークン名}";としたいところですが、Mroonga4.07では、PRIMARY KEYの値がSQLから取得できないため、REPLACEか事前にINSERTする必要があります。

ベクターカラムの作成方法

語彙表テーブルを指定する方法と上記のカラムCOMMENTによるテーブル参照を組み合わることによりMroongaでもベクターカラムを作成することができます。

CREATE TABLE Tags (
  name VARCHAR(64) PRIMARY KEY
) ENGINE=Mroonga DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='default_tokenizer "TokenDelimit"';
CREATE TABLE Bugs (
  id INT PRIMARY KEY AUTO_INCREMENT,
  tags TEXT COMMENT 'flags "COLUMN_VECTOR", type "Tags"',
  FULLTEXT INDEX bugs_tags_index(tags) COMMENT 'table "Tags"'
) ENGINE=Mroonga DEFAULT CHARSET=utf8;

詳細は、Qiitaの記事を参照してください。 ベクターカラムは、1:Nの関係がJoinを使わずに1テーブルで表現できたり、ドリルダウンを利用した集計が非常に便利です。

おわりに

トークナイザー、ノーマライザー、トークンフィルターは、共有ライブラリ形式のプラグインで拡張させることができます。

上記のように、これらは、GroongaだけでなくMroongaでも使うことができます。

興味がある人は、自作してみてはいかがでしょうか。

*1:インデックスをつくる対象のテーブルがGroongaと逆になっててさらに混乱しやすい感じです。