mita2 database life

主にMySQLに関するメモです

Mroonga から InnoDB FTS への乗り換えを考えてみた

このエントリーはMySQL Casual Advent Calendar 2019 の7日目です。 実は、毎年 12 /7 日書いてます。

mita2db.hateblo.jp

mita2db.hateblo.jp

--

昨日は、@SHINOHARATTT さんでした。 ポケモンを題材にして論理設計を学ぶ というエントリーでした。楽しく学べて良いです!

qiita.com

--

本エントリーではMroonga と InnoDB FTS の比較を軽くしてみたいと思います。

InnoDB FTS に乗り換えるモチベーション

  1. 耐障害性を高めたい
    mroonga ストレージエンジンは クラッシュセーフではありません。運が悪いと障害時に直前にコミットした内容が失われる可能性があります。 ラッパーモードでもテーブル本体はInnoDBなため、クラッシュセーフですが、インデックスはクラッシュセーフではありません。 障害時にテーブルとインデックスの整合性が崩れ「テーブルにはデータがあるのに、全文検索でSELECTするとヒットしない」という状況になりえます*1InnoDB FTS であればクラッシュセーフです。

  2. Group Replication への準備
    MySQL Group Replication は InnoDB のみをサポートします。GR構成を利用するには、mroongaを脱却する必要があります。とはいえ、GR構成でマスターを運用しつつ、スレーブだけ mroonga のストレージエンジンを使うという回避方法もあります。

mroonga を使い続けるモチベーション

  1. パフォーマンス
    mroongaとInnoDB FTSの性能特性には大きな違いがあります。 以下の数値は @ktou さんのスライドから、お借りしたものです。このテストケースでは mroonga のほうが圧倒的に高速です。 どちらが速いかはケースバイケースだと思いますが、InnoDB FTSだとパフォーマンス要件を満たせないケースも十分あり得ます。 f:id:mita2db:20191206154558p:plain

  2. 豊富なトークナイザ
    InnoDB FTSはシンプルなN-gramMecabによるトークナイズしかサポートしていません。 mroongaは多くのトークナイザをサポートしており、要件に応じてノイズの少ない検索結果を得ることができます。 実際に比較してみた例を、この後に記載しています。

  3. NATURAL LANGAGE MODE で結果セット(SELECT結果)が変化する
    Mroonga と InnoDB FTS ではスコアの計算方法が違います。 また、スコアだけでなく、マッチするレコード数も変わってしまいます。 これについては、また、別のエントリーで書きたいと思います。

  4. 豊富な検索オプション
    以下は、mroonga のスニペットの例です*2。検索語句にマッチした文字列の前後に、任意の文字列を付加できます。 スニペット機能を使うことで、マッチした文字列をハイライトする機能が簡単に実装できます。

mysql> SELECT id, text, mroonga_snippet(text, 8, 2, 'ascii_general_ci', 1, 1, '...', '...<br>', 'fulltext', '<span class="w1">', '</span>', 'MySQL', '<span class="w2">', '</span>', 'search', '<span calss="w3">', '</span>') FROM snippet_test WHERE MATCH(text) AGAINST ('+fulltext' IN BOOLEAN MODE);
+----+-------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| id | text                                                                                            | mroonga_snippet(text, 8, 2, 'ascii_general_ci', 1, 1, '...', '...<br>', 'fulltext', '<span class="w1">', '</span>', 'MySQL', '<span class="w2">', '</span>', 'search', '<span calss="w3">', '</span>') |
+----+-------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|  1 | An open-source fulltext search engine and column store.                                         | ...<span class="w1">fulltext</span>...<br>... <span calss="w3">search</span> ...<br>                                                                                                                   |
|  2 | An open-source storage engine for fast fulltext search with MySQL.                              | ...<span class="w1">fulltext</span>...<br>... <span calss="w3">search</span> ...<br>                                                                                                                   |
|  3 | Tritonn is a patched version of MySQL that supports better fulltext search function with Senna. | ...f <span class="w2">MySQL</span> ...<br>...<span class="w1">fulltext</span>...<br>                                                                                                                   |
+----+-------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
3 rows in set (0.00 sec)

スニペット以外にも、「痒い所に手が届く」機能がmroongaには多くあります。

トークナイザ比較

以下のようなテストデータで、代表的なトークナイザの結果比較を行います。

mysql> SELECT * FROM bigram_fts ;
+----+------------------------------------------------+
| id | c1                                             |
+----+------------------------------------------------+
|  1 | yoku0825さん、おはようございMySQL!                |
+----+------------------------------------------------+
2 rows in set (0.00 sec)

Mroonga TokenBigram vs InnoDB FTS ngram + ngram_token_size = 2

