mroongaで記号類を含むクエリのシンタックスエラーを回避する方法

> もっとgroongaを知ってもらおう!ということで週刊groongaをはじめました。毎週木曜にgroongaやmroonga、rroongaのトピックを投稿予定です。

[gihyo.jp](http://gihyo.jp/)さんでgroongaの隔週更新連載が始まっています。第7回の記事も公開されたので、一読をおすすめします。

* [「第7回 [実録] MySQL向け全文検索エンジン「Tritonn」から「mroonga」への移行ガイド（2）」](http://gihyo.jp/dev/clip/01/groonga/0007)

第1回から第6回までの過去記事については、[隔週連載groonga](http://gihyo.jp/dev/clip/01/groonga)のページを参照してください。

# はじめに

MySQLで高速に全文検索するためのオープンソースのストレージエンジンとして[mroonga](http://mroonga.org/ja/) (むるんが)を公開しています。
最新のバージョンは2013年7月29日にリリースした[3.06](http://mroonga.org/ja/docs/news.html#release-3-06-2013-7-29)です。

今回は、mroongaで記号類を含むクエリのシンタックスエラーを回避する方法を紹介します。

# 特別な文字とシンタックスエラー

mroongaではクエリにおいて特別扱いする文字がいくつかあります。そのため、注意しないと全文検索しようとしてエラーになってしまうケースがあります。

例えば、以下のようなエラーに遭遇したことはないでしょうか。

```
ERROR 1064 (42000) at line 23: failed to parse fulltext search keyword: <(仮)*>: 
```

mroongaはgroongaをストレージエンジンとして使うので[エスケープ](http://groonga.org/ja/docs/reference/grn_expr/query_syntax.html#escape)しなければいけない特別な文字がいくつかあります。(以下groongaのマニュアルより)

|記号| エスケープ|
|:-----:|:--------------|
|" | \"|
|'  | \'|
|(  | \(|
|)| \)|

また、mroongaで対応しているプラグマもあるので、そのあたりにも注意が必要になってきます。

@yoshi_kenさんがエントリとしてまとめてくださっているのでそちらも参考にしてください。

* [mroongaで特殊記号を用いた全文検索を行うときのエスケープ方法](http://y-ken.hatenablog.com/entry/mroonga-query-escape-string)

# 実際のシンタックスエラーへの対処

では、実際の例をもとに問題に対処してみましょう。

サンプルのスキーマは以下の通りとします。

```
CREATE TABLE test_escape (
  id int(11) NOT NULL AUTO_INCREMENT,
  content varchar(255) NOT NULL COLLATE 'utf8_unicode_ci',
  PRIMARY KEY (id),
  FULLTEXT INDEX(content) COMMENT 'parser "TokenDelimit"'
) ENGINE=mroonga DEFAULT CHARSET=utf8;
```

サンプルデータを以下のようにして投入します。

```
INSERT INTO test_escape(content) VALUES ("(括弧)");
INSERT INTO test_escape(content) VALUES ("括弧");
INSERT INTO test_escape(content) VALUES ('"ダブルクォート"');
INSERT INTO test_escape(content) VALUES ("ダブルクォート");
INSERT INTO test_escape(content) VALUES ("'シングルクォート'");
INSERT INTO test_escape(content) VALUES ("シングルクォート");
INSERT INTO test_escape(content) VALUES ("(仮)*");
INSERT INTO test_escape(content) VALUES ("(仮)テスト");
INSERT INTO test_escape(content) VALUES ("仮*");
INSERT INTO test_escape(content) VALUES ("仮面");
INSERT INTO test_escape(content) VALUES ("<meta>");
INSERT INTO test_escape(content) VALUES ("meta");
INSERT INTO test_escape(content) VALUES ("\\バックスラッシュ");
```

いくつか肩ならしに、記号類を含むデータに対して検索してみましょう。

``(``そのものを検索する場合には以下のようなクエリとなるでしょう。適切なエスケープを行うことで検索できます。

```
mysql> SELECT * FROM test_escape WHERE MATCH(content) AGAINST('\\(' IN BOOLEAN MODE);
+----+----------------+
| id | content        |
+----+----------------+
|  1 | (括弧)         |
|  7 | (仮)*          |
|  8 | (仮)テスト     |
+----+----------------+
3 rows in set (0.00 sec)
```

では、次に``仮``を前方一致検索してみます。

```
mysql> SELECT * FROM test_escape WHERE MATCH(content) AGAINST('仮*' IN BOOLEAN MODE);
+----+---------+
| id | content |
+----+---------+
| 10 | 仮面    |
|  9 | 仮*     |
+----+---------+
2 rows in set (0.00 sec)
```

何も問題ありませんね。

今度は、``(仮)``を前方一致検索してみましょう。

```
mysql> SELECT * FROM test_escape WHERE MATCH(content) AGAINST('(仮)*' IN BOOLEAN MODE);
ERROR 1064 (42000): failed to parse fulltext search keyword: <(仮)*>: <Syntax error! ((仮)*)>
```

うっかりエスケープしていなくてエラーになってしまいました。クエリを修正します。

```
mysql> SELECT * FROM test_escape WHERE MATCH(content) AGAINST('\\(仮\\)*' IN BOOLEAN MODE);
+----+----------------+
| id | content        |
+----+----------------+
|  8 | (仮)テスト     |
|  7 | (仮)*          |
+----+----------------+
2 rows in set (0.00 sec)
```

これできちんと検索できる理由は、groongaのクエリパーサーに``\(``を渡してあげると、特別な意味のある文字列が渡されたとみなすからです。
そこで、MySQLが文字列のリテラルでバックスラッシュを解釈することを踏まえてエスケープします。

この検索クエリの解釈の流れをMySQL、mroonga、groongaとレイヤをたどっていく順に書くと以下のようになります。
便宜的に入力、出力という書き方をしています。

|検索クエリ| 入力|出力|説明|
|:-------|:---|:--|:--|
|MySQL|'\\\\(仮\\\\)*'| '\\(仮\\)*' |エスケープを解釈してmroongaに渡す。|
|mroonga| '\\(仮\\)*'|'\\(仮\\)*'| mroongaはクエリを素通しするのでクエリに変化なし。|
|groonga| '\\(仮\\)*'|'(仮)*' | groongaは'\('を'('と解釈して検索実行するので'(仮)'の前方一致検索となる。|

上記を踏まえると、先程の``(仮)``の前方一致検索のうち``(仮)*``を絞り込みたいときにクエリをどうかけばいいかもわかります。

```
mysql> SELECT * FROM test_escape WHERE MATCH(content) AGAINST('\\(仮\\)\\**' IN BOOLEAN MODE);
+----+---------+
| id | content |
+----+---------+
|  7 | (仮)*   |
+----+---------+
1 row in set (0.00 sec)
```

きちんと検索できました。
これで、記号類が元データに含まれていても、どうエスケープしてあげればいいかがわかったので、シンタックスエラーもこわくありませんね。

# まとめ

今回は、mroongaで記号類を含むクエリのシンタックスエラーを回避する方法について紹介しました。

mroongaに興味を持ったなら、まずは[インストール](http://mroonga.org/ja/docs/install.html)して試してみてください。

mroongaの基本的な動作を知るための[ユーザガイド](http://mroonga.org/ja/docs/userguide.html)もあります。インストールしたら試してみてください。