共计 4166 个字符,预计需要花费 11 分钟才能阅读完成。
问题描述
数据库中有如下的地址信息表,需要实现一个更具用户输入的任何内容进行搜索可能匹配的地址信息。
-- MySQL 版本: 5.7.25
CREATE TABLE Address
(
id BIGINT NOT NULL AUTO_INCREMENT,
address VARCHAR(100) NOT NULL DEFAULT '',
city VARCHAR(50) NOT NULL DEFAULT '',
state VARCHAR(50) NOT NULL DEFAULT '',
country VARCHAR(50) NOT NULL DEFAULT '',
zip_code VARCHAR(10) NOT NULL DEFAULT '',
FULLTEXT ftidx_location(address, city, state, country, zip_code)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
insert into Address(city, state) values ('Irving', 'TX');
容易想到利用如下的 sql 进行检索。
-- 这里的 ${input} 为用户输入的内容
select * from Address where match(address, city, state, country, zip_code) against (${input});
然而对于太短的输入,如 “TX”,即使数据库中存在 state = TX 的数据,该 SQL 也是无法检索到任何结果。或者输入 “Irvin” 也是无法查找到内容的。下面将对该问题进行分析和解决,使用 ”Irvin,TX” 作为用户输入进行分析(不含双引号)。
原因分析
实现使用的是 MySQL 的 FULLTEXT INDEX
对(address, city, state, country, zip_code)进行了索引。FULLTEXT INDEX
的配置保留了 MySQL 的默认配置,如下:
mysql> SHOW VARIABLES LIKE '%ft%';
+---------------------------------+----------------+
| Variable_name | Value |
+---------------------------------+----------------+
| ft_boolean_syntax | + -><()~*:""&| |
| ft_max_word_len | 84 |
| ft_min_word_len | 4 |
| ft_query_expansion_limit | 20 |
| ft_stopword_file | (built-in) |
| innodb_ft_aux_table | |
| innodb_ft_cache_size | 8000000 |
| innodb_ft_enable_diag_print | OFF |
| innodb_ft_enable_stopword | ON |
| innodb_ft_max_token_size | 84 |
| innodb_ft_min_token_size | 3 |
| innodb_ft_num_word_optimize | 2000 |
| innodb_ft_result_cache_limit | 2000000000 |
| innodb_ft_server_stopword_table | |
| innodb_ft_sort_pll_degree | 2 |
| innodb_ft_total_cache_size | 640000000 |
| innodb_ft_user_stopword_table | |
+---------------------------------+----------------+
FULLTEXT 索引是按照“词”进行的索引,MySQL 默认的分词方法是 所有非字母和数字的特殊符号都是分词符(如果希望对中文进行分词,则可以使用 MySQL 内置的 ngram 全文检索插件)。按照分词方法,”Irving,TX” 将被划分为 “Irving” 和 “TX” 两个词。
再看下配置的内容,其中 innodb_ft_min_token_size
表示最短的索引词项,也就是只会对 3 个英文字符或者 3 个英文字符以上的关键字进行建立索引操作。MySQL 不会对 ”TX” 创建索引,这也就是没法搜索到 ”TX” 的数据的原因。而之所以没法搜索到 ”Irvin”,是因为 Fulltext 是对“词”进行构建索引,也就是索引文件中只有”Irving“的索引,没有“Irvin”的索引。
解决方法
方法一: 修改 FULLTEXT INDEX 配置
修改最小词项长度为 2,允许对长度为 2 的词进行索引。并使用 IN BOOLEAN MODE
匹配不完整单词。
修改最小词项长度为 2,允许对长度为 2 的词进行索引
觉得 1 太小了,一般的单词都不会是一个字母的,而且如果这个数值太小,会导致索引文件过大,不利于索引的更新。因而修改为 2 就行了。在 MyISAM 数据库引擎中使用的是ft_min_word_len
,而 InnoDB 中使用的是innodb_ft_min_token_size
。
在修改之前执行,即使数据库中含有 state=TX 的数据,查询的结果还是会为空。
select * from Address where match(address, city, state, country, zip_code) against ('TX');
-
修改 my.cnf,在 [mysqld] 后面加入配置项。
sudo vim /etc/mysql/my.cnf
配置内容
innodb_ft_min_token_size=2 ft_min_word_len=2
-
重启 mysql 服务。
sudo service mysql restart
-
重新构建索引文件。
对于使用 MyISAM 的表需要手动修复。详细见:what to do when ‘ repair table ‘ query won’t work in mysql? 以及 Fine-Tuning MySQL Full-Text Search 或者其中文翻译微调 MySQL 全文搜索。
REPAIR TABLE Address QUICK;
而对于使用 InnoDB 的表,可以使用如下指令对表进行索引的重新构建。该操作会获取到表的读锁。
ALTER TABLE Address ENGINE=INNODB;
使用优化指令也可以起到同样的作用,同时这个指令会完成更多的优化作用。OPTIMIZE TABLE 运行过程中,MySQL 会锁定表。
OPTIMIZE TABLE Project; -- 执行之后会返回如下信息,但实际上是执行成功的 -- Table does not support optimize, doing recreate + analyze instead
-
查看是否生效。
show variables like 'innodb_ft_min_token_size'; show variables like 'ft_min_word_len';
在修改之后执行,如果数据库中含有 state=TX 的数据都会被查询出来。
select * from Address where match(address, city, state, country, zip_code) against ('TX');
使用
IN BOOLEAN MODE
匹配不完整单词
用户输入的内容的顺序为从左到右输入,也就是如果用户要输入多个单词,那么最左侧的单词必然是完整的,最右侧的单词可能是不完整的。那么可以在用户输入的检索地址末尾添加 * 通配符,使得可以匹配不完整单词。可以修改为:
select * from Address where match(address, city, state, country, zip_code) against ('TX,Irvin*' IN BOOLEAN MODE);
该方法依赖于 MySQL 自身的配置,如果确定要使用FULLTEXT INDEX
,应该在创建数据库的时候就配置好需要的配置,以免影响已经上线的系统。
注意 :如果用户本身的输入中也含有了ft_boolean_syntax
指定的通配符,那么我们需要在程序中先进行一次格式化,以防出现非法查询,如“Irvin**”等。或者也可以限制 ft_boolean_syntax
的字符。
方法二: 使用 LIKE 做匹配
曲线救国,使用 Like
代替 FULLTEXT INDEX
。在程序中按照 MySQL 的方法进行分词,并在每个单词之间增加通配符 %
。
select * from Address where concat(address, ',', city, ',', state, '', zip_code,' ', country) like'%Irvin%TX%';
使用该方法,将会失去对地址信息的索引,并因为使用了 Like
,且查询的内容是以而导致需要对整表进行扫描。此外,Like
是没有匹配对度的,也就是结果的顺序将和匹配度无关。
注:Like
只有在非通配符开始的语句中才会使用到索引,如: “Irvin%” 将会使用索引,而 “%Irvin” 则不会使用到索引。
方法三: 增加 full_address 字段,并使用 LIKE 做匹配
添加一个完整的 full_address
字段,full_address
值为 address, city, state zip_code, country
。
select * from Address where full_address like '%Irvin%TX%';
该方法主要是利用空间换时间,解决了方法二中每次查询都需要拼接字符串的耗时操作。
方法四: 使用专业的搜索引擎
使用更为专业的搜索引擎,如 Elasticsearch 或者 Solr。
参考
- 为什么要设置 Mysql 的 ft_min_word_len=1 @csdn
- MySQL 使用全文索引(fulltext index) @cnblogs
- what to do when ‘ repair table ‘ query won’t work in mysql? @stackoverflow
- Fine-Tuning MySQL Full-Text Search @mysql
- 微调 MySQL 全文搜索 @docs4dev
- FullText Search Innodb Fails, MyIsam Returns Results @stackoverflow
- 实例说明 optimize table 在优化 mysql 时很重要 @51yip
- InnoDB/MyisAM 存储引擎支持中文全文索引 @51cto