在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; }
相关资料: