2016年7月25日月曜日

MySQL 5.7のngramパーサーはアルファベットを含む文章を上手くトークナイズできない

MySQL Bugs: #82330: Don't recursively-evaluate stopword after tokenize

ngramパーサー を使ってアルファベット混じりの日本語全文検索をしようとすると悲劇が起こる。デフォルトのストップワード一覧は このへん


MySQL 5.7.13で全文検索INDEXを使ってるんですけど半角英字でヒットする奴とヒットしないやつが居るんですけどこれって何ででしょう… ちなみに NGRAMを2文字 です。 例えばbabyって単語が含まれてる文章でbabyって単語がヒットしません。 stop wordsには含まれてないんですがなんでだろうと…

http://mysql-casual.slackarchive.io/general/-/1466580139/1469433820/1469174336000008/


mysql57> CREATE TABLE t1 (num serial, val varchar(32), FULLTEXT KEY (val) WITH PARSER ngram);
Query OK, 0 rows affected (0.40 sec)

mysql57> INSERT INTO t1 VALUES (1, 'baby');
Query OK, 1 row affected (0.04 sec)

mysql57> SELECT * FROM t1;
+-----+------+
| num | val  |
+-----+------+
|   1 | baby |
+-----+------+
1 row in set (0.01 sec)

mysql57> SELECT * FROM t1 WHERE MATCH(val) AGAINST ('baby' IN BOOLEAN MODE);
Empty set (0.02 sec)

mysql57> SET GLOBAL innodb_ft_aux_table = 'd1/t1';
Query OK, 0 rows affected (0.01 sec)

mysql57> SELECT * FROM i_s.INNODB_FT_INDEX_CACHE;
Empty set (0.05 sec)

確かに何も入らない。俺の知ってるBigramなら "ba", "ab", "by" の3つのトークンが入るはずなのに。


ソースコードをざっくり流してみたところ、

/*Ngram checks whether the token contains any words in stopwords.
We can't simply use CONTAIN to search in stopwords, because it's
built on COMPARE. So we need to tokenize the token into words
from unigram to f_n_char, and check them separately. */

https://github.com/mysql/mysql-server/blob/mysql-5.7.13/storage/innobase/fts/fts0fts.cc#L4790-L4845


( ゚д゚) ・・・

(つд⊂)ゴシゴシ

(;゚д゚) ・・・

(つд⊂)ゴシゴシゴシ
_, ._
(;゚ Д゚) …!?


トークナイズした後に、トークンにストップワードが含まれてないか、1gramからngramまで順番に評価する…だと…!?

よく見たらドキュメントにも書いてある。

ngram Parser Stopword Handling

The built-in MySQL full-text parser compares words to entries in the stopword list. If a word is equal to an entry in the stopword list, the word is excluded from the index. For the ngram parser, stopword handling is performed differently. Instead of excluding tokens that are equal to entries in the stopword list, the ngram parser excludes tokens that contain stopwords. For example, assuming ngram_token_size=2, a document that contains “a,b” is parsed to “a,” and “,b”. If a comma (“,”) is defined as a stopword, both “a,” and “,b” are excluded from the index because they contain a comma.

By default, the ngram parser uses the default stopword list, which contains a list of English stopwords. For a stopword list applicable to Chinese, Japanese, or Korean, you must create your own. For information about creating a stopword list, see Section 13.9.4, “Full-Text Stopwords”.

Stopwords greater in length than ngram_token_size are ignored.

http://dev.mysql.com/doc/refman/5.7/en/fulltext-search-ngram.html


というわけで、MySQL 5.7.13現在のngramパーサーは

1. "baby" を "ba", "ab", "by"にトークナイズする
2. "ba" を "b", "a" に再分割してストップワード評価。"a" がストップワードなのでこの "ba" のトークンは登録されない。
3. "ab" を "a", "b" に再分割してストップワード評価。"a" がストップワードなのでこの "ab" のトークンも登録されない
4. "by" は再分割する前からストップワードなので "by" のトークンも登録されない

たまたま、"baby"は完全にトークナイズされないワードだった…:(;゙゚'ω゚'):

他にデフォルトのストップワードである "to" を含む "TOKYO" なんかは、

mysql57> truncate t1;
Query OK, 0 rows affected (0.41 sec)

mysql57> INSERT INTO t1 VALUES (1, 'tokyo');
Query OK, 1 row affected (0.04 sec)

