隔週連載groonga

第6回[実録] MySQL向け全文検索エンジン「Tritonn」から「mroonga」への移行ガイド(1)

初めまして、吉田健太郎と申します。目黒にある株式会社リブセンスで、Web系インフラの研究開発エンジニアをしております。

MySQLで完結する日本語対応の全文検索プロダクトである「Tritonn(MySQL 5.0+Senna⁠⁠」が広まり始めたのは2008年頃でした。そして時は過ぎること約5年。データやトランザクション数の増加などに伴い、レガシーを捨てMySQL 5.5や5.6への移行を検討したい。しかしSolrへ乗り換えるほどでもなく、引き続きシンプルにSQLを用いた、リレーショナルな日本語対応の全文検索機能を使いたい。私のほかにも、そういった構想をお持ちの方はいるかと思います。

そこで、groongaのMySQLバインディング版である「mroonga」の出番です。MySQL 5.6とmroongaを組み合わせることで、この構想を実現できます。

1ヶ月間の検証期間で見つかった総計16にも及ぶ機能追加や不具合改善を、開発者の皆様と共に乗り越えたことで、mroongaの安定性がとても向上しました。さらに2013年5月のリリースで待望のWプラグマにも対応し、システム移行の条件がすべて整いました。

数回に渡り、私が体験したTritonnからmroongaへのシステム移行プロジェクトをお届けします。

Tritonnが実現できたことと、残された課題

MySQLが標準で備える全文検索機能は簡素なもので、単語をスペースなどで区切るという欧米圏の分かち書きに基づいたトークナイザを前提とした設計です。そのため、日本語文での使用には難がありました。

Tritonnは、日本語全文検索機能の追加と検索の高速化を実現すべく、ビルトインのFULLTEXT検索をSennaに置き換えたプロダクトです。これにより、MyISAMのビルトインFULLTEXTでは使えなかった、MeCabやN-gramでのトークナイザと高速な検索機能を実現しました。しかしながら、Tritonnは次の点で優れた成果を納めたものの、いくつかの課題が残りました。

実現できたこと
  • MeCabやN-gramでのトークナイズに対応
  • 完全転置インデックスを用いた高速な検索を実現
  • 転置インデックスの一部をmmapすることで、データ更新速度の大幅改善を実現
  • MeCabを用いてトークナイズした形態素を単語として転置インデックスとすることで、高精度を実現
  • SQLならではの複雑な問い合わせにも対応できる柔軟性を維持
残された課題
  • パフォーマンス問題
    • LIMIT句, COUNT(*), OR条件追加時の検索が遅い
    • MySQLが持つ1テーブル1インデックスという制約を受け、全文検索以外の条件を加えて絞り込むと遅い
    • MyISAMを利用しているため、更新中はテーブルロックとなり参照クエリを発行できない
  • 信頼性の問題
    • ストレージエンジンがMyISAMのため、トランザクションに対応していない

これらの課題を解決すべく、2009年よりSennaの後継プロジェクトとしてgroongaが動きだし、翌2010年にはgroongaのMySQLバインディング版の開発も始まりました。それがmroongaです。

次の記事も参照ください。

Tritonn, mroonga以外のMySQLで完結する全文検索プロダクト

mroonga移行を検討している際に見つけた、InnoDB単体での全文検索機能と、PARSERプラグインであるMySQL-ftppcという2つの製品を紹介します。

InnoDB fulltext search (InnoDB FTS)

MySQL 5.6.4からは、InnoDB単体でFULLTEXT検索が可能となりました。機能としてはMyISAMが備えていたものと同程度ですが、裏側がInnoDBであるため、参照・更新速度が格段に向上しています。しかしMySQL 5.6.12時点では、CJK(Chinese-Japanese-Korean)非対応なだけでなく、Full-Text Parser Pluginによる拡張に対応していません。

そのため、MySQL-ftppcのようなMeCabやN-gramを用いた日本語検索対応を実現するプラグインの利用が手軽にできないことが惜しいポイントです。

より詳細な情報は次の記事を参照ください。

MySQL-ftppc

MySQL-ftppcとは、MySQL Full Text Parser Plugin Collectionの略で、MyISAMのFULLTEXTインデックスのトークナイザ機能を追加するPARSERプラグインです。

これは5つのトークナイザに対応しており、bigram, mecab, snowball, space, suffixをインデックス毎に使い分けることができます。

パフォーマンスとしてはXeon L5520のマシンで400万行、インデックスファイル(.MYI)のサイズが2GBを対象とした単純な全文検索で1クエリ0.8秒程度の結果でした。Tritonn/mroongaでは0.2秒程度のため、実用性から考えると、数十万件規模のレコードまでをAND検索するシンプルな利用方法に限定されます。

より詳細な情報は次の記事を参照ください。

機能比較

これまでに取り上げた4つのプロダクト、Tritonn、mroonga、MySQL-ftppc、InnoDB FTSの対応機能比較を次の表にまとめました。

Tritonn mroonga MySQL-ftppc InnoDB FTS
InnoDB × ×
MyISAM ×
MeCab ×
N-gram ×
Dプラグマ × ×
Eプラグマ × × ×
Wプラグマ × ×
MySQL 5.0 × × ×
MySQL 5.1 × ×
MySQL 5.5 × ×
MySQL 5.6 ×

mroongaのN-gramについては、bigramだけでなく、uni-gramやtri-gramの他、多数の派生N-gramでのトークナイザに対応しています。さらに2013年5月末リリースのmysql-mroonga-3.04にて、待望のWプラグマに対応しました。これにより、Tritonnからmroongaへ移行する際の障壁はEプラグマのみになったと言えます。

Tritonnから乗り換えるならば、mroongaの1択ですね。

Tritonnからmroongaへ移行する7つのメリット

それでは早速、Tritonnからmroongaヘ移行するメリットを見てみましょう。

MySQL 5.6対応であること

もはやレガシーとなったMySQL 5.0という縛りがなくなることで、MySQL 5.1以降の先進的な機能を取り入れることができます。特にMySQL 5.6ではその進化が甚だしく、次の点を始めとする新機能が利用できます。

  • CPUスケーラビリティの向上
  • サブクエリを始めとするオプティマイザの最適化
  • GTIDを利用したフェイルオーバー
  • memcached API対応
  • より詳細な情報は次の記事を参照ください。

    参照ロックフリー

    ストレージモードでは、groongaの性能特性をフルに活かした高速なデータ更新・全文検索・位置情報検索が可能となります。

    ストレージエンジンにInnoDBを利用できること

    mroongaのラッパーモードを利用すれば、全文検索機能に関してはmroongaが担い、それ以外を別のストレージエンジンに委ねることもできます。これはつまり、歴史が長く信頼性の高いInnoDBといったストレージエンジンが使えるということです。

    InnoDBの主な特徴としては次の通りです。

    • トランザクション対応である
    • クラッシュセーフである
    • 更新と参照が入り乱れた場合の同時実行性能が良い

    MySQL 5.1以降からの大幅な性能向上を果たしたInnoDBが使えるというだけでも、mroongaを導入する価値があります。

    プラガブルストレージエンジンである

    アップデートが容易なプラガブルストレージエンジン、つまりプラグイン形式であることもメリットの一つです。TritonnのようなMySQLのソースに手を入れるパッチ形式ではないため、MySQL本体のバージョンアップが容易です。

    bigram以外の豊富なN-gramトークナイザが利用可能

    Tritonnではmecab、bigramとdelimited(空白区切り)のみの対応でした。mroongaではそれだけでなく、uni-gram、tri-gramの他、多数の派生N-gramでのトークナイズに対応しています。

    デフォルトはparser "TokenBigram"が指定されているものとして動作しますが、次のように指定することもできます。

    CREATE TABLE search (
      id INT PRIMARY KEY AUTO_INCREMENT,
      content VARCHAR(255),
      FULLTEXT INDEX (content) COMMENT 'parser "TokenUnigram"'
    ) ENGINE=mroonga DEFAULT CHARSET=utf8;

    より強化された文字列正規化機能が利用可能

    Tritonnでは、MySQLが提供するcollate(照合順序)と呼ばれる文字列正規化機能がデフォルトで働いており、NO NORMALIZEと指定することで無効化される挙動でした。

    mroongaでは全文検索機能を完全に外部で持つため、指定のない場合にはgroonga独自のnormalizer(NormalizerAuto)を利用します。これは、大文字・小文字だけでなく、全角・半角を同一視できるというUnicodeのNFKCを用いたとても便利な機能です。

    しかしながら、これまで同様の正規化機能が必要な場合には、別途groonga-normalizer-mysqlパッケージをインストールの上、次のようにインデックスコメントにて、COMMENT 'normalizer "NormalizerMySQLGeneralCI"'のように指定しましょう。

    CREATE TABLE search (
      id INT PRIMARY KEY AUTO_INCREMENT,
      content VARCHAR(255),
      FULLTEXT INDEX (content) COMMENT 'normalizer "NormalizerMySQLGeneralCI"'
    ) ENGINE=mroonga DEFAULT CHARSET=utf8;

    より具体的な挙動の違いに関しては、groonga/mroongaの文字列正規化機能 (normalizer) の挙動を追ってみるの記事が参考になります。

    位置情報検索に対応

    mroongaのストレージモード・ラッパーモード共に、SPATIAL INDEXに対するMBRContains関数を用いた位置情報検索に対応しています。ストレージモードであればPOINT型を、InnoDBラッパーモードであればGEOMETRY型を指定します。

    テーブルのスキーマは次のように定義します。

    -- ストレージモードでは、POINT型を指定する
    CREATE TABLE shops (
      id INT PRIMARY KEY AUTO_INCREMENT,
      name VARCHAR(255),
      location POINT NOT NULL,
      SPATIAL INDEX (location)
    ) ENGINE = mroonga;
    
    -- InnoDBラッパーモードでは、GEOMETRY型を指定する
    CREATE TABLE shops (
      id INT PRIMARY KEY AUTO_INCREMENT,
      name TEXT,
      location GEOMETRY NOT NULL,
      SPATIAL INDEX location_index (location)
    ) ENGINE = mroonga comment = 'engine "innodb"';

    データの登録・検索は次のように行います。

    -- データの登録例
    -- GeomFromText()関数を利用し、文字列からPOINT型に変換します
    INSERT INTO shops VALUES (null, 'Naniwaya', GeomFromText('POINT(139.796234 35.730061)'));
    
    -- データの検索例
    -- 池袋駅(139.7101 35.7292)を左上の点、東京駅(139.7662 35.6815)を右下の点とした長方形内にあるお店を探す場合のクエリです
    SELECT id, name, AsText(location) AS location_text FROM shops
      WHERE MBRContains(GeomFromText('LineString(139.7101 35.7292, 139.7662 35.6815)'), location);

    ただし、データとして格納できるのはPOINT(GEOMETRY)型のみで、LINEなどの他のデータ型は保存できないという制約があります。また、MBRDisjointなどにも対応していません。GEOMETRY型についての詳細は、第5回 位置情報を保存しよう(前編⁠⁠:位置情報サービスのはじめ方が参考になります。

    汎用的に矩形での位置情報検索をするなら、B-Treeを用いた前方一致探索のできるGeoHashという手法があります。このGeoHashの具体的な使い方に関しては、第6回 位置情報を保存しよう(後編⁠⁠:位置情報サービスのはじめ方が参考になります。

    移行する際に気をつけたいポイント

    ストレージモードとラッパーモード

    ストレージモードとは、MySQLのデータベースを利用せずに、groongaのデータベースに全データを格納して利用するモードです。参照ロックフリーの恩恵を受けられるなどのメリットは多くありますが、データ型は厳密にMySQLと同一の挙動を示すわけではありません。

    そういった事情もあり、私としてはTritonnからの移行を行うなら、ラッパーモードの利用をお勧めしたいです。

    ラッパーモードで使うストレージエンジン

    COMMENT='engine "innodb"'のように、テーブルのスキーマにて指定ができます。次のサンプルではinnodbを指定していますが、myisamなど、どんなストレージエンジンの利用も可能です。

    CREATE TABLE search (
      id INT PRIMARY KEY AUTO_INCREMENT,
      content VARCHAR(255),
      FULLTEXT INDEX (content)
    ) ENGINE=mroonga DEFAULT CHARSET=utf8 COMMENT='engine "innodb"';

    ストレージエンジンをMyISAMからInnoDBへ切り替える際の注意

    mroongaと間接的に関わる話として、MyISAMからInnoDBへ乗り換えるときにハマりやすい挙動の違いを紹介します。それは、Auto Incrementの挙動がMyISAMとInnoDBで異なるということです。 Tritonn/MyISAMを利用しているシステムをInnoDB化する際、次の2点について確認が必要です。

    • 以下に該当するクエリを利用している
      • INSERT IGNORE INTO ...
      • INSERT INTO ... ON DUPLICATE KEY UPDATE ...
      • LOAD DATA ... IGNORE INTO ...
    • 行削除するケースがある

    具体的にどのような挙動の違いがあるか、次のテーブルを利用して説明します。

    -- テーブルを作成
    CREATE TABLE test (
      id INT PRIMARY KEY AUTO_INCREMENT,
      name VARCHAR(10),
      UNIQUE INDEX (name)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    IGNORE INTO文の動作の違い

    INSERT IGNORE INTO ...を用いて解説します。なお、INSERT INTO ... ON DUPLICATE KEY UPDATE ...の挙動も同一です。

    -- 1つめのレコードを入れます
    mysql> INSERT IGNORE INTO test (name) VALUES('トマト');
    Query OK, 1 row affected (0.00 sec)
    
    -- 1件目なので、1が返ります
    mysql> SELECT LAST_INSERT_ID()\G
    *************************** 1. row ***************************
    LAST_INSERT_ID(): 1
    1 row in set (0.00 sec)
    
    -- 重複のためスキップされるレコードを入れます
    mysql> INSERT IGNORE INTO test (name) VALUES('トマト');
    Query OK, 0 rows affected (0.00 sec)
    
    -- 重複のため、1のままです
    mysql> SELECT LAST_INSERT_ID()\G
    *************************** 1. row ***************************
    LAST_INSERT_ID(): 1
    1 row in set (0.00 sec)
    
    -- 重複とならないデータを追加します
    mysql> INSERT IGNORE INTO test (name) VALUES('トマト2');
    Query OK, 1 row affected (0.00 sec)
    
    -- MyISAMではidが2となるのですが、InnoDBでは3となります
    mysql> SELECT LAST_INSERT_ID()\G
    *************************** 1. row ***************************
    LAST_INSERT_ID(): 3
    1 row in set (0.00 sec)
    
    -- InnoDBのテーブル内容は以下の通りです
    mysql> select * from test;
    +----+------------+
    | id | name       |
    +----+------------+
    |  1 | トマト     |
    |  3 | トマト2    |
    +----+------------+
    2 rows in set (0.00 sec)

    データを削除した時のプライマリキーの挙動の違い

    InnoDBの場合、Auto Incrementで取る値は保持されません。MySQLを一度終了すると、実際に格納されている主キーの最大値+1に自動調整となる挙動を示します。

    具体例とともに解説します。

    -- まずは1件目のデータを挿入します
    mysql> INSERT INTO test (name) VALUES('トマト1');
    Query OK, 1 row affected (0.02 sec)
    
    -- 続けて2件目のデータを挿入します
    mysql> INSERT INTO test (name) VALUES('トマト2');
    Query OK, 1 row affected (0.02 sec)
    
    -- 期待通り、idは2となりました
    mysql> SELECT LAST_INSERT_ID()\G
    *************************** 1. row ***************************
    LAST_INSERT_ID(): 2
    1 row in set (0.00 sec)
    
    -- idが2のデータを削除します
    mysql> DELETE FROM test WHERE name IN('トマト2');
    Query OK, 1 row affected (0.03 sec)
    
    -- 続けて3件目のデータを挿入します
    mysql> INSERT INTO test (name) VALUES('トマト3');
    Query OK, 1 row affected (0.03 sec)
    
    -- 期待通り、idは3となりました
    mysql> SELECT LAST_INSERT_ID()\G
    *************************** 1. row ***************************
    LAST_INSERT_ID(): 3
    1 row in set (0.00 sec)
    
    -- idが3のデータを削除します
    mysql> DELETE FROM test WHERE name IN('トマト3');
    Query OK, 1 row affected (0.02 sec)
    
    -- ここで、MySQLを再起動し、再度接続後に以下の行を挿入します
    mysql> INSERT INTO test (name) VALUES('トマト4');
    Query OK, 1 row affected (0.03 sec)
    
    -- 再起動により、MyISAMでは4となる一方、InnoDBでは2となりました
    mysql> SELECT LAST_INSERT_ID()\G
    *************************** 1. row ***************************
    LAST_INSERT_ID(): 2
    1 row in set (0.00 sec)
    
    -- InnoDBのテーブル内容は以下の通りです
    mysql> select * from test;
    +----+------------+
    | id | name       |
    +----+------------+
    |  1 | トマト1    |
    |  2 | トマト4    |
    +----+------------+
    2 rows in set (0.01 sec)

    この挙動となる仕組みについては、MySQLリファレンスマニュアルのAUTO_INCREMENT カラムが InnoDB 内でどのように機能するかにて解説されています。

    なお、mroongaのストレージモードも、このInnoDBと同じ挙動となります。これらの挙動によるトラブルが想定できる場合、ラッパーモードを利用した上でストレージエンジンをMyISAMにした方が良いでしょう。COMMENT='engine "myisam"'と指定することで、引き続きMyISAMを利用できます。

    その他、MyISAMとInnoDBの違いを8つの角度から解説している漢(オトコ)のコンピュータ道: MyISAMからInnoDBへ切り替えるときの注意点も併せてご参照ください。

    パーティショニング

    InnoDBラッパーモードそのものでは、パーティショニングに対応していません。もしパーティショニングが必要な場合には、Spider 3.0と組み合わせることで実現できます。

    具体的な方法に関しては、mroongaってなんじゃ?(Spiderで分散全文検索)を参照ください。

    まとめと次回予告

    今回はTritonnとmroongaそれぞれの紹介と移行時の要注意点、現在利用できる日本語全文検索ソリューションの比較を行いました。

    次回は、より具体的なシステム移行に関するお話をお届けする予定です。ご期待ください。

おすすめ記事

記事・ニュース一覧