在PHP+MySQL構架的網站中,大數據量的全文檢索一般都會用到MySQL的FULLTEXT全文索引,通過SELECT...MATCH...AGAINST語句來進行查找。
迄今為止,MySQL對中文全文索引無法正確支持,MySQL是不會識別中文詞語的。參照MySQL識別英文單詞機制,要建立中文全文索引,暫時的解決方案只有手動將中文分詞(以空格的形式將中文詞語分開),來將中文轉換成MySQL認識的語言。
如今網上對於中文分詞的解決方案有很多,有基於MySQL插件的,有談論算法思想的。基於插件(如海量科技的MySQL--LinuxX86-Chinese+,hightman開發的mysql--ft-hightman)的方式主要通過對MySQL數據庫安裝一個別人提供好的插件,在建FULLTEXT索引的字段時后面加上WITH PARSER ×××(大多都是這樣)的形式。而基於算法思想的則大部分工作都要自己完成,但他們的大體思想都差不多:
1. 對插入的要建全文索引的中文數據進行分詞;
2. 將原始數據和分詞后的數據都存入數據庫中,並以某種方式建立聯系;
3. 在存儲分詞數據的字段上建立FULLTEXT索引;
4. 查詢時以SELECT...MATCH...AGAINST的方式在分詞字段上搜索,將搜到的行通過前面建立的聯系找到原始數據行並返回。
而我們在討論解決方案時,考慮到使用開源插件的話可控性比價差,而且插件會對MySQL做一些改變,我們決定將分詞存儲的工作自己寫代碼完成,這樣雖然工作量加大,但以后的維護成本卻降低了很多。下面我們來看下大體實現。
一、首先,先建立數據庫
要注意的是只有MyISAM表類型才能支持FULLTEXT,MyISAM與InnoDB各有優劣,我們決定將原始數據與索引分表存儲,原始數據存入InnoDB表,同時建立MyISAM表存入作為檢索的字段和用於關聯的字段id。這里我建立了兩張表questions和questions_idx:


其中,title和detail是要建立全文索引的字段,而id則是建立兩張表的聯系。值得注意的是,MyISAM是不支持事務和外鍵的,因此對於兩張表數據的同步還要靠額外代碼邏輯來實現。
還有就是索引字段有可能需要比原始數據更大的空間,這里我分配了2倍(這個是我隨意想的,有可能需要更多)。
二、接着,討論中文分詞
網上流傳的分詞方法有很多,主要有基於算法的(比如二元分詞算法,字節交叉切分算法)和基於詞庫的。基於算法是不必要維護詞庫的,而詞庫法則必須維護詞庫,有可能跟不上詞匯的發展。實際上現在很多著名的搜索引擎都使用了多種分詞的辦法,比如“正向最大匹配”+“逆向最大匹配”,基於統計學的新詞識別,自動維護詞庫等技術。
我們采用的是基於詞庫的,並且使用了hightman的scws的php擴展模塊方式。參考http://www.ftphp.com/scws/ 。這個開源分詞系統這里不多說,總之利用的是詞庫來分詞,而且最新版的是支持自定義詞庫的,這對於我們的內部網站來說,詞庫的維護問題變得簡單了,因為新增詞匯不會像外部網站那么大,也不需要維護太多。
下面定義了類CWS,方法get_idx將輸入中文數據,輸出分詞並編碼后的數據:
class CWS {
//對輸入字符串使用scws進行分詞,去重復項,進行urlencode編碼
public static function get_idx($input) {
//--------分詞-----------
$so = scws_new();
$so->set_charset('utf8');
$so->set_ignore(true);
$output = '';
$so->send_text($input);
while ($tmp = $so->get_result()) {
foreach ($tmp as $item) {
$output .= $item['word'] . ' ';
}
}
$so->close();
//--------編碼-----------
$data = array_filter(explode(" ",$output)); //刪除數組空項
$data = array_flip(array_flip($data)); //刪除重復項
//對分詞結果進行urlcode編碼
foreach ($data as $ss) {
if (strlen($ss) > 1) {
$data_code .= str_replace('%','',urlencode($ss)) . ' ';
}
}
return $data_code;
}
}
對於scws的那段代碼請參照scws使用手冊。
而對於為什么要進行編碼,網上大都解釋是:MySQL系統自變量規定了全文檢索被編入索引單詞的最小長度和最大長度(ft_min_word_len和ft_max_word_len),默認的最小值為4個字符,默認的最大值取決於使用的 MySQL 版本。 參考http://dev.mysql.com/doc/refman/4.1/en/server-system-variables.html#sysvar_ft_min_word_len

為了不改變這個默認值同時也是兼考慮這個值對於英文的意義,則需要通過編碼將中文詞變長。
而對於編碼,網上流傳的方式也有很多,比如base64編碼、urlencode編碼等,甚至還有漢字轉拼音。這里我嘗試了urlencode編碼,需要注意的是urlencode會產生很多’%’,這在MySQL中是通配符,要去掉。
三、插入數據
插入數據的時候我們就要調用以上的函數了:
public function add($title, $detail, $askerid) {
$date = NOW;
$sql = 'INSERT INTO questions ' .
'(title, detail, askerid, date) ' .
'values ' .
"('$title', '$detail', $askerid, '$date')";
$result = $this->db->query($sql);
$id = $this->db->lastId();
//scws分詞存儲
$title_idx = CWS::get_idx($title);
//使用strip_tags函數過濾掉富文本編輯器產生的標簽
$detail_idx = CWS::get_idx(strip_tags($detail));
$sql = 'INSERT INTO questions_idx ' .
'(id, title, detail) ' .
'values ' .
"($id, '$title_idx', '$detail_idx')";
$this->db->query($sql);
return $result;
}
四、搜索數據
搜索的時候,從輸入框獲取問題,當然如果你輸入的是關鍵詞,那就直接搜就是了,但我們是要用戶輸入的一個完整的問題,因此也要分詞,否則MySQL還是檢索不到:
public function search($word, $limit) {
$word = CWS::get_idx($word);
$sql = "SELECT A.title, A.detail, askerid, date " .
"FROM questions as A, questions_idx as B " .
"WHERE A.id = B.id " .
"AND MATCH (B.title, B.detail) AGAINST ('$word')";
$result = $this->db->getAll($sql, $limit);
return $result;
}
相關資料:
