1. 程式人生 > >MySQL全文檢索fulltext和中日韓文解析外掛ngram使用記錄

MySQL全文檢索fulltext和中日韓文解析外掛ngram使用記錄

專案資料庫中遇到一個情況,有一個欄位儲存的是經銷廠商在工商局註冊時的(官方)全稱,但是使用者在查詢這個欄位時,很多時候都會使用到各種各樣的簡稱別稱。於是,對於該欄位,希望能夠實現類似於搜尋引擎的查詢。就像資料庫裡面存的是“中華人民共和國”,但希望是隻要使用者輸入“中國”、“中華”、“民國”、“中華民國”等關鍵字,最終都能定位到“中華人民共和國”的記錄。這種情況下模糊查詢和正則表示式有點捉襟見肘了,只能寄希望於其他特性。我們用的資料庫是MySQL,後來查了查文件,經過測試,藉助MySQL的全文檢索(Full-Text Search)和相應的中文解析外掛ngram解決這一問題。

在具體介紹MySQL的Full-Text Search和中文解析外掛ngram之前,先聊一聊關於自然語言的話題,MySQL在實現解析時將其分成兩大語系:第一種是use word delimiter language,這類語言單詞與單詞之間有天然的分隔符,例如英語;第二種是ideographic language,字詞之間沒有分隔符,其代表是漢語、日語和韓語(MySQL官方將其統稱為CJK:Chinese、Japanese、Korean)。因為個人只懂中文,學習過英語和日語,所以不能拿其他語言來作例證,這一部分關於自然語言劃分的內容只是看了MySQL的Reference Manual後受到啟發而自己設想的結論。

對於第一種語系,MySQL解析起來非常簡單,只要以空格(white space)作為界定符(delimiter)來進行分詞(tokenize)操作即可。這也是MySQL最初內建的全文檢索。MySQL5.6以前只能用於MyISAM表,5.6版本時InnoDB儲存引擎也實現並擴充套件了這種全文檢索。第二種CJK語系沒有分隔符,即MySQL原始無法自動分詞,所以需要一種新的實現來進行Full-Text Search,這就是前文提到的ngram外掛,由MySQL5.7.6開始引入。

Full-Text Search全文檢索是通過對欄位新增全文索引(FULLTEXT INDEX),然後將索引的每條記錄(document)的文字內容進行分詞,並將分好的單詞(word)記入輔助表(auxiliary table)來實現的。auxiliary table記錄了單詞與FULLTEXT index記錄行的對映。`INFORMATION_SCHEMA`.`INNODB_FT_INDEX_CACHE`系統表記錄了相關分詞資訊。設定引數“innodb_ft_aux_table”可以檢視該表中的記錄。

mysql> CREATE TABLE `player`.`articles` (
    -> `FTS_DOC_ID` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
    -> `title` VARCHAR(200) NULL DEFAULT NULL,
    -> `body` TEXT NULL,
    -> PRIMARY KEY (`FTS_DOC_ID`),
    -> FULLTEXT INDEX `title` (`title`, `body`)
    -> )
    -> ENGINE=InnoDB
    -> ;
Query OK, 0 rows affected (0.25 sec)

mysql> INSERT INTO `player`.`articles` (`title`, `body`) VALUES
    -> ('MySQL Tutorial','DBMS stands for DataBase ...'),
    -> ('How To Use MySQL Well','After you went through a ...'),
    -> ('Optimizing MySQL','In this tutorial we will show ...'),
    -> ('1001 MySQL Tricks','1. Never run mysqld as root. 2. ...'),
    -> ('MySQL vs. YourSQL','In the following database comparison ...'),
    -> ('MySQL Security','When configured properly, MySQL ...');
Query OK, 6 rows affected (0.02 sec)
Records: 6  Duplicates: 0  Warnings: 0