TokenBigram は mroonga のデフォルトのトークナイザです。Bigramは2文字ずつに区切ってトークナイズします。

Mroonga の TokenBigram と同じ2文字区切りにするため、ngram_token_size は2を設定しています。

InnoDB FTS の場合: ※ マッチした部分に * でマークしています。

mysql> SELECT * FROM bigram_fts WHERE MATCH(`c1`) AGAINST('SQ' IN BOOLEAN MODE);
+----+------------------------------------------------+
| id | c1                                             |
+----+------------------------------------------------+
|  1 | yoku0825さん、おはようございMy*SQ*L!              |
+----+------------------------------------------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM bigram_fts WHERE MATCH(`c1`) AGAINST('82' IN BOOLEAN MODE);
+----+------------------------------------------------+
| id | c1                                             |
+----+------------------------------------------------+
|  1 | yoku0*82*5さん、おはようございMySQL!              |
+----+------------------------------------------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM bigram_fts WHERE MATCH(`c1`) AGAINST('5さ' IN BOOLEAN MODE);
+----+------------------------------------------------+
| id | c1                                             |
+----+------------------------------------------------+
|  1 | yoku082*5さ*ん、おはようございMySQL!              |
+----+------------------------------------------------+
1 row in set (0.00 sec)

Mroonga の場合:

mysql> SELECT * FROM bigram_mrn WHERE MATCH(`c1`) AGAINST('SQ' IN BOOLEAN MODE);
Empty set (0.00 sec)

mysql> SELECT * FROM bigram_mrn WHERE MATCH(`c1`) AGAINST('82' IN BOOLEAN MODE);
Empty set (0.00 sec)

mysql> SELECT * FROM bigram_mrn WHERE MATCH(`c1`) AGAINST('5さ' IN BOOLEAN MODE);
Empty set (0.00 sec)

InnoDB FTS では LIKE検索のように、単に文字列が存在していれば、SELECT結果に含まれます。 一方、Mroongaでは検索にマッチしませんでした。

この差は、MroongaのTokenBigramの以下の仕様に由来します。

バイグラムでトークナイズする。ただし、連続したアルファベット・連続した数字・連続した記号はそれぞれ1つのトークンとして扱う。そのため、3文字以上のトークンも存在する。これはノイズを減らすためである。

MySQL」や「0825」であればどちらのストレージエンジンでもマッチします。

Mroonga TokenBigramSplitSymbolAlphaDigit vs InnoDB FTS ngram + ngram_token_size = 2

Mroonga の TokenBigramSplitSymbolAlphaDigit の説明には以下のように記載があります。 このトークナイザであれば、Mroonga と InnoDB FTS の結果は一致しそうです。

バイグラムでトークナイズする。 TokenBigramSplitSymbolAlpha に加えて、連続した数字も特別扱いせずに通常のバイグラムの処理を行う。つまり、すべての字種を特別扱いせずにバイグラムの処理を行う。

念のため、試してみます。

Mroonga の場合:

mysql> SELECT * FROM bigram_mrn_alphadigit WHERE MATCH(`c1`) AGAINST ('SQ' IN BOOLEAN MODE);
+----+------------------------------------------------+
| id | c1                                             |
+----+------------------------------------------------+
|  1 | yoku0825さん、おはようございMySQL!            |
+----+------------------------------------------------+
1 row in set (0.03 sec)

mysql> SELECT * FROM bigram_mrn_alphadigit WHERE MATCH(`c1`) AGAINST ('82' IN BOOLEAN MODE);
+----+------------------------------------------------+
| id | c1                                             |
+----+------------------------------------------------+
|  1 | yoku0825さん、おはようございMySQL!            |
+----+------------------------------------------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM bigram_mrn_alphadigit WHERE MATCH(`c1`) AGAINST ('5さ' IN BOOLEAN MODE);
+----+------------------------------------------------+
| id | c1                                             |
+----+------------------------------------------------+
|  1 | yoku0825さん、おはようございMySQL!            |
+----+------------------------------------------------+
1 row in set (0.00 sec)

TokenBigramSplitSymbolAlphaDigit をつかっているケースでは、InnoDB FTS の ngram と同一の結果になることが確認できました。

まとめ

ということで、mroonga から InnoDB FTS に変更することを検討してみた結果でした。 mroongaのデフォルトの TokenBigram の挙動が InnoDB FTS とは異なるため、注意が必要そうです。

明日のAdvent Calendar は、@kk2170さんです。 去年はMySQLでクリスマスケーキを焼いてらっしゃいました。今年はどんな内容か楽しみです。

*1:その場合、テーブルをリビルドし、インデックスを再作成することで解決します

*2:mroonga 公式マニュアルより