使用Mroonga全文搜尋引擎替代MySQL的LIKE模糊查詢

在上一篇文章 《關係型 SQL 的全文搜尋 FullText 方案先行探究分析》 中我簡單的介紹了一下現行常見的几種全文搜尋的方案。在其中我提到過這個由日本人開發的 Mroonga 全文搜尋引擎,由於此引擎在網路中鮮有資料能詢,索性我自己來寫一篇關於此引擎的用法,同時也會向大家説明此引擎在使用過程中如何避免踩坑。


Mroonga 的原理

Mroonga 實際上是一個 MySQL 系的關係型 SQL 中的外挂,但又與一般的 Plugin 有所區別。Mroonga 實際上在 MySQL 中會被當作一個存儲引擎所被 SQL 辨識。然而實際上 Mroonga 其實只是一個中間件,他的核心帶了一個完整的 Groonga。與 SphinxSearch 的 MySQL 外挂 SphinxSE 作爲一個中間件的原理類似,但又與 SphinxSE 不同的是 Mroonga 中已經自帶了 Groonga,所以並不需要像使用了 SphinxSE 后還需安裝一個 SphinxSearch 本體程式。不管是 SphinxSE 還是 Mroonga,兩者實現原理基本是一致的。

雖説 Mroonga 也算是 SphinxSearch 師門中的一個弟子,但在實際使用過程中就能發現,Mroonga 算是洗心革面重新做了人,與祖師 SphinxSearch 徹底不同的是,使用的作業指令與 MySQL 保持了最大的相容,基本可以説是 MySQL 的 FullText 全文搜尋指令如何寫,Mroonga 就如何寫,格式可以説是一模一樣。反觀 SphinxSearch,正宗俄國血統,作業指令基本就突出了一個暴力美學,與正常的 SQL 指令相差非常大。


Mroonga 的使用限制

Mroonga 的使用限制其實是源自 Groonga 的内部限制。

表(table)的限制:

  • 單個 Key(PRIMARY KEY 或 UNIQUE KEY)的大小最大為 4KiB,也就是 4KB。
  • 單個索引中所有 Key(PRIMARY KEY 或 UNIQUE KEY)的大小最大為 4GiB,也就是 4GB。
  • 單表最大的記錄數(Maximum Records):

    1. 無主鍵的表(Not have PRIMARY KEY)最大記錄條數為 1073741815 條。差不多 10 億。
    2. 有主鍵(PRIMARY KEY)或者使用了 B 樹(B-TREE)的主鍵(PRIMARY KEY USING BTREE)的最大記錄條數為 1073741823 條。差不多也是 10 億。
    3. 使用了雜湊表(HASH)的主鍵(PRIMARY KEY USING HASH)的最大記錄條數為 536870912 條。差不多是 5 億。

另外,如果表中的主鍵的值(Value of PRIMARY KEY)為非數字的情況下,例如是中文,那表將會無法變更為 Mroonga。

表中的列(Column)的限制:

  • 單個列(Column)的數據存儲大小最大為 256GiB,也就是 256GB。

全文索引(FULLTEXT INDEX)的限制:

  • 單個全文索引中不重複條目的情況下最大可索引 268435455 條記錄,約 2.68 億。
  • 單個全文索引的大小最大為 256GiB,也就是 256GB。

列(Column)中的值(Value)的限制:

列(Column)中的值(Value)不能為NULL。如果你設置了NULL將會根據類型被自動轉換爲相應的值。擧個例子就是,比如你的值是DATE或者DATETIMENULL將會被轉換成 0 或者1970-01-01 00:00:00。字符串類型(String)的值將會被轉換成空字符(empty)。其實與NULL沒區別但就是不能是NULL的意思。此限制根據官方的説明說的是只會影響存儲模式(Storage Mode),但實際上也同樣會影響中間件模式(Wrapper Mode)的索引内容。這限制直接導致了以下類似的 SQL 指令無法使用:

  • ... VALUES (NULL)
  • ... WHERE xx = NULL

無法使用的 SQL 指令:

  • CREATE TABLE (...) CHARSET not_listed_charset_above
  • INSERT INTO (geometry_column_name) VALUES (GeomFromText('LineString(...)'))
  • INSERT INTO (...) VALUES (NULL)
  • START TRANSACTION