mysql> SET GLOBAL innodb_ft_aux_table="player/articles";
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM `INFORMATION_SCHEMA`.`INNODB_FT_INDEX_CACHE` ORDER BY `doc_id`, `position` LIMIT 8;
+----------+--------------+-------------+-----------+--------+----------+
| WORD     | FIRST_DOC_ID | LAST_DOC_ID | DOC_COUNT | DOC_ID | POSITION |
+----------+--------------+-------------+-----------+--------+----------+
| mysql    |            1 |           6 |         6 |      1 |        0 |
| tutorial |            1 |           3 |         2 |      1 |        6 |
| dbms     |            1 |           1 |         1 |      1 |       15 |
| stands   |            1 |           1 |         1 |      1 |       20 |
| database |            1 |           5 |         2 |      1 |       31 |
| use      |            2 |           2 |         1 |      2 |        7 |
| mysql    |            1 |           6 |         6 |      2 |       11 |
| well     |            2 |           2 |         1 |      2 |       17 |
+----------+--------------+-------------+-----------+--------+----------+
8 rows in set (0.01 sec)

在以上DDL建表SQL中,有一個特殊的欄位“FTS_DOC_ID”。InnoDB儲存引擎固定使用該欄位與記錄行中的每個單詞(一對多)對映,型別必須為“BIGINT UNSIGNED NOT NULL”,並且為其主動加上一個UNIQUE INDEX。所以,InnoDB儲存引擎的Full-Text Search表一定會有一個“FTS_DOC_ID”欄位,如果在建表時沒有定義該欄位,MySQL會隱式地主動新增該欄位。因此,全文檢索表中的“FTS_DOC_ID”欄位直接對應`INNODB_FT_INDEX_CACHE`系統表中的“DOC_ID”列。

  • WORD:分詞出來的單詞。
  • FIRST_DOC_ID:該“WORD”首次出現的“DOC_ID”。
  • LAST_DOC_ID:該“WORD”最後一次次出現的“DOC_ID”。
  • DOC_COUNT:該“WORD”在多少行記錄(多少個“DOC_ID”)中存在。
  • DOC_ID:對應全文檢索表中的“FTS_DOC_ID”欄位(顯式或隱式建立),對映該“WORD”屬於哪行記錄。
  • POSITION。該“WORD”在該“DOC_ID”行的位置(位元組)。

上面的查詢為了簡約展示只返回了8條記錄,包含DOC_ID=1,即第一行記錄的“完整”分詞,而第二行記錄的資訊只顯示了部分。但是對比發現,第一行中的單詞“for”並沒有記入分詞,如果展示完整的查詢結果,會發現幾乎每行記錄都存在這種“棄詞”的情況。這裡涉及到stopword list,在tokenize時,stopword list中的word會直接忽略。在英語中,常見的像“a”、“an”和“the”等詞語並沒有實際的涵義,因此將其記入分詞並沒有多大的意義,只是增加了索引的大小而降低了查詢的效能。InnoDB儲存引擎預設的stopword記錄在`INFORMATION_SCHEMA`.`INNODB_FT_DEFAULT_STOPWORD`系統表,預設包含36個stopword。除此之外,MySQL系統引數“innodb_ft_min_token_size”、“innodb_ft_max_token_size”和“ft_min_word_len”、“ft_max_word_len”用來控制進行tokenize時word的長度區間,不在引數設定區間段內的word也無法全文檢索。前兩個引數控制InnoDB表,後兩個引數控制MyISAM表,它們都是靜態引數(需重啟MySQL伺服器來修改)。

Full-Text Search的查詢語句如下:MATCH()指定被查詢的欄位(全文索引列);AGAINST()指定查詢關鍵字表達式和查詢模式。

MATCH (col1,col2,...) AGAINST (expr [search_modifier])

	search_modifier:
	  {
		   IN NATURAL LANGUAGE MODE
		 | IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION
		 | IN BOOLEAN MODE
		 | WITH QUERY EXPANSION
	  }

①NATURAL LANGUAGE MODE

查詢帶有指定word的文件。因“NATURAL LANGUAGE MODE”為預設全文檢索模式,所以查詢語句中可以省略該關鍵字。

mysql> SELECT * FROM `player`.`articles`
    -> WHERE MATCH (`title`, `body`)
    -> AGAINST ('database' IN NATURAL LANGUAGE MODE);
+------------+-------------------+------------------------------------------+
| FTS_DOC_ID | title             | body                                     |
+------------+-------------------+------------------------------------------+
|          1 | MySQL Tutorial    | DBMS stands for DataBase ...             |
|          5 | MySQL vs. YourSQL | In the following database comparison ... |
+------------+-------------------+------------------------------------------+
2 rows in set (0.00 sec)

