Mattermost(v2.1.0)の日本語メッセージ全文検索をMySQL 5.7の標準ngramパーサを使い暫定対応

Mattermost 2.1.0が2016/03/16にリリースされました。

@yuya-ocによるelectron製クライアントが公式採用されたり、[公式ブログ](https://www.mattermost.org/category/blog/)にどっかで見たような画面が出てきたり、["If anyone is interested in working on a Japanese translation, or other language translations, please join in the discussion."](https://github.com/mattermost/platform/issues/2444) と絶賛日本語ローカライズしてくれる人大募集中らしくていろいろアツい感じです。

で、日本語検索が動かなかったのが今更気になったので軽く追ってみたところ、インデックス貼るだけで対応できそうだったので展開させていただきます。
皆様の世知辛いMattermost生活の一助になれば幸いです。

> 追伸: 2016/03/28
既出でした。。。 [Cannot search CJK contents · Issue #2033 · mattermost/platform](https://github.com/mattermost/platform/issues/2033#issuecomment-183872616)

# 概要

こちらでわかりやすくまとめられておりますが、簡単に検索に関する部分だけ。
[Mattermost の検索機能 - Qiita](http://qiita.com/usutani/items/36777c14502c80f86336)

## 検索SQL組み立て

* Postgres
    * [platform/sql_post_store.go at v1.2.1 · mattermost/platform](https://github.com/mattermost/platform/blob/v1.2.1/store/sql_post_store.go#L640)
* MySQL
    * [platform/sql_post_store.go at v1.2.1 · mattermost/platform](https://github.com/mattermost/platform/blob/v1.2.1/store/sql_post_store.go#L650)
    
## 全文検索インデックス作成

* Postgres
    * [platform/sql_store.go at v1.2.1 · mattermost/platform](https://github.com/mattermost/platform/blob/v1.2.1/store/sql_store.go#L410)
* MySQL
    * [platform/sql_store.go at v1.2.1 · mattermost/platform](https://github.com/mattermost/platform/blob/v1.2.1/store/sql_store.go#L439)

どちらも各RDBMSの標準的な全文検索を使っているだけのようです。
特にMySQL側は、5.7から標準で対応しているngramパーサでインデックスを貼り直せば簡単にそのまま動きそうです。

## 参考: MySQL ngram全文検索パーサ

* [まだ日本語全文検索で消耗してるの？ - Qiita](http://qiita.com/ArimaRyunosuke/items/7b74316c03ef02276675)
* [InnoDB 全文検索 : N-gram Parser | MySQL Server Blog](http://mysqlserverteam.com/innodb-%E5%85%A8%E6%96%87%E6%A4%9C%E7%B4%A2-n-gram-parser/)
* [MySQL :: MySQL 5.7 Reference Manual :: 12.9.8 ngram Full-Text Parser](https://dev.mysql.com/doc/refman/5.7/en/fulltext-search-ngram.html)

# 対応方針

* RDBMSをMySQL 5.7.xに変更
* FULLTEXT Indexの貼り直し

## RDBMSをMySQL 5.7.xに変更

Vagrantの動作サンプルをMySQL 5.7.xを使うように修正しました。(Postgresqlでも引き続き使えます)

[Mattermost Sample for Vagrant (VirtualBox + CentOS 7.2.x)](https://github.com/terukizm/mattermost-vagrant)


## my.cnf設定

Mattermostの文字コードが`utf8mb4`なので、ラージインデックス設定を入れてあります。
あと、DB接続文字列を公式サンプルそのままにするためにvalidate_passwordプラグインを殺しておきました。
本運用する場合はその他のパラメータ含め、適宜設定してください。

```my.cnf
[mysql]
default-character-set = utf8mb4

[mysqld]
# setting for utf8mb4
character-set-server = utf8mb4
innodb_large_prefix = ON
innodb_file_format = Barracuda
innodb_file_format_max = Barracuda

# for test-env setting (DO NOT USE PRODUCTION!!)
validate_password=OFF
```

## インデックス貼り直し

### スキーマ確認

```shell
$  mysqldump -d mattermost -u mmuser -pmmuser_password > ddl.sql

$  grep "FULLTEXT" ddl.sql
  FULLTEXT KEY `idx_posts_message_txt` (`Message`),
  FULLTEXT KEY `idx_posts_hashtags_txt` (`Hashtags`)
```

```mysql:ddl.sql
CREATE TABLE `Posts` (
  `Id` varchar(26) NOT NULL,
  `CreateAt` bigint(20) DEFAULT NULL,
  `UpdateAt` bigint(20) DEFAULT NULL,
  `DeleteAt` bigint(20) DEFAULT NULL,
  `UserId` varchar(26) DEFAULT NULL,
  `ChannelId` varchar(26) DEFAULT NULL,
  `RootId` varchar(26) DEFAULT NULL,
  `ParentId` varchar(26) DEFAULT NULL,
  `OriginalId` varchar(255) DEFAULT NULL,
  `Message` text,
  `Type` varchar(26) DEFAULT NULL,
  `Props` text,
  `Hashtags` text,
  `Filenames` text,
  PRIMARY KEY (`Id`),
  KEY `idx_posts_update_at` (`UpdateAt`),
  KEY `idx_posts_create_at` (`CreateAt`),
  KEY `idx_posts_channel_id` (`ChannelId`),
  KEY `idx_posts_root_id` (`RootId`),
  FULLTEXT KEY `idx_posts_message_txt` (`Message`),
  FULLTEXT KEY `idx_posts_hashtags_txt` (`Hashtags`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

FULLTEXTってPostsのメッセージとハッシュタグ(twitterみたいに `#タグ` を付与できる機能)しかないんですね。
ハッシュタグの方は完全一致のほうがいいと思うので、そのままにしておきます。

### ngramインデックスで貼り直し

既存のインデックスを落として、新しくngramを指定して貼り直します。

```mysql
$  mysql mattermost -u mmuser -pmmuser_password

mysql> ALTER TABLE Posts DROP INDEX idx_posts_message_txt;
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> ALTER TABLE Posts ADD FULLTEXT INDEX idx_posts_message_txt (`Message`) WITH PARSER ngram COMMENT 'ngram index sample';
Query OK, 0 rows affected (0.22 sec)
Records: 0  Duplicates: 0  Warnings: 0


mysql> show index from Posts;
+-------+------------+------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+--------------------+
| Table | Non_unique | Key_name               | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment      |
+-------+------------+------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+--------------------+
| Posts |          0 | PRIMARY                |            1 | Id          | A         |           5 |     NULL | NULL   |      | BTREE      |         |                    |
| Posts |          1 | idx_posts_update_at    |            1 | UpdateAt    | A         |           5 |     NULL | NULL   | YES  | BTREE      |         |                    |
| Posts |          1 | idx_posts_create_at    |            1 | CreateAt    | A         |           5 |     NULL | NULL   | YES  | BTREE      |         |                    |
| Posts |          1 | idx_posts_channel_id   |            1 | ChannelId   | A         |           1 |     NULL | NULL   | YES  | BTREE      |         |                    |
| Posts |          1 | idx_posts_root_id      |            1 | RootId      | A         |           1 |     NULL | NULL   | YES  | BTREE      |         |                    |
| Posts |          1 | idx_posts_hashtags_txt |            1 | Hashtags    | NULL      |           5 |     NULL | NULL   | YES  | FULLTEXT   |         |                    |
| Posts |          1 | idx_posts_message_txt  |            1 | Message     | NULL      |           5 |     NULL | NULL   | YES  | FULLTEXT   |         | ngram index sample |
+-------+------------+------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+--------------------+
7 rows in set (0.00 sec)
```

コメント欄以外で区別が付かないのですが、これでngramパーサ指定のインデックスで貼り直されたはずです。確認してみます。

# 動作確認

こんな感じで検索結果に出るようになりました。N-gramなんで途中文字列でも当たります。

<img src="https://qiita-image-store.s3.amazonaws.com/0/50184/7ecde3b6-d4bc-c2e6-be93-e1020ac3999a.png" width="1000px">


デフォルトは2-gramみたいですが、[my.cnfの設定で変えられる](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_ngram_token_size)みたいなので、そのへんはお好みで。[MeCabパーサ](https://dev.mysql.com/doc/refman/5.7/en/fulltext-search-mecab.html)を使ってみるのもいいかもですね。

<img src="https://qiita-image-store.s3.amazonaws.com/0/50184/47b0f641-1eb7-28fb-7198-2852e7713122.png" width="1000px">

ハイライトはクライアント側？なので対応してないのですが、jump機能が使えるのであんまり気にならないかもしれません。
とりあえずやっつけ対応にしては上々じゃないでしょうか。

<img src="https://qiita-image-store.s3.amazonaws.com/0/50184/b3145036-1a93-006c-74ea-fe844c876280.png" width="1000px">

副作用として英単語も部分一致になったりします。

<img src="https://qiita-image-store.s3.amazonaws.com/0/50184/17e71afa-be3e-5983-37a3-46ec44a1e2e7.png" width="1000px">

インデックス方式を変えただけなので問題は出ないだろう…と思ってはいたのですが、以下のように関係ない英語文字列がHitしたりしていたので、使っていくといろいろ怪しいかもしれません。(もともとwithはストップワードなのかな…って気もしますが)

<img src="https://qiita-image-store.s3.amazonaws.com/0/50184/023a71c8-3856-29d3-d693-61eb85394ce3.png" width="1000px">

あと上記のテーブル形式とN-gramインデックスだと、きつめのパフォーマンス低下とかあるかもしれません。
インデックス貼っただけなので何かあっても比較的すぐ元に戻せると思いますが、自己責任でよろしくお願い致します。

皆様の世知辛いMattermostライフのお役にたてば幸いです。

## その他

* postgresの場合は`ts_query("english")` で実装されているので、インデックス貼り直すだけではなく手を入れないとダメっぽいのと、posgresいじるのがだんだんめんどくさくなってきて途中で投げました…
  * fulltext-ja + mecabでクエリ投げて動作確認まではできたんですが、RDBMS側で頑張るよりelasticsearchとかの全文検索エンジンと連携させたほうがいいかなって… 
     * Mattermostの実装自体はかなりわかりやすい（と思う）のでどなたか…
* MySQL 5.7の標準組み込みパーサによる全文検索はすごく楽に使えて素晴らしかったです。
  * 他にも全文検索対応してないやつでもこんな感じでやっつけ対応できそうで便利だなって思いました。
  * でも、それ以上に5.7の罠が多すぎて正直辛かったです…

以上です。