由於 LINE 大幅調漲其官方帳號推送訊息的價格,許多具有重要影響力的團體及個人逐漸從 LINE 轉移至 Telegram,也讓這個在台灣原屬小眾的通訊軟體浮出水面,進入大眾的視野。
然而,Telegram 對於中文的支持並不甚良好,尤其搜尋功能遇上漢字即與殘廢無異。所以,原因究竟是什麼呢??讓我們來逐步抽絲剝繭吧
首先,我們看看 Telegram 核心 td-lib 是如何儲存訊息的。這是 MessagesDb.cpp,從 12 到 14 行我們可以知道 Telegram 使用 SQLite 作為資料庫:
#include "td/db/SqliteConnectionSafe.h"
#include "td/db/SqliteDb.h"
#include "td/db/SqliteStatement.h"
接著,我們可以發現這個 database 中有不少 table,但是我們只關心 messages_fts 這個 virtual table。那麼,這個 fts 又是什麼呢??
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(text, content='messages', content_rowid='search_id', tokenize = "unicode61 remove_diacritics 0 tokenchars '\a'")
原來,fts 其實是 full-text search,也就是所謂的全文搜索,而 fts5 則是 SQLite 內建可以提供全文搜索的模組。
說到這裡,先來打個岔談談字串搜尋。在高中演算法競賽中,除了樸素的暴力解外,最常見而廣為人知的方法有兩種:KMP 演算法與其他基於 hash 的演算法。前者是由資訊界史詩級教授 Donald E. Knuth 及 Pratt, Morris 於 1977 提出。
而 SQLite fts5 比較類似基於 hash 的演算法,他的做法大約如下:
- 將字串以 tokenizer 切割成一段段的 phrase
- 將 phrase 進行 hash 後存進 table
- 搜尋時也將搜尋字串以 tokenizer 切割成一段段的 phrase,在 hash table 中進行搜尋
舉例而言,訊息 qwert qaz qsc
在搜尋時只會分別匹配到 qwert
、qaz
、 qsc
這三個字串,你搜尋單獨的 q
、a
、er
都不會匹配到。
那問題接著來了:tokenizer 根據什麼切割??用以切割 phase 的字元,我們稱之 separator or delimiter,而剩下構成 phase 的字元稱之 token。進一步來說,依據 unicode 的類別,屬於 L*(letter)、N*(number) 及 Co(other) 預設為 token,其餘都被視為 separator。
所以我們終於找到 Telegram 不能搜尋中文訊息的根本原因了。原來,由於 unicode CJK (中日韓表意文字)絕大部分皆屬於 Lo 類別,也就是說,漢字語漢字間沒有任何 separator,會被整串拿去 hash,從而導致搜尋無效。
結語
不小心就打了一堆廢話呢,相信大家都知道 Telegram 不能搜尋中文訊息的原因了。解決的方法我有想到兩種,其一是手動在漢字間插入不可見的 separator,另一種是寫個客製化的 tokenizer 。
於此將我的一點淺見拿來分享作為拋磚引玉,希望各位電神不吝指教。未來倘若有空我也十分希望能為 Telegram 社群做出貢獻。