mysql57> SELECT * FROM t1;
+-----+-------+
| num | val   |
+-----+-------+
|   1 | tokyo |
+-----+-------+
1 row in set (0.03 sec)

mysql57> SELECT * FROM t1 WHERE MATCH(val) AGAINST ('tokyo' IN BOOLEAN MODE);
+-----+-------+
| num | val   |
+-----+-------+
|   1 | tokyo |
+-----+-------+
1 row in set (0.04 sec)

mysql57> SET GLOBAL innodb_ft_aux_table = 'd1/t1';
Query OK, 0 rows affected (0.00 sec)

mysql57> SELECT * FROM i_s.INNODB_FT_INDEX_CACHE ORDER BY position;
+------+--------------+-------------+-----------+--------+----------+
| WORD | FIRST_DOC_ID | LAST_DOC_ID | DOC_COUNT | DOC_ID | POSITION |
+------+--------------+-------------+-----------+--------+----------+
| ok   |            2 |           2 |         1 |      2 |        1 |
| ky   |            2 |           2 |         1 |      2 |        2 |
| yo   |            2 |           2 |         1 |      2 |        3 |
+------+--------------+-------------+-----------+--------+----------+
3 rows in set (0.00 sec)

"to"の部分以外はトークナイズされるので、一見検索できてそうに見えるけど、やっぱり "to" のトークンが握りつぶされているので


mysql57> INSERT INTO t1 VALUES (2, 'okyo');
Query OK, 1 row affected (0.03 sec)

mysql57> SELECT * FROM t1;
+-----+-------+
| num | val   |
+-----+-------+
|   1 | tokyo |
|   2 | okyo  |
+-----+-------+
2 rows in set (0.01 sec)

mysql57> SELECT * FROM t1 WHERE MATCH(val) AGAINST ('tokyo' IN BOOLEAN MODE);
+-----+-------+
| num | val   |
+-----+-------+
|   1 | tokyo |
|   2 | okyo  |
+-----+-------+
2 rows in set (0.01 sec)

こうなる(´・ω・`)

取り敢えずの回避策は、 デフォルトのストップワードリスト を使わせなければいいので、 innodb_ft_server_stopword_table を指定してやればいいはず。


mysql57> CREATE TABLE stopwords (value varchar(255) NOT NULL PRIMARY KEY);
Query OK, 0 rows affected (0.16 sec)

mysql57> SET GLOBAL innodb_ft_server_stopword_table = 'd1/stopwords';
Query OK, 0 rows affected (0.02 sec)

mysql57> OPTIMIZE TABLE t1;
+-------+----------+----------+-------------------------------------------------------------------+
| Table | Op       | Msg_type | Msg_text                                                          |
+-------+----------+----------+-------------------------------------------------------------------+
| d1.t1 | optimize | note     | Table does not support optimize, doing recreate + analyze instead |
| d1.t1 | optimize | status   | OK                                                                |
+-------+----------+----------+-------------------------------------------------------------------+
2 rows in set (0.68 sec)

mysql57> SELECT * FROM t1;
+-----+-------+
| num | val   |
+-----+-------+
|   1 | tokyo |
|   2 | okyo  |
+-----+-------+
2 rows in set (0.01 sec)

mysql57> SELECT * FROM t1 WHERE MATCH(val) AGAINST ('tokyo' IN BOOLEAN MODE);
+-----+-------+
| num | val   |
+-----+-------+
|   1 | tokyo |
+-----+-------+
1 row in set (0.07 sec)

mysql57> SELECT * FROM i_s.INNODB_FT_INDEX_CACHE ORDER BY position;
+------+--------------+-------------+-----------+--------+----------+
| WORD | FIRST_DOC_ID | LAST_DOC_ID | DOC_COUNT | DOC_ID | POSITION |
+------+--------------+-------------+-----------+--------+----------+
| ok   |            2 |           3 |         2 |      3 |        0 |
| to   |            2 |           2 |         1 |      2 |        0 |
| ky   |            2 |           3 |         2 |      3 |        1 |
| ok   |            2 |           3 |         2 |      2 |        1 |
| ky   |            2 |           3 |         2 |      2 |        2 |
| yo   |            2 |           3 |         2 |      3 |        2 |
| yo   |            2 |           3 |         2 |      2 |        3 |
+------+--------------+-------------+-----------+--------+----------+
7 rows in set (0.02 sec)

回避できた。
しかしこれ、どうしてこんな仕様にした。。

0 件のコメント :

コメントを投稿