mysql> SELECT * FROM `player`.`articles`
    -> WHERE MATCH (`title`, `body`)
    -> AGAINST ('database');
+------------+-------------------+------------------------------------------+
| FTS_DOC_ID | title             | body                                     |
+------------+-------------------+------------------------------------------+
|          1 | MySQL Tutorial    | DBMS stands for DataBase ...             |
|          5 | MySQL vs. YourSQL | In the following database comparison ... |
+------------+-------------------+------------------------------------------+
2 rows in set (0.00 sec)

MATCH()返回一個相關係數(relevance value),查詢結果會按relevance value值降序排列,即相關性最高的結果置於首位。relevance value根據以下條件計算。

  • word是否在記錄中出現
  • word在記錄中出現的次數
  • word在索引欄位中的數量
  • 多少行記錄包含該word

以下查詢可以檢視relevance value。

mysql> SELECT `pa`.`FTS_DOC_ID`, `pa`.`title`, `pa`.`body`,
    -> MATCH (`pa`.`title`, `pa`.`body`) AGAINST ('Tutorial' IN NATURAL LANGUAGE MODE) AS `score`
    -> FROM `player`.`articles` AS `pa`
    -> ORDER BY `score` DESC
    -> LIMIT 4;
+------------+-----------------------+-------------------------------------+---------------------+
| FTS_DOC_ID | title                 | body                                | score               |
+------------+-----------------------+-------------------------------------+---------------------+
|          1 | MySQL Tutorial        | DBMS stands for DataBase ...        | 0.22764469683170319 |
|          3 | Optimizing MySQL      | In this tutorial we will show ...   | 0.22764469683170319 |
|          2 | How To Use MySQL Well | After you went through a ...        |                   0 |
|          4 | 1001 MySQL Tricks     | 1. Never run mysqld as root. 2. ... |                   0 |
+------------+-----------------------+-------------------------------------+---------------------+
4 rows in set (0.00 sec)

②BOOLEAN MODE

AGAINST()中關鍵字的前後字元會有特殊含義。BOOLEAN MODE全文檢索支援以下操作符。

  • +

word必須存在。

  • -

word必須不存在。

  • (no operator)

該word可選,如果出現relevance value更高。

  • @distance

僅用於InnoDB表。查詢多個單詞之間的距離是否在distance(位元組)內。

  • > <

分別表示出現該word時增加和降低relevance value。

  • ~

出現該word時relevance value變負值,用於製造噪音詞(“noise” word)。

  • *

表示以該字串開頭的word。

  • ''

''中的內容視作一個短語(整體)

mysql> /* 查詢包含“tutorial”但不包含“dbms”的記錄 */
mysql> SELECT * FROM `player`.`articles`
    -> WHERE MATCH (`title`, `body`) AGAINST ('+tutorial -dbms' IN BOOLEAN MODE);
+------------+------------------+-----------------------------------+
| FTS_DOC_ID | title            | body                              |
+------------+------------------+-----------------------------------+
|          3 | Optimizing MySQL | In this tutorial we will show ... |
+------------+------------------+-----------------------------------+
1 row in set (0.01 sec)

mysql> /* 查詢“mysql”與“tutorial”之間的距離在2個位元組以內的記錄 */
mysql> SELECT * FROM `player`.`articles`
    -> WHERE MATCH (`title`, `body`) AGAINST ('"mysql tutorial" @2' IN BOOLEAN MODE);
+------------+----------------+------------------------------+
| FTS_DOC_ID | title          | body                         |
+------------+----------------+------------------------------+
|          1 | MySQL Tutorial | DBMS stands for DataBase ... |
+------------+----------------+------------------------------+
1 row in set (0.01 sec)

mysql> SELECT * FROM `player`.`articles`
    -> WHERE MATCH (`title`, `body`) AGAINST ('"mysql tutorial" @4' IN BOOLEAN MODE);
+------------+------------------+-----------------------------------+
| FTS_DOC_ID | title            | body                              |
+------------+------------------+-----------------------------------+
|          1 | MySQL Tutorial   | DBMS stands for DataBase ...      |
|          3 | Optimizing MySQL | In this tutorial we will show ... |
+------------+------------------+-----------------------------------+
2 rows in set (0.00 sec)

mysql> /* 根據單詞“tutorial”和“optimizing”統計relevance value:其中包含“optimizing”的記錄增加relevance value */
mysql> SELECT `pa`.`FTS_DOC_ID`, `pa`.`title`, `pa`.`body`,
    -> MATCH (`pa`.`title`, `pa`.`body`) AGAINST ('tutorial' IN BOOLEAN MODE) AS `score`
    -> FROM `player`.`articles` AS `pa`
    -> ORDER BY `score` DESC
    -> LIMIT 2;
+------------+------------------+-----------------------------------+---------------------+
| FTS_DOC_ID | title            | body                              | score               |
+------------+------------------+-----------------------------------+---------------------+
|          1 | MySQL Tutorial   | DBMS stands for DataBase ...      | 0.22764469683170319 |
|          3 | Optimizing MySQL | In this tutorial we will show ... | 0.22764469683170319 |
+------------+------------------+-----------------------------------+---------------------+
2 rows in set (0.00 sec)

mysql> SELECT `pa`.`FTS_DOC_ID`, `pa`.`title`, `pa`.`body`,
    -> MATCH (`pa`.`title`, `pa`.`body`) AGAINST ('tutorial >optimizing' IN BOOLEAN MODE) AS `score`
    -> FROM `player`.`articles` AS `pa`
    -> ORDER BY `score` DESC
    -> LIMIT 2;
+------------+------------------+-----------------------------------+---------------------+
| FTS_DOC_ID | title            | body                              | score               |
+------------+------------------+-----------------------------------+---------------------+
|          3 | Optimizing MySQL | In this tutorial we will show ... |  1.8331639766693115 |
|          1 | MySQL Tutorial   | DBMS stands for DataBase ...      | 0.22764469683170319 |
+------------+------------------+-----------------------------------+---------------------+
2 rows in set (0.00 sec)

mysql> /* 查詢包含“tutorial”的記錄:其中包含“optimizing”的記錄relevance value置為負值 */
mysql> SELECT `pa`.`FTS_DOC_ID`, `pa`.`title`, `pa`.`body`,
    -> MATCH (`pa`.`title`, `pa`.`body`) AGAINST ('tutorial ~optimizing' IN BOOLEAN MODE) AS `score`
    -> FROM `player`.`articles` AS `pa`
    -> ORDER BY `score` DESC
    -> LIMIT 2;
+------------+------------------+-----------------------------------+---------------------+
| FTS_DOC_ID | title            | body                              | score               |
+------------+------------------+-----------------------------------+---------------------+
|          1 | MySQL Tutorial   | DBMS stands for DataBase ...      | 0.22764469683170319 |
|          3 | Optimizing MySQL | In this tutorial we will show ... | -0.1668359637260437 |
+------------+------------------+-----------------------------------+---------------------+
2 rows in set (0.00 sec)

mysql> /* 查詢包含以字母“c”開頭的單詞的記錄 */
mysql> SELECT * FROM `player`.`articles`
    -> WHERE MATCH (`title`, `body`) AGAINST ('c*' IN BOOLEAN MODE);
+------------+-------------------+------------------------------------------+
| FTS_DOC_ID | title             | body                                     |
+------------+-------------------+------------------------------------------+
|          5 | MySQL vs. YourSQL | In the following database comparison ... |
|          6 | MySQL Security    | When configured properly, MySQL ...      |
+------------+-------------------+------------------------------------------+
2 rows in set (0.00 sec)

mysql> /* 查詢包含“mysql”或者“tutorial”的記錄 */
mysql> SELECT * FROM `player`.`articles`
    -> WHERE MATCH (`title`, `body`) AGAINST ('mysql tutorial' IN BOOLEAN MODE);
+------------+-----------------------+------------------------------------------+
| FTS_DOC_ID | title                 | body                                     |
+------------+-----------------------+------------------------------------------+
|          1 | MySQL Tutorial        | DBMS stands for DataBase ...             |
|          3 | Optimizing MySQL      | In this tutorial we will show ...        |
|          6 | MySQL Security        | When configured properly, MySQL ...      |
|          2 | How To Use MySQL Well | After you went through a ...             |
|          4 | 1001 MySQL Tricks     | 1. Never run mysqld as root. 2. ...      |
|          5 | MySQL vs. YourSQL     | In the following database comparison ... |
+------------+-----------------------+------------------------------------------+
6 rows in set (0.00 sec)

mysql> /* 查詢包含短語“mysql tutorial”的記錄 */
mysql> SELECT * FROM `player`.`articles`
    -> WHERE MATCH (`title`, `body`) AGAINST ('"mysql tutorial"' IN BOOLEAN MODE);
+------------+----------------+------------------------------+
| FTS_DOC_ID | title          | body                         |
+------------+----------------+------------------------------+
|          1 | MySQL Tutorial | DBMS stands for DataBase ... |
+------------+----------------+------------------------------+
1 row in set (0.00 sec)

③(NATURAL LANGUAGE MODE)WITH QUERY EXPANSION

全文檢索擴充套件查詢。當查詢的關鍵詞太短,需要隱含資訊(implied knowledge)時比較有用。例如,當查詢“database”時可能意味著“MySQL”、“Oracle”、“DB2”以及“RDBMS”等詞語都應該匹配和返回。在查詢語句中新增WITH QUERY EXPANSION或者IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION啟用“blind query expansion”(也稱“automatic relevance feedback”)。擴充套件查詢分兩步執行。

  • 根據原始的查詢關鍵字表達式進行全文檢索
  • 根據第一步查詢的返回結果分詞後再進行一次全文檢索

因此,如果有記錄包含“database”和“MySQL”,第二步查詢也會找出那些包含“MySQL”的記錄,即便這些記錄沒有包含“database”。

mysql> SELECT * FROM `player`.`articles`
    -> WHERE MATCH (`title`, `body`)
    -> AGAINST ('database' IN NATURAL LANGUAGE MODE);
+------------+-------------------+------------------------------------------+
| FTS_DOC_ID | title             | body                                     |
+------------+-------------------+------------------------------------------+
|          1 | MySQL Tutorial    | DBMS stands for DataBase ...             |
|          5 | MySQL vs. YourSQL | In the following database comparison ... |
+------------+-------------------+------------------------------------------+
2 rows in set (0.00 sec)

mysql> SELECT * FROM `player`.`articles`
    -> WHERE MATCH (`title`, `body`)
    -> AGAINST ('database' WITH QUERY EXPANSION);
+------------+-----------------------+------------------------------------------+
| FTS_DOC_ID | title                 | body                                     |
+------------+-----------------------+------------------------------------------+
|          5 | MySQL vs. YourSQL     | In the following database comparison ... |
|          1 | MySQL Tutorial        | DBMS stands for DataBase ...             |
|          3 | Optimizing MySQL      | In this tutorial we will show ...        |
|          6 | MySQL Security        | When configured properly, MySQL ...      |
|          2 | How To Use MySQL Well | After you went through a ...             |
|          4 | 1001 MySQL Tricks     | 1. Never run mysqld as root. 2. ...      |
+------------+-----------------------+------------------------------------------+
6 rows in set (0.00 sec)

blind query expansion通常會返回不相關的記錄而極大地增加查詢結果的“噪音”(noise),因此僅當查詢語句表示式簡短的情況下使用。

在CJK語系中,為了解決word與word之間不使用分隔符而原始無法分詞的問題,MySQL引入ngram全文解析器(ngram full-text parser)。ngram的核心思想是使用者自定義分詞長度。一個ngram指的是給定文字序列中n個字元的連續序列。ngram解析器將文字序列分詞成n個字元的連續序列。例如,通過ngram full-text parser可以以不同的n值將“abcd”分詞。

  • n=1: 'a', 'b', 'c', 'd'
  • n=2: 'ab', 'bc', 'cd'
  • n=3: 'abc', 'bcd'
  • n=4: 'abcd'

ngram parser預設的分詞大小(token size)是2(bigram)。在此預設值下,字串“abc def”會被解析成四個分詞:“ab”、“bc”、“de”和“ef”。該值大小由引數“ngram_token_size”控制,值域為1 ~ 10。通常將其設定為你想要查詢的token的最大size。如果只想要搜尋單字元,將其設為1。越小的token size生成越小的full-text search索引和更快的查詢效能。如果需要搜尋由多於一個的字元組成的word,以此相應地設定“ngram_token_size”。例如,“Happy Birthday”在簡體中文裡是“生日快樂”,其中“生日”是“birthday”、“快樂”譯作“happy”。要搜尋類似的雙字元word,設定“ngram_token_size”的值為2或者更大。該引數為只讀變數,只能在啟動行或者配置檔案裡面設定。

ngram parser在解析時會消除空格。

  • “ab cd”解析為:“ab”、“cd”
  • “a bc”解析為:“bc”

在以空格作為分隔符來分詞的第一語系,stopword list是相同剔除(word與stopword相同時不會FULLTEXT索引);CJK語系則與此不同,stopword list為包含剔除。例如,假設ngram_token_size=2,一個包含“a,b”的記錄會被解析為“a,”和“,b”,如果逗號(“,”)被定義為stopword,那麼“a,”和“,b”都會被FULLTEXT索引排除,因為它們包含逗號。預設情況下,ngram parser使用MySQL預設的stopword list,它包含一列英文stopword。對於適用於CJK語系的stopword list,則必須自己建立(通過系統引數“innodb_ft_server_stopword_table”指定)。寬度超過“ngram_token_size”的stopword會被忽略。

要建立啟用ngram parser的FULLTEXT index,在建表的DDL中指定“WITH PARSER ngram”。

mysql> CREATE TABLE `player`.`articles_ngram` (
    -> `FTS_DOC_ID` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
    -> `title` VARCHAR(200) NULL DEFAULT NULL,
    -> `body` TEXT NULL,
    -> PRIMARY KEY (`FTS_DOC_ID`),
    -> FULLTEXT INDEX `title` (`title`, `body`) WITH PARSER ngram
    -> )
    -> ENGINE=InnoDB CHARACTER SET utf8mb4
    -> ;
Query OK, 0 rows affected (0.11 sec)

mysql> SET NAMES utf8mb4;
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO `player`.`articles_ngram` (`title`, `body`) VALUES
    -> ('中華人民共和國', '成立於1949年10月1日'),
    -> ('中華', '這裡是指中華民族'),
    -> ('中國', '擁有五千年曆史的國家'),
    -> ('中華民國', '1912年~1949年的中國大陸;1949年至今的中華臺灣'),
    -> ('民國', '應該是指民國時期吧(1912年~1949年的中國)'),
    -> ('日本國', '太陽が昇るお國'),
    -> ('大和', '日本國家的主要民族'),
    -> ('島國', '就是日本 國家的元首叫天皇'),
    -> ('UK', 'country of the sun say goodbye'),
    -> ('英國', '老牌資本主義國家');
Query OK, 10 rows affected (0.01 sec)
Records: 10  Duplicates: 0  Warnings: 0

mysql> SET GLOBAL innodb_ft_aux_table="player/articles_ngram";
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW GLOBAL VARIABLES LIKE 'ngram_token_size';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| ngram_token_size | 2     |
+------------------+-------+
1 row in set (0.02 sec)

mysql> SELECT * FROM `INFORMATION_SCHEMA`.`INNODB_FT_INDEX_CACHE` AS `ii` WHERE `ii`.`DOC_ID` = 1 ORDER BY `ii`.`POSITION`;
+--------+--------------+-------------+-----------+--------+----------+
| WORD   | FIRST_DOC_ID | LAST_DOC_ID | DOC_COUNT | DOC_ID | POSITION |
+--------+--------------+-------------+-----------+--------+----------+
| 中華   |            1 |           4 |         3 |      1 |        0 |
| 華人   |            1 |           1 |         1 |      1 |        3 |
| 人民   |            1 |           1 |         1 |      1 |        6 |
| 民共   |            1 |           1 |         1 |      1 |        9 |
| 共和   |            1 |           1 |         1 |      1 |       12 |
| 和國   |            1 |           1 |         1 |      1 |       15 |
| 成立   |            1 |           1 |         1 |      1 |       22 |
| 立於   |            1 |           1 |         1 |      1 |       25 |
| 於1    |            1 |           1 |         1 |      1 |       28 |
| 19     |            1 |           5 |         3 |      1 |       31 |
| 94     |            1 |           5 |         3 |      1 |       32 |
| 49     |            1 |           5 |         3 |      1 |       33 |
| 9年    |            1 |           5 |         3 |      1 |       34 |
| 年1    |            1 |           1 |         1 |      1 |       35 |
| 10     |            1 |           1 |         1 |      1 |       38 |
| 0月    |            1 |           1 |         1 |      1 |       39 |
| 月1    |            1 |           1 |         1 |      1 |       40 |
| 1日    |            1 |           1 |         1 |      1 |       43 |
+--------+--------------+-------------+-----------+--------+----------+
18 rows in set (0.01 sec)

