MySQL5.6.10作为GA版本发布,内置了对Innodb的全文索引支持。5.6.4之前版本的MySQL只有MyISAM支持全文搜索。
MySQL具备全文搜索的能力。全文搜索引擎可以在不使用模板匹配操作的情况下查找单词或短语。全文搜索分为3种模式,如下所示。
自然语言模式。把搜索字符串解释为一系列单词并查找包含这些单词的数据行。
布尔模式。把搜索字符串解释为一系列单词,但允许使用一些操作符字符来"修饰"这些单词以表明特定的要求,如某给定单词必须出现(或不出现)在匹配数据行里,某个数据行必须包含一个精确的短语,等等。
查询扩展模式。这种搜索分两阶段进行。第一阶段是自然语言搜索,第二阶段使用原来的搜索字符串加上在第一次搜索中找到的相关度最高的匹配数据行再进行一次搜索。这扩大了搜索范围,它可以把与原来的搜索字符串相关、但用原来的搜索字符串匹配不到的数据行也找出来。
要想对某个数据表进行全文搜索,必须事先为它创建一个FULLTEXT索引,这种索引具有以下特点。
全文搜索的基础是FULLTEXT索引,这种索引只能在MyISAM数据表里创建。FULLTEXT索引只能由CHAR、VARCHAR和TEXT这几种类型的数据列构成。
全文搜索将忽略"常见的"单词,而"常见"在这里的含义是"至少在一半的数据行里出现过"。千万不要忘记这个特点,尤其是在你准备对数据表进行全文搜索测试时。你至少要在测试数据表里插入3个数据行。如果那个数据表只有一个或两个数据行,它里面的每个单词将至少有50%的出现几率,所以对它进行全文搜索将不会有任何结果。
全文搜索还将忽略一些常用单词,如"the"、"after"和"other"等,这些单词被称为"休止单词",MySQL在进行全文搜索时总是会忽略它们。
太短的单词也将被忽略。在默认的情况下,"太短"指少于4个字符。但你可以通过重新配置服务器的办法把这个最小长度设置为其他值。
全文搜索对"单词"的定义是:由字母、数字、撇号(如"it's"中的"'")和下划线字符构成的字符序列。这意味着字符串"full-blood"将被解释为包含"full"和"blood"两个单词。全文搜索匹配整个单词,而不是单词的一部分。只要在一个数据行里找到了搜索字符串里的任何单词,FULLTEXT引擎就会认为这个数据行与搜索字符串是匹配的。在此基础上,布尔式全文搜索还允许你加上一些额外的要求,比如说,所有的单词都必须出现(不论顺序)才认为是匹配,(在搜索一条短语时)单词顺序必须与在搜索字符串里列出的一致,等等。布尔搜索还可以用来匹配不包含特定单词的数据行,或者通过添加一个通配符来匹配以一个给定前缀开头的所有单词。
FULLTEXT索引可以为一个或多个数据列而创建。如果它涉及多个数据列,基于该索引的搜索将在所有数据列上同时进行。反过来说,在进行全文搜索时,你给出的数据列清单必须和某个FULLTEXT索引所匹配的那些数据列精确匹配。比如说,如果你需要分别搜索col1、col2以及"col1和col2",你将需要创建3个索引:col1和col2各有一个,"col1和col2"有一个。
接下来的例子演示了全文搜索的使用方法。我们将先创建几个FULLTEXT索引,然后用MATCH操作符对它们进行一些查询。用来创建数据表并把一些样板数据加载到其中的脚本可以在sampdb数据库的fulltext子目录里找到。
FULLTEXT索引的具体创建过程与其他索引大同小异。你可以在创建一个新数据表的同时在CREATE TABLE语句里定义它们,也可以在数据表被创建出来以后再用ALTER TABLE或CREATE INDEX语句添加它们。因为FULLTEXT索引要求你必须使用MyISAM数据表,所以如果你正在创建一个需要使用全文搜索的MyISAM数据表,不妨利用一下MyISAM存储引擎的这个特点来加快点儿速度。在加载数据时,先填充数据表、再添加索引的办法要比先创建索引再加载数据的办法快得多。假设你有一个名为apothegm.txt的数据文件,其内容是一些名人名言:
- Aeschylus Time as he grows old teaches many lessons
- Alexander Graham Bell Mr. Watson, come here. I want you!
- Benjamin Franklin It is hard for an empty bag to stand upright
- Benjamin Franklin Little strokes fell great oaks
- Benjamin Franklin Remember that time is money
- Miguel de Cervantes Bell, book, and candle
- Proverbs 15:1 A soft answer turneth away wrath
- Theodore Roosevelt Speak softly and carry a big stick
- William Shakespeare But, soft! what light through yonder window breaks?
- Robert Burton I light my candle from their torches.
如果按"名人"、"名言"和"名人加名言"进行搜索,你需要创建3个FULLTEXT索引:两个数据列各有一个,它们加起来有一个。下面这些语句将创建、填充一个名为apothegm的数据表,并为它编制索引:
- CREATE TABLE apothegm (attribution VARCHAR(40), phrase TEXT) ENGINE = MyISAM;
- LOAD DATA LOCAL INFILE 'apothegm.txt' INTO TABLE apothegm;
- ALTER TABLE apothegm
- ADD FULLTEXT (phrase),
- ADD FULLTEXT (attribution),
- ADD FULLTEXT (phrase, attribution);
在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; } |
相关资料:
SCWS:http://www.ftphp.com/scws/download.php