僅支援以下類型的字符集(Character)

  • ASCII
  • BINARY
  • CP932
  • EUCJPMS
  • KOI8R
  • LATIN1
  • SJIS
  • UJIS
  • UTF8
  • UTF8MB4

字符集(Character)的使用是可以支援該字符集的排序規則(Collations)的。例如utf8_general_ci等。


Mroonga 的工作模式選擇

Mroonga 有兩種工作模式,一種是存儲模式(Storage Mode),另一種是中間件模式(或者稱為包裝器模式,Wrapper Mode)。這兩種模式的作業原理不一樣,下面將簡單説明一下兩種模式的原理與有什麽優缺點。


  • 存儲模式 Storage Mode

storage-mode.png

在此模式下,Mroonga 將作爲一種存儲引擎與 MyISAM 或 InnoDB 一樣,直接提供存儲功能與 SQL 進行交互。在 MySQL 中我們直接查看的話,表的存儲引擎都會展示為 Mroonga。但由於 Mroonga 實際上只是個中間件,實際上數據都會存儲在 Groonga 中。所以我們可以簡單的理解為,在此模式下,所有的數據并不存放在 SQL 中,而是直接存儲在 Groonga 中。

存儲模式的優點:

存儲模式帶來的優點是顯而易見的,一是更高的搜尋速度與更低的性能開銷。這裏我拿 Mroonga 與 MyISAM 進行對比。衆所周知,MyISAM 在執行搜尋作業(SELECT)之時,會對所涉及的表自動加上讀取鎖(Read Lock),同時在更改數據之時(UPDATE/DELETE/INSERT 等),又會對所涉及的表自動加上寫入鎖(Write Lock)。但 Mroonga 在執行搜尋作業的時候并不會對表進行加鎖作業,所以搜尋效能會比 MyISAM 所使用的性能開銷更小。同時在數據更新之後 Groonga 内部能做到即時更新索引,且開銷會比中間件模式更低。二是在存儲模式下,數據只會存儲一份。這意味著占用硬碟的空間會比中間件模式更小。這個優點其實說的有點牽强,但我們可以來看看現在企業級固態硬碟 SSD 的價格與容量,在荷包不是特別充裕的情況下,這點就有點重要了。

存儲模式的缺點:

但存儲模式并不是全是優點,相反此模式下的缺點也非常突出。首先,存儲模式下的 Mroonga 存儲引擎不支援事務(Transaction)。這點上來説,如果你之前使用的是 MyISAM 引擎準備遷移至 Mroonga 的話,這是沒問題的。但如果你使用的是 InnoDB,那就千萬別直接遷移至 Mroonga 的存儲模式。

缺點二,在不使用SSD固態硬碟的情況下,同時表内數據量較大之時,除了全文搜尋Full Text Search以外,一定時間内的首次查詢速度都會比MyISAM慢。因爲 Groonga 使用的是列存儲(Column Based Storage)而不是行存儲(Row Based Storage),Mroonga 需要從 Groonga 中提取數據經過轉換后才會交給 SQL,這個轉換過程很明顯是需要時間和性能開銷的。然而如果只是取一行數據的話,那這個轉換過程的時間與性能的開銷可以忽略不計。但如果一次取個几千條幾萬條數據呢?例如這句LIMIT 9900, 100我要取這 10000 條數據中的最后 100 條且我要求是這行的所有數據都取出來並不使用全文搜尋的方式而使用一般的 SELECT,那 Mroonga 就需要同時定位所有的列到此位置並同時取出進行轉換,同時 Groonga 的檔案存儲方式使用了分散式存儲,按照列分散的存儲在了不同的文件中,這個讀取數據過程將會占用非常高的 IO 資源。如果這時使用的是 HDD 機械硬碟,將直接導致查詢速度肉眼可見的龜速。但這個問題其實也是可以解決的,優化的辦法就是使用 SSD 固態硬碟。因爲隨便一個正常的 SSD 固態硬碟,讀取的 IOPS 都是幾萬以上。而 HDD 機械硬碟的讀取 IOPS 都是 70~100 IOPS,就算是 10000RPM 的企業級 SAS HDD,其讀取的 IOPS 也不會超過 230 IOPS。但以至於爲何是一定時間内的首次查詢速度都會比 MyISAM 慢呢而不是每次都慢的原因是有快取(Cache)的存在。當然如果快取過期了下次查詢的速度又會慢了。但爲何全文搜尋 Full Text Search 的速度就沒龜速呢,直接原因就是全文搜尋是在 Groonga 建立的 FullText Index 索引中進行特徵碼搜尋,之後就能直接快速的提出所需要的數據。

缺點三,這個問題其實是個玄學問題。Mroonga的存儲模式穩定性存在不確定因素。這個其實我并沒有直接的測試數據與證據能直接證明 Mroonga 作爲存儲引擎的可靠性不行,但由於這種直接替代的存儲引擎的作業方式是否能保證數據的安定?這個可能還需要時間的檢驗。

綜上所述,如果要使用存儲模式進行作業,我能給出的建議是:

  • 使用可靠性和性能都高的 SSD 固態硬碟,同時記憶體最好大於或者等於 16GB,CPU 性能最好不要太弱(比如 Celeron 或者 Atom 就算了吧)。
  • 在使用 SELECT 進行查詢作業的時候,做到需要什麽值取什麽值,例如SELECT value1, value2,不要直接SELECT *全部取了。
  • 如果是從老數據庫遷移過來的話,最好是 MyISAM 遷移至 Mroonga。
  • 如果可以建議將整個庫的所有表都變更為 Mroonga 存儲引擎,避免出現JOIN了不是 Mroonga 的表影響性能。
  • 備份作業一定要做好。

很明顯,此模式比較適合不想做太多改動就能用上全文搜尋引擎和主要是以搜尋為主要服務的項目。唯獨不適用于需要使用 InnoDB 特性的項目,例如 EC 電子商務平臺等對支援事務作業的項目。


  • 中間件模式(包裝器模式,Wrapper Mode)

wrapper-mode.png

在此模式下,Mroonga 將作爲 SQL 處理模組(SQL Handler)與存儲引擎之間的一個中間件。Mroonga 只會處理 FullText 的全文搜尋 SQL 指令。按照官方給出的説明圖中我們可以看出 Mroonga 作爲一個存儲引擎展現給 SQL 處理模組,但實際數據將會全部交給其他存儲引擎,同時將所有除 FullText 全文搜尋指令以外的指令都交由其存儲引擎來進行處理。實際上我們可以這麽理解,Mroonga 的中間件模式下,對 MySQL 而言,MySQL 依舊是將 Mroonga 認作一個存儲引擎,實際上 Mroonga 内部包了一層其他存儲引擎做數據的存儲。

但官方未説明的是,Groonga 此時又做了什麽呢。根據我翻看其原始程式碼的結果來看,實際上此實現并非想象中的複雜,Mroonga 作爲一個中間件,除了 SELECT 指令以外的 SQL 指令,都將會同時發給其他存儲引擎與 Groonga。意思就是 Groonga 内依舊留有一份數據。但由於 Mroonga 只在處理 FullText 全文搜尋指令之時,才會只向 Groonga 發送搜尋指令。其他的 SELECT 指令都會只交給其他存儲引擎處理。

但由於 Groonga 并不支援事務(Transaction)作業,如果使用的是 InnoDB 的特性或者一些特有機能的話,例如事務作業,Mroonga 將會在事務作業結束後根據結果向 Groonga 同步一次存儲引擎内的數據並更新全文搜尋索引。然而實際上并非所有的 InnoDB 事務作業指令都能成功轉換為非事務作業,所以 Mroonga 會定期向 Groonga 發送一次同步存儲引擎内的數據並重建索引的作業指令。

中間件模式的優點:

很明顯,中間件模式下能支援其他存儲引擎的特性了。例如 InnoDB 的事務作業等等。實際上這個模式下的原理與我們使用其他獨立的全文搜尋引擎的原理是一樣的。例如 SphinxSearch 的直接連接 SQL 模式而非通過 SphinxSE 插件連接,在全文搜尋引擎内部與 SQL 中都存有一份數據來做各自的處理。這樣就能實現互不干擾互相獨立作業。

中間件模式的缺點:

中間件模式也非常明顯,通過上面的原理分析我們能很輕鬆的看出在數據更改作業下,寫入的性能開銷會非常大。事實上也是如此。畢竟要同時向兩邊寫入數據更新,如是使用了一些 Groonga 本身并不能支援的特性的情況下,例如事務作業,Mroonga 只能采取全量數據更新並重建索引的作業。當然,如果在表中數據量並不多的情況下,這個處理資源占用并不會非常明顯。但如果是一個幾十萬起步的表,在執行以上操作之時將會占用大量的 CPU、IO 與記憶體資源。

缺點二,在使用了一些Groonga本身并不能支援的特性的情況下,更改寫入數據之後在一定時間内會出現存儲引擎内的數據内容與Groonga中的數據内容與全文搜尋索引不一致的情況。造成這點的原因也可以從上面說的原理中看出,重新更新數據與重建索引是需要一定處理時間的,在處理完成之前數據肯定是會出現不同步的情況。

缺點三,這問題其實還是個玄學問題。還是這個中間件模式的可靠性存疑。雖然 Mroonga 在中間件模式下的實現原理與 SphinxSearch 的普通索引模式一致,但不代表 Mroonga 就與 SphinxSearch 一樣并不會影響到 MySQL。例如 SphinxSearch 在自身進程崩潰的情況下,他并不會影響到 MySQL 的作業。但 Mroonga 并不是這樣。Mroonga 就算是使用中間件模式,SQL 處理模組依舊是將此表認定為 Mroonga 存儲引擎並將所有的指令都交給 Mroonga 來進行處理。而就算使用了其他存儲引擎進行存儲,也都是被包在 Mroonga 中。所以能很明顯看出一點就是,如果 Mroonga 的中間件出現了些什麽問題,最壞的情況也會導致數據損毀。

綜上所述,如果要使用中間件模式進行作業,我能給出的建議是:

  • 使用可靠性和性能都高的 SSD 固態硬碟,同時記憶體最好大於或者等於 16GB,CPU 性能最好不要太弱(比如 Celeron 或者 Atom 就算了吧)。
  • 建議將所有可能涉及到 FullText 全文搜尋作業的表都轉變為此模式以免造成JOIN了不是 Mroonga 的表影響性能。
  • 備份作業一定一定一定要做好。

此模式很明顯,如果你不是使用什麽 InnoDB 的特性的話,例如事務作業等等,我并不推薦使用此模式。我們對數據庫的要求最低底綫是保證數據安全。但很明顯此模式下會造成數據損毀的隱患并不小。這個道理其實很簡單,結構越複雜的東西出錯的可能性越高。


Mroonga 的安裝

Mroonga 的安裝我大致給他分成兩種,一種是獨立安裝 Mroonga,另一種是直接使用高版本 MariaDB 自帶的 Mroonga。

獨立安裝 Mroonga

獨立安裝 Mroonga 的話可以參考他們的官方文檔 《如何安裝 Mroonga》,因爲針對系統與使用的 SQL 不用,都有不一樣的安裝方法,這裏就不細講了。畢竟以後如果安裝方式有任何更新,官方文檔肯定更新的最快,所以我在這邊細説如何安裝也沒有任何意義。這種方式適合使用 MySQL、Percona 或者是沒有自帶 Mroonga 的 MariaDB 版本或者想在已經自帶了 Mroonga 的 MariaDB 上安裝新版本的 Mroonga。

使用自帶 Mroonga 的 MariaDB

這種方式其實對於像我這種大部分都在用 MariaDB 的人來説,是最簡單的方式。雖然簡單,但 MariaDB 自帶的 Mroonga 版本并不是最新的,且并不是所有的 MariaDB 版本都自帶了 Mroonga。

如果要使用自帶 Mroonga 的 MariaDB,需要安裝以下版本號或以上的 MariaDB:

MroongaWithMariaDB.png

在上圖所示的 MariaDB 版本或者更新的版本中,我們只需使用 MySQL Client 連接至 MariaDB 的命令行控制臺中使用以下命令就能啓用:

mysql> INSTALL SONAME 'ha_mroonga';

但如果你現在所安裝的 MariaDB 版本中并無自帶 Mroonga 并且同時使用的是 Ubuntu 或者 Debian 的系統,且你的 MariaDB 是通過 APT 包管理器安裝的,你可以使用以下命令安裝 Mroonga 的 MariaDB 外挂:

sudo apt-get install mariadb-plugin-mroonga

除此之外,請使用獨立安裝 Mroonga 的方式進行安裝。


安裝完畢后,我們可以在 MySQL 命令行控制臺中使用以下命令查看 Mroonga 引擎的情況:

mysql> SHOW ENGINES;

正常的話此命令的展示結果類似下面這樣:

...
*************************** 8. row ***************************
      Engine: Mroonga
     Support: YES
     Comment: CJK-ready fulltext search, column store
Transactions: NO
          XA: NO
  Savepoints: NO
...

如果你想能獲取到 Groonga 中的最後一條插入的數據的 ID,可以使用以下的命令新增一個用戶自定義函數(UDF, User-Defined Function):

mysql> CREATE FUNCTION last_insert_grn_id RETURNS INTEGER SONAME 'ha_mroonga.so';

這樣就能使用last_insert_grn_id這個函數獲取到 Groonga 中的最後一條插入的數據的 ID。


Mroonga 的使用

正如本文標題所説,我的目的是用 Mroonga 的全文搜尋取代LIKE模糊搜尋機能。看到這裏肯定有人會問,爲什麽拿一個全文搜尋引擎來替代LIKE模糊搜尋機能呢?這個問題的答案其實很簡單,我需要更高的搜尋性能和更低的伺服器性能占用。且我研究了這麽多年的全文搜尋引擎的經驗來看,一般全文搜尋引擎中都要經歷一個斷詞的作業。然而根據我的實際經驗來看,實際上斷詞這個作業只是爲了降低全文搜尋引擎壓力而必須的一個作業而已,實際上還會因爲斷詞誤差導致搜尋結果不準確。我對搜尋的定義其實就是要求他對關鍵詞準確匹配,在 CJK 語言(中國語 Chinese,日本語 Japanese,韓國語 Korean)中實際上準確率最高的搜尋方式就是將一句話例如“我吃飽了”拆成一個個字進行搜尋。在LIKE模糊搜尋下我一般是這麽處理:

SELECT ... WHERE 
           (`field` LIKE '%我%') 
       AND (`field` LIKE '%吃%') 
       AND (`field` LIKE '%飽%') 
       AND (`field` LIKE '%了%') 
  LIMIT 50;

類似如此的處理方式來進行搜尋。當然,肉眼可見在一個大型數據庫中這搜尋速度將會十分龜速,以上這句搜尋指令將會對全表進行最低 4 次的掃描。

以此爲基礎我要如何使用 Mroonga 進行替代操作呢?接下來將會從零開始説明如何一步步從模糊搜尋切換為 Mroonga。


  • 1. 將表的存儲改爲 Mroonga

不管是要用存儲模式還是中間件模式,都得將表的存儲引擎改爲 Mroonga,唯一的區別就只是是否對表的備注説明中加入模式的關鍵詞。

將現有的表更改為存儲模式:

mysql> ALTER TABLE `table` ENGINE = Mroonga;

將現有的表更改為中間件模式:

mysql> ALTER TABLE `table` ENGINE = Mroonga COMMENT = 'engine "MyISAM"';

這裏的 MyISAM 可以選擇 InnoDB。

但如果只是新建一個表,那一切就容易的多了。

新建一個表並使用存儲模式:

mysql> CREATE TABLE `table` ( id INT PRIMARY KEY AUTO_INCREMENT, content VARCHAR(255), FULLTEXT INDEX (content) ) ENGINE = Mroonga DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;

utf8mb4 與 utf8mb4_general_ci 也可按需要選擇。

新建一個表並使用中間件模式:

mysql> CREATE TABLE `table` ( id INT PRIMARY KEY AUTO_INCREMENT, content VARCHAR(255), FULLTEXT INDEX (content) ) ENGINE = Mroonga COMMENT = 'engine "MyISAM"' DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;

從上面的指令可以很簡單的看出,存儲模式與中間件模式的選擇只是COMMENT的内容有無而已。中間件模式需要對COMMENT加上相應的標記COMMENT = 'engine "MyISAM"'


  • 2. 建立全文索引並選擇相應的解析器

由於 Mroonga 的目的是做到最大兼容,所以我們要使用全文搜尋之前必須得為此建立一個全文搜尋索引。同時,Mroonga 的解析器的概念與 MySQL 的 FullText 索引的解析器概念也是一樣的。唯一不同的是,Mroonga 爲了更方便的熱切換解析器,解析器選擇依舊是用了對COMMENT添加描述關鍵詞來進行設置。

Mroonga 的解析器其實有 15 種可選,由於我的目標是替代模糊搜尋,所以我選擇的解析器為TokenBigramSplitSymbolAlphaDigit。由於正常的模糊搜尋是會對空格敏感的,所以要做到 100% 替代的話不能使用TokenBigramIgnoreBlankSplitSymbolAlphaDigit這個解析器。其他的解析器的説明可以參考 Mroonga 官方的説明文檔 《如何為全文搜尋索引指定解析器》

新建全文搜尋索引並指定解析器:

mysql> FULLTEXT INDEX (column_fulltext) COMMENT 'tokenizer "TokenBigramSplitSymbolAlphaDigit"'

當然,這個解析器是可以熱更換的,我們直接修改 COMMENT 的内容即可。

更換全文搜尋索引的解析器:

mysql> ALTER TABLE `DataBase`.`table` ADD FULLTEXT `column_fulltext` (`column`) COMMENT 'tokenizer \"TokenBigramSplitSymbolAlphaDigit\"';

從上面的指令我們還是能看出 Mroonga 的設計理念,爲了最大化兼容 MySQL 的正常 SQL 指令,并沒有使用一些非正常指令。但是這設定解析器的方式是有限制的,限制來源於 MySQL。因爲在 MySQL 5.5 版本開始才支持對索引設置 COMMENT 描述内容。所以如果你使用的是低於這個版本的 MySQL,需要你在my.cnf中設定一個固定的參數mroonga_default_tokenizer=TokenBigramSplitSymbolAlphaDigit來進行指定解析器的操作。然而這樣就無法做到熱設置解析器了。


  • 3. 使用全文搜尋索引進行全文搜尋

到這步,我們就已經可以開始使用 Mroonga 來進行全文搜尋了。Mroonga 的全文搜尋指令與正常的 MySQL FullText 全文搜尋使用的語法結構是一致的。Mroonga 的全文搜尋模式有兩種,一種是普通的布林搜尋模式(BOOLEAN MODE),另一種是自然語言模式(NATURAL LANGUAGE MODE)。簡單來説,就是布林搜尋模式下只有匹配與不匹配,沒有其他結果,類似的使用場景為精準搜尋。而自然語言模式下提供的並不是一個精準搜尋而是提供一個近似内容,類似的使用場景爲搜尋關鍵字補全候選功能等。其實這兩個模式也是 MySQL 的 FullText 所提供的標準模式,所以用法也與 MySQL 沒有區別。


回歸到我的目標上,我要替代模糊搜尋,例如以下這條 LIKE 指令:

SELECT ... WHERE 
                (`field` LIKE '%我%') 
            AND (`field` LIKE '%吃%') 
            AND (`field` LIKE '%飽%') 
            AND (`field` LIKE '%了%') 
       LIMIT 50;

使用 Mroonga 我們需要這麽寫:

SELECT ...USE INDEX(column_fulltext) WHERE 
                   (MATCH(field) AGAINST('*D+ "我" "吃" "飽" "了"' IN BOOLEAN MODE)) 
           LIMIT 50;

同時以下這條指令與上面這條指令的效果一致:

SELECT ...USE INDEX(column_fulltext) WHERE 
                   (MATCH(field) AGAINST('*D+ "我"' IN BOOLEAN MODE)) 
               AND (MATCH(field) AGAINST('*D+ "吃"' IN BOOLEAN MODE)) 
               AND (MATCH(field) AGAINST('*D+ "飽"' IN BOOLEAN MODE)) 
               AND (MATCH(field) AGAINST('*D+ "了"' IN BOOLEAN MODE)) 
          LIMIT 50;

以上是關鍵詞的 AND 搜尋示例。那如果我要使用的是多個關鍵詞中只要出現一個就命中的 OR 模式呢?例如以下這條指令:

SELECT ... WHERE 
      (`field` LIKE '%我%' OR `field` LIKE '%吃%' OR `field` LIKE '%飽%' OR `field` LIKE '%了%') 
LIMIT 50;

使用 Mroonga 我們需要這麽寫:

SELECT ...USE INDEX(column_fulltext) WHERE 
                   (MATCH(field) AGAINST('*D+ "我" OR "吃" OR "飽" OR "了"' IN BOOLEAN MODE)) 
           LIMIT 50;

那如果我要 AND 和 OR 混寫呢?無問題,例如以下這條:

SELECT ... WHERE 
                (`field` LIKE '%我%') 
            AND (`field` LIKE '%吃%') 
            AND (`field` LIKE '%飽%') 
            AND (`field` LIKE '%了%') 
            AND (`field` LIKE '%塞%' OR `field` LIKE '%林%' OR `field` LIKE '%老%' OR `field` LIKE '%目%') 
       LIMIT 50;

使用 Mroonga 我們可以這麽寫:

SELECT ...USE INDEX(column_fulltext) WHERE 
              (MATCH(field) AGAINST('*D+ "塞" OR "林" OR "老" OR "目"' IN BOOLEAN MODE)) 
          AND (MATCH(field) AGAINST('*D+ "我" "吃" "飽" "了"' IN BOOLEAN MODE)) 
     LIMIT 50;

同理,也可以這樣寫:

SELECT ...USE INDEX(column_fulltext) WHERE 
          (MATCH(field) AGAINST('*D+ "塞" OR "林" OR "老" OR "目"' IN BOOLEAN MODE)) 
      AND (MATCH(field) AGAINST('*D+ "我"' IN BOOLEAN MODE)) 
      AND (MATCH(field) AGAINST('*D+ "吃"' IN BOOLEAN MODE)) 
      AND (MATCH(field) AGAINST('*D+ "飽"' IN BOOLEAN MODE)) 
      AND (MATCH(field) AGAINST('*D+ "了"' IN BOOLEAN MODE)) 
 LIMIT 50;

那麽假如我要在搜尋的同時去掉某些關鍵詞呢?例如以下這樣的指令:

SELECT ... WHERE 
          (`field` LIKE '%我%') 
      AND (`field` LIKE '%吃%') 
      AND (`field` LIKE '%飽%') 
      AND (`field` LIKE '%了%') 
      AND (`field` LIKE '%塞%' OR `field` LIKE '%林%' OR `field` LIKE '%老%' OR `field` LIKE '%目%') 
      AND (`field` NOT LIKE '%靠北%') 
 LIMIT 50;

使用 Mroonga 我們可以這麽寫:

SELECT ...USE INDEX(column_fulltext) WHERE 
          (MATCH(field) AGAINST('*D+ "塞" OR "林" OR "老" OR "目" -"靠北"' IN BOOLEAN MODE)) 
      AND (MATCH(field) AGAINST('*D+ "我" "吃" "飽" "了" -"靠北"' IN BOOLEAN MODE)) 
 LIMIT 50;

或者這樣:

SELECT ...USE INDEX(column_fulltext) WHERE 
                   (MATCH(field) AGAINST('*D+ "塞" OR "林" OR "老" OR "目"' IN BOOLEAN MODE)) 
               AND (MATCH(field) AGAINST('*D+ "我"' IN BOOLEAN MODE)) 
               AND (MATCH(field) AGAINST('*D+ "吃"' IN BOOLEAN MODE)) 
               AND (MATCH(field) AGAINST('*D+ "飽"' IN BOOLEAN MODE)) 
               AND (MATCH(field) AGAINST('*D+ "了"' IN BOOLEAN MODE)) 
               AND (MATCH(field) AGAINST('*D+ -"靠北"' IN BOOLEAN MODE)) 
          LIMIT 50;

以上就是使用 Mroonga 進行全文搜尋的一些例子。我們可以總結一下:

  • AND 關鍵詞

使用方式:

   ...AGAINST('*D+ "我" "吃"' IN ... 

或者

   ...AGAINST('*D+ "我"' IN ... AND ...AGAINST('*D+ "吃"' IN ...
  • OR 關鍵詞

使用方式:

   ...AGAINST('*D+ "我" OR "吃"' IN ... 
  • 不需要的關鍵詞

使用方式:

   ...AGAINST('*D+ "我" OR "吃" -"靠北"' IN ... 

可以看到,如果我們希望過濾掉搜尋結果中出現某個關鍵詞的結果,我們可以使用-關鍵詞的方式設置一個過濾。


這麽看下來,可能大家還有最後一個問題,這個*D+是什麽鬼。其實這個*D+的用處是指定默認搜尋模式為 AND 搜尋。如果使用的*D那就是默認模式為 OR 搜尋。

如果不加此模式指定的話,那我們必須在每個關鍵詞前指定這個關鍵是究竟是 AND 還是 OR 還是 NOT。他們的關係是這樣的:

  • '+ 關鍵詞' 代表著這個關鍵詞為 AND,必須存在。
  • '- 關鍵詞' 代表著這個關鍵詞為 NOT,不能出現。
  • '關鍵詞' 什麽都不加的話代表著這個關鍵詞為 OR,有就出現沒有就算了。

同理,如果我們不指定默認運算符的話,將下面這條指令變成 Mroonga 指令:

SELECT ... WHERE 
           (`field` LIKE '%我%') 
       AND (`field` LIKE '%吃%') 
       AND (`field` LIKE '%飽%') 
       AND (`field` LIKE '%了%') 
       AND (`field` LIKE '%塞%' OR `field` LIKE '%林%' OR `field` LIKE '%老%' OR `field` LIKE '%目%') 
       AND (`field` NOT LIKE '%靠北%') 
  LIMIT 50;

變成不設置默認搜尋模式的 Mroonga 搜尋指令的話就應該是這樣:

SELECT ...USE INDEX(column_fulltext) 
             WHERE (MATCH(field) AGAINST('+"我" +"吃" +"飽" +"了" "塞" "林" "老" "目" -"靠北"' IN BOOLEAN MODE)) 
    LIMIT 50;

是不是更簡單了呢?是不是有人想問了爲什麽一開始不告訴大家有這麽一種簡單的寫法?其實原因很簡單,這種簡單的寫法很容易出現定義不清晰導致搜尋出不是想要的結果,而且有很多複雜的搜尋指令是完全無法實現的。所以盡量還是別使用這種簡單的指令來進行搜尋。

最後,其實 Mroonga 還支持正則表達式的搜尋,但前提條件是將解析器改爲TokenRegexp(COMMENT 'tokenizer"TokenRegexp"),示例如下:

SELECT ...USE INDEX(column_fulltext) WHERE 
          MATCH(field) AGAINST ('*SS content @~ "\\\\A/var/log/auth"' IN BOOLEAN MODE);

具體的正則表達式搜尋可參考官方文檔 《如何使用正則表達式進行搜尋》


總結

説實話,我最開始尋找全文搜尋引擎的原因就是需要找一個能花最少力氣修改原始程式碼就能從模糊搜尋遷移的方案。在我測試過幾乎所有的全文搜尋引擎之後,幾乎沒有一款能滿足我的需求。直到我知道了 Mroonga 的存在之後進行了相應的測試,我發現 Mroonga 最合適的用途就是這個,花最少的力氣替代LIKE模糊搜尋。

當然我并不是說其他的全文搜尋引擎不好,只是不合適這個需求。

誠然,我承認 Mroonga 的效能在所有全文搜尋引擎裏并不是最好的,但我要求本來也不高,只要能比 MySQL 原生的模糊搜尋快就行了。事實上 Mroonga 也勝任了這點。

Mroonga力求不過多改變MySQL的使用體驗應該說是他最大的亮點。

所以儅你有類似的需求的時候,不妨來考慮一下 Mroonga。