全文檢索的SQL查詢語法對於ngram解析器外掛同樣適用,但解析方式存在一些不同之處。

①ngram Parser Term Search

  • 在NATURAL LANGUAGE MODE檢索模式下,查詢關鍵字表達式被轉換為若干個ngram詞語的聯合。例如,字串“abc”(假設ngram_token_size=2)會被轉換為“ab bc”。給出兩行記錄,一行包含“ab”而另一行包含“abc”,這兩行記錄都匹配檢索詞語“ab bc”。即匹配其一即可返回。
mysql> SELECT * FROM `player`.`articles_ngram`
    -> WHERE MATCH (`title`, `body`)
    -> AGAINST ('中華人民' IN NATURAL LANGUAGE MODE);
+------------+-----------------------+---------------------------------------------------------------+
| FTS_DOC_ID | title                 | body                                                          |
+------------+-----------------------+---------------------------------------------------------------+
|          1 | 中華人民共和國        | 成立於1949年10月1日                                           |
|          2 | 中華                  | 這裡是指中華民族                                              |
|          4 | 中華民國              | 1912年~1949年的中國大陸;1949年至今的中華臺灣                 |
+------------+-----------------------+---------------------------------------------------------------+
3 rows in set (0.00 sec)
  • 在BOOLEAN MODE檢索模式下,查詢關鍵字表達式被轉換為一個ngram短語檢索。例如,字串“abc”(假設ngram_token_size=2)會被轉換為'"ab bc"'。給出兩行記錄,一行包含“ab”而另一行包含“abc”,只有包含“abc”的記錄行匹配檢索短語'"ab bc"'。即完全匹配才可返回。
mysql> SELECT * FROM `player`.`articles_ngram`
    -> WHERE MATCH (`title`, `body`)
    -> AGAINST ('中華人民' IN BOOLEAN MODE);
+------------+-----------------------+---------------------------+
| FTS_DOC_ID | title                 | body                      |
+------------+-----------------------+---------------------------+
|          1 | 中華人民共和國        | 成立於1949年10月1日       |
+------------+-----------------------+---------------------------+
1 row in set (0.00 sec)

②ngram Parser Wildcard Search

由於ngram FULLTEXT index僅包含ngram分詞,而不包含詞語的詞首資訊,所以萬用字元檢索可能返回預想之外的結果。

  • 如果萬用字元檢索的字首詞語小於“ngram_token_size”,查詢會返回包含以該字首詞語開頭的ngram分詞的所有索引行。例如,假設ngram_token_size=2,對於“a*”的檢索會返回所有以“a”開頭的記錄。
mysql> SELECT * FROM `player`.`articles_ngram`
    -> WHERE MATCH (`title`, `body`)
    -> AGAINST ('中*' IN BOOLEAN MODE);
+------------+-----------------------+---------------------------------------------------------------+
| FTS_DOC_ID | title                 | body                                                          |
+------------+-----------------------+---------------------------------------------------------------+
|          2 | 中華                  | 這裡是指中華民族                                              |
|          4 | 中華民國              | 1912年~1949年的中國大陸;1949年至今的中華臺灣                 |
|          1 | 中華人民共和國        | 成立於1949年10月1日                                           |
|          3 | 中國                  | 擁有五千年曆史的國家                                          |
|          5 | 民國                  | 應該是指民國時期吧(1912年~1949年的中國)                     |
+------------+-----------------------+---------------------------------------------------------------+
5 rows in set (0.01 sec)
  • 如果萬用字元檢索的字首詞語大於“ngram_token_size”,該字首詞語會被轉換為一個ngram短語,與此同時,萬用字元符號會被忽略。例如,假設ngram_token_size=2,“abc*”萬用字元檢索會被轉換為“ab bc”(“ngram Parser Term Search”的BOOLEAN MODE)。
mysql> SELECT * FROM `player`.`articles_ngram`
    -> WHERE MATCH (`title`, `body`)
    -> AGAINST ('中華人民*' IN BOOLEAN MODE);
+------------+-----------------------+---------------------------+
| FTS_DOC_ID | title                 | body                      |
+------------+-----------------------+---------------------------+
|          1 | 中華人民共和國        | 成立於1949年10月1日       |
+------------+-----------------------+---------------------------+
1 row in set (0.00 sec)

③ngram Parser Phrase Search

短語檢索會被轉換為ngram短語檢索。例如,檢索短語“abc”會被轉換為“ab bc”,包含“abc”和“ab bc”的記錄都會被返回。檢索短語“abc def”會被轉換為“ab bc de ef”,包含“abcdef”的記錄不會被返回。

mysql> SELECT * FROM `player`.`articles_ngram`
    -> WHERE MATCH (`title`, `body`)
    -> AGAINST ('"日本國家"' IN BOOLEAN MODE);
+------------+--------+-----------------------------+
| FTS_DOC_ID | title  | body                        |
+------------+--------+-----------------------------+
|          7 | 大和   | 日本國家的主要民族          |
+------------+--------+-----------------------------+
1 row in set (0.01 sec)

mysql> SELECT * FROM `player`.`articles_ngram`
    -> WHERE MATCH (`title`, `body`)
    -> AGAINST ('"日本 國家"' IN BOOLEAN MODE);
+------------+--------+---------------------------------------+
| FTS_DOC_ID | title  | body                                  |
+------------+--------+---------------------------------------+
|          8 | 島國   | 就是日本 國家的元首叫天皇             |
+------------+--------+---------------------------------------+
1 row in set (0.00 sec)

query expansion擴充套件查詢同樣適用於ngram parser。

mysql> SELECT * FROM `player`.`articles_ngram`
    -> WHERE MATCH (`title`, `body`)
    -> AGAINST ('國家');
+------------+--------+---------------------------------------+
| FTS_DOC_ID | title  | body                                  |
+------------+--------+---------------------------------------+
|          3 | 中國   | 擁有五千年曆史的國家                  |
|          7 | 大和   | 日本國家的主要民族                    |
|          8 | 島國   | 就是日本 國家的元首叫天皇             |
|         10 | 英國   | 老牌資本主義國家                      |
+------------+--------+---------------------------------------+
4 rows in set (0.00 sec)

mysql> SELECT * FROM `player`.`articles_ngram`
    -> WHERE MATCH (`title`, `body`)
    -> AGAINST ('國家' WITH QUERY EXPANSION);
+------------+--------------+---------------------------------------------------------------+
| FTS_DOC_ID | title        | body                                                          |
+------------+--------------+---------------------------------------------------------------+
|          8 | 島國         | 就是日本 國家的元首叫天皇                                     |
|          3 | 中國         | 擁有五千年曆史的國家                                          |
|         10 | 英國         | 老牌資本主義國家                                              |
|          7 | 大和         | 日本國家的主要民族                                            |
|          6 | 日本國       | 太陽が昇るお國                                                |
|          2 | 中華         | 這裡是指中華民族                                              |
|          4 | 中華民國     | 1912年~1949年的中國大陸;1949年至今的中華臺灣                 |
|          5 | 民國         | 應該是指民國時期吧(1912年~1949年的中國)                     |
+------------+--------------+---------------------------------------------------------------+
8 rows in set (0.00 sec)

除了上文第一語系解析時提到的用於InnoDB的“innodb_ft_min_token_size”和“innodb_ft_max_token_size”以及用於MyISAM的“ft_min_word_len”、“ft_max_word_len”(ngram parser會自動忽略其值設定),其他內建的Full-Text Search配置引數同樣適用於ngram parser。

MySQL同時提供了一個專用於日語的MeCab full-text parser,將記錄行tokenize為有實際意義的詞語。

參考: