MySQL在高併發連接、數據庫記錄數較多的情況下,SELECT ... WHERE ... LIKE '%...%'的全文搜索方式不僅效率差,而且以通配符%和_開頭作查詢時,使用不到索引,需要全表掃描,對數據庫的壓力也很大。MySQL針對這一問題提供了一種全文索引解決方案,這不僅僅提高了性能和效率(因爲MySQL對這些字段做了索引來優化搜索),而且實現了更高質量的搜索。但是,至今爲止,MySQL對中文全文索引無法正確支持。
中文與西方文字如英文的一個重要區別在於,西方文字以單詞爲單位,單詞與單詞之間以空格分隔。而中文以字爲單位,詞由一個或多個字組成,詞與詞之間沒有空格分隔。當試圖在一個含有中文字符的字段中使用全文搜索時,不會得到正確的結果,原因在於中文中沒有像英文空格那樣對詞定界,不能以空格作爲分割,對中文詞語進行索引。
引用《MySQL 5.1參考手冊》中的一段話:
● MySQL支持全文索引和搜索功能。MySQL中的全文索引類型FULLTEXT的索引。FULLTEXT 索引僅可用於 MyISAM 表;他們可以從CHAR、 VARCHAR或TEXT列中作爲CREATE TABLE語句的一部分被創建,或是隨後使用ALTER TABLE 或 CREATE INDEX被添加。對於較大的數據集,將你的資料輸入一個沒有FULLTEXT索引的表中,然後創建索引, 其速度比把資料輸入現有FULLTEXT索引的速度更爲快。
● FULLTEXT分析程序會通過尋找某些分隔符來確定單詞的起始位置和結束位置,例如' ' (間隔符號)、 , (逗號)以及 . (句號 )。假如單詞沒有被分隔符分開,(例如在中文裏 ), 則 FULLTEXT 分析程序不能確定一個詞的起始位置和結束位置。爲了能夠在這樣的語言中向FULLTEXT 索引添加單詞或其它編入索引的術語,你必須對它們進行預處理,使其被一些諸如"之類的任意分隔符分隔開。
● 諸如漢語和日語這樣的表意語言沒有自定界符。因此, FULLTEXT分析程序不能確定在這些或其它的這類語言中詞的起始和結束的位置。
國內已有的MySQL中文全文索引解決方案有兩個:一是海量科技的MySQL5.0.37--LinuxX86-Chinese+,二是hightman開發的mysql-5.1.11-ft-hightman,兩者都是基於中文分詞技術,對中文語句進行拆分。但是,兩者都有弊端,一是不支持64位操作系統;二是對修改了MySQL源碼,只支持某一MySQL版本,不便於跟進新版本;三是詞庫不能做到很大很全,對於專業性質較強的數據庫內容(例如搜索“頤和園路東口”、“清華東路西口”等公交站點,“萊鎮香格里”、“碧海雲天”等樓盤名稱),基於中文分詞的全文索引經常搜索不出來任何內容,即使添加分詞詞庫,也不會很全面。
由於業務上精準全文查詢的需要,我借鑑了二元交叉切分算法的思想,用自創的“三字節交叉切分算法”,寫出了這款“MySQL中文全文索引插件──mysqlcft 1.0.0”。由於開發時間倉促,難免存在未發現的問題,這將後續的版本中不斷完善。對於百萬條記錄的MySQL表進行全文檢索,mysqlcft已經夠用。
Mysqlcft 網址:http://code.google.com/p/mysqlcft/
Mysqlcft 作者:張宴
一、MySQL中文全文索引插件mysqlcft的特點:
1、優點:
①、精準度很高:採用自創的“三字節交叉切分算法”,對中文語句進行分割,無中文分詞詞庫,搜索精準度遠比中文分詞算法高,能達到LIKE '%...%"的準確率。
②、查詢速度快:查詢速度比LIKE '%...%"搜索快3~50倍,文章末尾有測試結果;
③、標準插件式:以MySQL 5.1全文索引的標準插件形式開發,不修改MySQL源代碼,不影響MySQL的其他功能,可快速跟進MySQL新版本;
④、支持版本多:支持所有的MySQL 5.1 Release Candidate版本,即MySQL 5.1.22 RC~最新的MySQL 5.1.25 RC;
⑤、支持字符集:支持包括GBK、GB2312、UTF-8、Latin1、BIG5在內的MySQL字符集(其他字符集沒有測試過);
⑥、系統兼容好:具有i386和x86_64兩個版本,支持32位(i386)和64位(x86_64)CPU及Linux系統;
⑦、適合分佈式:非常適合MySQL Slave分佈式系統架構,無詞庫維護成本,不存在詞庫同步問題。
2、缺點:
①、mysqlcft中文全文索引只適用於MyISAM表,因爲MySQL只支持對MyISAM表建立FULLTEXT索引;
②、MySQL不能靜態編譯安裝,否則無法安裝mysqlcft插件;
③、基於“三字節交叉切分算法”的索引文件會比海量、ft-hightman等基於“中文分詞算法”的索引文件稍大,但不是大很多。根據我的測試,mysqlcft全文索引的.MYI索引文件是.MYD數據文件的2~5倍。
二、mysqlcft的核心思想──“三字節交叉切分算法”
注:本文以0~7數字序號代表“英文”、“數字”和“半個漢字”,以便說明。
1、按三字節對中文語句進行切分,建立全文索引:
例如:“全文索引”或“1臺x光機”四個字會被交叉分拆爲6份,建立反向索引:
012 123 234 345 456 567
2、按三字節對搜索的關鍵字進行切分,在全文索引中找出對應信息:
例①:搜索關鍵字“文索”,用數字序號表示就是“2~5”,那麼它將被切分成:
234 345
這樣,就與全文索引對上了。
例②:搜索關鍵字“x光機”,用數字序號表示就是“3~7”,那麼它將被切分成:
345 456 567
這樣,也與全文索引對上了。
例③:搜索關鍵字“1臺 光機”,用數字序號表示就是“0~2”和“4~7”,那麼它將被切分成:
012 456 567
這樣,多關鍵字搜索也與全文索引對上了。
三、編譯安裝MySQL(如果已經裝有不是靜態編譯安裝的MySQL 5.1.22 RC~MySQL 5.1.25 RC,此步驟可省略)
1、下載並編譯安裝MySQL 5.1.25 RC
在http://dev.mysql.com/get/Downloads/MySQL-5.1/mysql-5.1.25-rc.tar.gz/from/pick(點擊No thanks, just take me to the downloads!鏈接),選擇一個鏡像,下載MySQL 5.1.25 RC源碼包:
cd mysql-5.1.25-rc/
./configure --prefix=/usr/local/mysqlcft/ --without-debug --enable-assembler --with-extra-charsets=all --with-pthread --enable-thread-safe-client
make && make install
/usr/sbin/groupadd mysql
/usr/sbin/useradd -g mysql mysql
chmod +w /usr/local/mysqlcft
chown -R mysql:mysql /usr/local/mysqlcft
2、創建MySQL數據文件存放目錄/mysql/3306
chmod +w /mysql/3306
chown -R mysql:mysql /mysql/3306
mkdir -p /mysql/3306/data
chmod +w /mysql/3306/data
chown -R mysql:mysql /mysql/3306/data
chown -R mysql:mysql /mysql
#cp support-files/my-medium.cnf /mysql/3306/my.cnf
cd ../
3、創建配置文件/mysql/3306/my.cnf
輸入以下內容(注意:必須設置ft_min_word_len = 1):
#password = your_password
port = 3306
socket = /mysql/3306/mysql.sock
default-character-set = gbk
[mysqld_safe]
datadir = /mysql/3306/data
log-error = /mysql/3306/mysql_error.log
pid-file = /mysql/3306/mysql.pid
[mysqld]
port = 3306
socket = /mysql/3306/mysql.sock
default-character-set = gbk
#init_connect = 'SET NAMES gbk'
skip-locking
#skip-slave-start
key_buffer = 512M
max_allowed_packet = 2M
table_cache = 1024
sort_buffer_size = 32M
read_buffer_size = 2M
read_rnd_buffer_size = 32M
max_length_for_sort_data = 64
myisam_sort_buffer_size = 128M
thread_cache = 8
query_cache_size = 64M
# Try number of CPU's*2 for thread_concurrency
thread_concurrency = 8
#skip-name-resolve
set-variable = max_connections=1000
open_files_limit = 51200
ft_min_word_len = 1
low_priority_updates = 1
slave-skip-errors = 1032,1062,126
server-id = 9
#master-host = host
#master-user = user
#master-password = password
#master-port = 3306
#replicate-do-db = db1
#replicate-do-db = db2
[mysqldump]
quick
max_allowed_packet = 16M
[mysql]
no-auto-rehash
# Remove the next comment character if you are not familiar with SQL
#safe-updates
[isamchk]
key_buffer = 256M
sort_buffer_size = 256M
read_buffer = 2M
write_buffer = 2M
[myisamchk]
key_buffer = 256M
sort_buffer_size = 256M
read_buffer = 2M
write_buffer = 2M
[mysqlhotcopy]
interactive-timeout
附:MySQL配置文件在全文索引應用中的優化
# key_buffer 指定用於索引的緩衝區大小,在全文索引中,增加它可得到更好的索引處理與查詢性能
key_buffer = 512M
# sort_buffer_size 爲查詢排序時所能使用的緩衝區大小,全文索引的SQL語句之後通常會使用ORDER BY排序,增加它可以加快SQL語句執行時間。該參數對應的分配內存是每連接獨佔,100個連接使用的內存將是32M*100=3200M
sort_buffer_size = 32M
# 對大於可用內存的表執行GROUP BY或ORDER BY操作,應增加read_rnd_buffer_size的值以加速排序操作後面的行讀取
read_rnd_buffer_size = 64M
# 如果表出現故障或索引出錯,REPAIR TABLE時用到的緩衝區大小
myisam_sort_buffer_size = 128M
# 確定使用的filesort算法的索引值大小的限值
max_length_for_sort_data = 64
# MySQL全文索引查詢所用關鍵詞最小長度限制(不要改變這項值)
ft_min_word_len = 1
# 降低UPDATE優先級,設置查詢優先
low_priority_updates = 1
4、以mysql用戶帳號的身份建立數據表
5、啓動MySQL
附:停止MySQL
四、安裝mysqlcft中文全文索引插件
1、從命令行登入MySQL服務器:
2、查看MySQL插件目錄的默認路徑的SQL語句:
3、下載mysqlcft中文全文索引插件,解壓後拷貝mysqlcft.so文件到MySQL插件目錄
①、32位Linux操作系統:
tar zxvf mysqlcft-1.0.0-i386-bin.tar.gz
mkdir -p /usr/local/mysqlcft/lib/mysql/plugin/
cp mysqlcft.so /usr/local/mysqlcft/lib/mysql/plugin/
②、64位Linux操作系統:
tar zxvf mysqlcft-1.0.0-x86_64-bin.tar.gz
mkdir -p /usr/local/mysqlcft/lib/mysql/plugin/
cp mysqlcft.so /usr/local/mysqlcft/lib/mysql/plugin/
4、安裝mysqlcft.so插件
①、從命令行登入MySQL服務器:
②、安裝mysqlcft.so插件的SQL語句:
③、查看mysqlcft.so插件是否安裝成功的SQL語句:
SHOW PLUGINS;
附:如果要卸載mysqlcft.so插件,執行以下SQL語句(如果已經創建了mysqlcft索引,請先刪除mysqlcft索引,再卸載mysqlcft.so插件):
五、爲已經存在的表添加mysqlcft中文全文索引
1、創建單列全文索引SQL語句
2、創建全文聯合索引SQL語句
六、重建mysqlcft中文全文索引(索引損壞時需要用到)
七、建表時創建mysqlcft中文全文索引+全文搜索測試
1、以latin1字符集爲例
USE `mysqlcft_latin1`;
CREATE TABLE `test` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(200) DEFAULT NULL,
`body` mediumtext,
PRIMARY KEY (`id`),
FULLTEXT KEY `title_body` (`title`,`body`) WITH PARSER mysqlcft
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
INSERT INTO `mysqlcft_latin1`.`test` (`id`, `title`, `body`) VALUES (NULL, '北京房價', '北京市統計局、國家統計局北京調查總隊近日聯合對外發布消息,今年以來,北京的商品房價格一直呈上升趨勢,五環路以內住宅期房均價已漲至13754元/平方米。');
INSERT INTO `mysqlcft_latin1`.`test` (`id`, `title`, `body`) VALUES (NULL, '北京中心城區今起可無線寬帶上網 奧運期間免費', '新浪科技訊 6月25日消息,北京無線城市一期網絡今日起試運行,即日起北京市民和海外遊客可以通過無線網絡在北京中心城區接入互聯網。');
INSERT INTO `mysqlcft_latin1`.`test` (`id`, `title`, `body`) VALUES (NULL, '數據庫', '歡迎使用MySQL中文全文索引插件mysqlcft!');
SELECT * FROM mysqlcft_latin1.test WHERE MATCH(title,body) AGAINST ('13754元/平方米' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_latin1.test WHERE MATCH(title,body) AGAINST ('6月25日' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_latin1.test WHERE MATCH(title,body) AGAINST ('北京' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_latin1.test WHERE MATCH(title,body) AGAINST ('北京 寬帶' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_latin1.test WHERE MATCH(title,body) AGAINST ('mysqlcft' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_latin1.test WHERE MATCH(title,body) AGAINST ('數據' IN BOOLEAN MODE);
2、以UTF-8字符集爲例
USE `mysqlcft_utf8`;
CREATE TABLE `test` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(200) DEFAULT NULL,
`body` mediumtext,
PRIMARY KEY (`id`),
FULLTEXT KEY `title_body` (`title`,`body`) WITH PARSER mysqlcft
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `mysqlcft_utf8`.`test` (`id`, `title`, `body`) VALUES (NULL, '北京房價', '北京市統計局、國家統計局北京調查總隊近日聯合對外發布消息,今年以來,北京的商品房價格一直呈上升趨勢,五環路以內住宅期房均價已漲至13754元/平方米。');
INSERT INTO `mysqlcft_utf8`.`test` (`id`, `title`, `body`) VALUES (NULL, '北京中心城區今起可無線寬帶上網 奧運期間免費', '新浪科技訊 6月25日消息,北京無線城市一期網絡今日起試運行,即日起北京市民和海外遊客可以通過無線網絡在北京中心城區接入互聯網。');
INSERT INTO `mysqlcft_utf8`.`test` (`id`, `title`, `body`) VALUES (NULL, '數據庫', '歡迎使用MySQL中文全文索引插件mysqlcft!');
SELECT * FROM mysqlcft_utf8.test WHERE MATCH(title,body) AGAINST ('13754元/平方米' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_utf8.test WHERE MATCH(title,body) AGAINST ('6月25日' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_utf8.test WHERE MATCH(title,body) AGAINST ('北京' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_utf8.test WHERE MATCH(title,body) AGAINST ('北京 寬帶' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_utf8.test WHERE MATCH(title,body) AGAINST ('mysqlcft' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_utf8.test WHERE MATCH(title,body) AGAINST ('數據' IN BOOLEAN MODE);
3、以gbk字符集爲例
USE `mysqlcft_gbk`;
CREATE TABLE `test` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(200) DEFAULT NULL,
`body` mediumtext,
PRIMARY KEY (`id`),
FULLTEXT KEY `title_body` (`title`,`body`) WITH PARSER mysqlcft
) ENGINE=MyISAM DEFAULT CHARSET=gbk;
INSERT INTO `mysqlcft_gbk`.`test` (`id`, `title`, `body`) VALUES (NULL, '北京房價', '北京市統計局、國家統計局北京調查總隊近日聯合對外發布消息,今年以來,北京的商品房價格一直呈上升趨勢,五環路以內住宅期房均價已漲至13754元/平方米。');
INSERT INTO `mysqlcft_gbk`.`test` (`id`, `title`, `body`) VALUES (NULL, '北京中心城區今起可無線寬帶上網 奧運期間免費', '新浪科技訊 6月25日消息,北京無線城市一期網絡今日起試運行,即日起北京市民和海外遊客可以通過無線網絡在北京中心城區接入互聯網。');
INSERT INTO `mysqlcft_gbk`.`test` (`id`, `title`, `body`) VALUES (NULL, '數據庫', '歡迎使用MySQL中文全文索引插件mysqlcft!');
SELECT * FROM mysqlcft_gbk.test WHERE MATCH(title,body) AGAINST ('13754元/平方米' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_gbk.test WHERE MATCH(title,body) AGAINST ('6月25日' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_gbk.test WHERE MATCH(title,body) AGAINST ('北京' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_gbk.test WHERE MATCH(title,body) AGAINST ('北京 寬帶' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_gbk.test WHERE MATCH(title,body) AGAINST ('mysqlcft' IN BOOLEAN MODE);
SELECT * FROM mysqlcft_gbk.test WHERE MATCH(title,body) AGAINST ('數據' IN BOOLEAN MODE);
八、性能測試報告
服務器:DELL PowerEdge 6850 (四顆雙核Xeon 3.0GHz,8GB內存) 4U機架式服務器
操作系統:RedHat AS4 (x86_64位)
數據庫:MySQL 5.1.25 RC + mysqlcft 1.0.0
數據表:超過80萬條(807346條)記錄的表,字段“id”爲int類型,主鍵;字段“title”爲varchar類型,字段“body”爲text類型。“title”和“body”分別建有INDEX普通單列索引、INDEX聯合索引,FULLTEXT單字段全文索引、FULLTEXT聯合全文索引。
1、在字段“title”中搜索中文關鍵字:
30 rows in set (0.04 sec)
SELECT * FROM database.table WHERE title LIKE '%朝陽區%' limit 0,30;
30 rows in set (6.56 sec)
SELECT * FROM database.table WHERE MATCH(title) AGAINST ('通州區' IN BOOLEAN MODE) ORDER BY id DESC limit 0,30;
30 rows in set (0.13 sec)
SELECT * FROM database.table WHERE title LIKE '%通州區%' ORDER BY id DESC limit 0,30;
30 rows in set (8.15 sec)
SELECT * FROM database.table WHERE MATCH(title) AGAINST ('建國門外' IN BOOLEAN MODE) ORDER BY id DESC limit 0,30;
30 rows in set (0.08 sec)
SELECT * FROM database.table WHERE title LIKE '%建國門外%' ORDER BY id DESC limit 0,30;
30 rows in set (5.34 sec)
SELECT * FROM database.table WHERE MATCH(title) AGAINST ('靠近通惠河' IN BOOLEAN MODE) ORDER BY id DESC limit 0,30;
4 row in set (0.06 sec)
SELECT * FROM database.table WHERE title LIKE '%靠近通惠河%' ORDER BY id DESC limit 0,30;
4 row in set (12.88 sec)
2、在字段“body”中搜索中文關鍵字:
30 rows in set (0.23 sec)
SELECT * FROM database.table WHERE body LIKE '%海淀區%' ORDER BY id DESC limit 0,30;
30 rows in set (15.71 sec)
SELECT * FROM database.table WHERE MATCH(body) AGAINST ('萊鎮香格里' IN BOOLEAN MODE) ORDER BY id DESC limit 0,30;
6 rows in set (0.18 sec)
SELECT * FROM database.table WHERE body LIKE '%萊鎮香格里%' ORDER BY id DESC limit 0,30;
6 row in set (13.34 sec)
3、在字段“title”和“body”中,搜索包含“西城區”和“商場”兩個關鍵字的記錄:
13 rows in set (0.27 sec)
SELECT * FROM database.table WHERE title LIKE '%西城區%商場%' OR body LIKE '%西城區%商場%' ORDER BY id DESC limit 0,30;
13 rows in set (51.74 sec)