MySQL 全文检索的实现
Posted 古时的风筝
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL 全文检索的实现相关的知识,希望对你有一定的参考价值。
微信搜「古时的风筝」,还有更多技术干货
这有朋友聊到他们的系统中要接入全文检索,这让我想起了很久以前为一个很古老的项目添加搜索功能的事儿。
一提到全文检索,我们首先就会想到搜索引擎。也就是用一个词、一段文本搜索出匹配的内容。一般这种技术都有对应的实现方式,ES(ElasticSearch)就是专门干这个的,如果你们的业务中明确需要全文检索,或者简单一点说,需要根据关键词搜索出匹配的内容,那就直接用 ES 就好了。
无论你怎么调研,都不推荐使用 mysql 实现这种需求,显而易见,MySQL 作为关系型数据库,本身就不适合做搜索这种需求。
但是,奈何,今天我们就要用 MySQL 来做这件事儿。
背景
有一个很古老的业务采集了大量的信息,当然是合法采集了。系统用的人已经不多了,并且在平稳的运行,那就不要动它了就好了嘛。可偏偏为数不多的人非要加一个搜索功能,根据一个关键词来搜索。
这项目直接没接触过,咱也不敢随意改呀,通过和少有的还有了解这个系统的同事沟通,发现有一类角色本来就有搜索功能,只不过这功能基本没法用,从来搜不出内容。现象就是点完搜索按钮,后台接口就一直 pending
,不用说了,那肯定是因为数据量太大了,或者某种很傻的原因,比如直接在大数据量、大段文本的字段中使用了 like
模糊查询。
经过一番查看,发现这个准备要支持搜索的字段是 text
类型的, 字段本身是不参与业务计算的,只是用来展示。而要搜索的内容还不止一个字段,好几个字段,这些字段的内容是什么呢,就是一段描述内容,里面有各种各样的专业名词,每一行记录中这个字段最大长度可能有几十到上千个字不等。
这张表由于数据量较大,并且字段很多,所以进行了分表,根据某个上层类型进行拆分,这样分出来的表,大的有上百万,小的有几十万。业务运算的时候,也是固定类型后,在这个类型下的分表中进行增删改查。
一看代码,果然,一条查询好几个 like
,在几十万数据量的表中like
好几个字段,不慢才怪,能查出来就是奇迹了。
于是勇敢的在数据库中尝试了一下一条查询的完整 SQL,在10分钟之后,还是果断结束了任务,一条SQL执行10分钟,就算用户能接受,我们自己也接受不了,好不好。
分析并思考解决方案
有需求就要处理,这种搜索的需求很明显就要用 ES 嘛,下载ES,准备本地搭建环境。
开玩笑的,加上 ES 不知道何年何年了,况且这么老的项目,能少动就少动,能不碰就不碰。这个法则,每个程序员都应该掌握。
思考
如果用户想要的不是通过任意关键字检索,而是通过指定一些我们为他预设好的关键词查询,就类似于抽出一些标签,可以按照标签组合搜索,那可以将需要搜索的字段中的内容拿出来分词、归类,抽取出相关的标签。这又是分词、又是分析的,想想也不比直接上 ES 简单。
还好,用户不想要这种的,就要不做限制,直接用关键词搜索。
务实主义
目前的处境是这样的:
1、不要做大的改动,因为项目老旧,并且不熟悉,用的人也不多了;
2、逻辑很明晰,就是模糊查询,但是目前性能极低;
3、直接在 MySQL 层做优化,确实是有办法的,具体效果只能试过之后才知道;
直接的优化手段其实也是非常简单的,MySQL 5.6版本后,MyISAM 和InnoDB 引擎已经全部支持全文索引了。还好,目前使用的数据库在5.6版本之后。
为了演示,我将最小的一张 296,560
表缩小了10倍变成了 2万9千多条,没有做任何处理,直接在一个最长的 text
类型的字段上做 like
查询,最后的查询时间是 1秒左右,偶尔慢的时候能达到2、3秒。
select * from case_data where case_name like '%侵权责任%';
用explain
分析一下,发现是全表扫描。
这只是查询了将近3万条数据,并且只查询了一个字段,并且没有其他逻辑,真实环境中的逻辑要复杂的多。
全文索引简单原理
MySQL 5.6之后的版本支持对 char、varchar、text 类型的字段创建全文索引。
当添加了全文索引之后,数据库引擎就会对添加索引的列进行语法语义的分析,并对它进行分词,之后对这些分出的短语进行索引,每个短语对应包含它的行的集合。
短语 | 包含的行的集合 |
---|---|
合同 | 第1行、第5行、第10行、第n行 |
项目管理 | 第2行、第3行、第22行、第1999行、第n+1行 |
产品研发 | 第500行、第3899行、第8899行、第n+2行 |
这样当我们搜索某个关键词后,如果正好对应了某个短语,就可以直接命中包含它的行。
有几个参数是控制全文索引的, ft(FullText) 开头的。用下面的命令可以查看。
show variables like '%ft%'
ft_boolean_syntax
表示布尔查询时的可以用的符号,改变IN BOOLEAN MODE的查询字符,一会儿下面会演示用法。
innodb_ft_min_token_size
对与 innodb 引擎,最短的索引字符串,默认值为84,修改后要重建索引
innodb_ft_max_token_size
对与 innodb 引擎,最长的索引字符串,默认值为3,修改后要重建索引
创建全文检索
下面这两种方式都可以对已经存在的表创建全文索引。
CREATE FULLTEXT INDEX <index_name> on tableName(字段名);
ALTER TABLE tableName ADD FULLTEXT <index_name>(字段名);
当然,如果你不想用SQL语句创建,也可以直接使用客户端工具创建。
比如我测试用的这个表叫做 case_data
,要支持全文检索的字段叫做 case_name
,使用下面的 SQL 创建索引,索引名称为 inde_case_name
。
ALTER TABLE case_data ADD FULLTEXT index_case_name(`case_name`);
创建索引的过程比较缓慢,对于大数据量的表更慢,尤其是全文索引,这3万条数据对这一个字段创建索引的过程差不多10秒钟左右,如果是线上正在使用的服务,创建这种耗时索引就要酌情考虑一下什么时机创建比较合适了。
再次查询测试性能
全文索引创建好之后,就可以测试一下效果如何了,执行一下,等着见证奇迹。
select * from case_data where case_name like '%侵权责任%';
咦,怎么不仅没快,反而慢了一点儿。
别慌,姿势不太对。全文索引有专门对应的查询关键字。使用 match
和against
配合查询,match 表示要匹配的列名称,against 表示要查询的关键词。比如下面这样:
select * from case_data where match(case_name) against('侵权责任');
确实是快了,通过分析可以看出已经开始走全文索引了,扫描的行数已经是常数行了。
但是,一顿操作猛如虎,一看结果啥都没有啊。
因为全文检索是有精度的,是按照分词出来的关键词进行完全匹配的,也就是说当前的分词短语中并不存在侵权责任
这个词,但是可能存在人身侵权责任
、无故侵权责任人
等短语。最简单的办法就是在查询侵权责任
这个短语时,也要命中人身侵权责任
、无故侵权责任人
这两个短语,又类似于模糊查询了。
怎么办呢,这样写就可以了。
select * from case_data where match(case_name) against('*侵权责任*' in boolean mode);
这样再次查询,结果就出来了。为什么会这样呢,前面我们提到一个变量,叫做ft_boolean_syntax
,这个变量中的符号就类似于正则表达式里支持的规则符号。
常见的匹配模式有下面这些:
空格
:可选的,包含该词的顺序较高
"text"
:全词匹配查找
text*
:通配符查找,*
只能放在后面
+text
:必须包含,+
只能放在词前面
-text
:必须不包含,不能单独使用,如`+aaaa-cccc
>text
:如果含有该词,提高词的相关性
<text
:如果含有该词,降低词的相关性
()
:条件组,如aaaa+(bbbb cccc)
表示必须包含 bbbb 或 cccc
本来就叫全文检索了,结果又整个模糊查找,一点儿也不彻底呀,还有没有别的办法了。
有一个,在5.7版本开始就内置了中文分词插件 ngram
,我们将刚才创建的索引删掉,然后重新用 ngram
做分词重新建立索引。
ALTER TABLE case_data ADD FULLTEXT index_case_name(`case_name`) WITH PARSER ngram;
等个十几秒中,然后再执行第一次差不到数据的SQL。
select * from case_data where match(case_name) against('侵权责任');
再看查询结果,已经有数据了。
性能提升
我的测试数据只有2万多条,这种少量数据的情况下,性能是看不到提升的。并且还由于创建了索引,增大了存储空间。
但是将数据量提升十倍,到二十多万,会看到性能明显提升了几十倍。我在线上测试了 200万的表,用全文索引的方式0.5秒内能出结果,用 like 的话,喝完一杯茶,发现还在跑着。
因为全文检索本来就是适用于大数据量的场景,所以对于小样本的数据量,直接用 like
也查不到哪儿去。
对于大数据量的场景,如果不引入ES等全文检索的中间件的情况下,用全文索引可以说是最快最划算的方式了。
如果对你有帮助,欢迎给个一键三连。
微信搜「古时的风筝」,还有更多技术干货
用 Mysql 实现一个全文检索系统
说到全文检索,大家接触最多的便是以 Solr、ElasticSearch 为首的分布式搜索引擎。一个常规技术方案是 OLTP 数据库用来满足常规的业务系统查询,而文本数据用 ES 进行存储,两个系统混合来满足业务需求。那么我们将应用场景简化:
总数据量百万级别;
需要建立文本索引字段平均长度在1000;
全文检索满足基本诉求即可:与或非逻辑支持、短语查询等;
语言以中文为主;
性能要求在 1s以内(没有其他额外查询开销);
Mysql FullText 简介
Mysql 对全文检索支持的版本迭代历程:
5.6 版本以下,只有MyISAM引擎支持全文检索;
5.6 版本以上,支持Innodb引擎;
5.7.6 版本以下只支持英文全文检索;
5.7.6 版本以上扩展出 Ngram-Parser,支持以 CJK 语言的全文检索;
我们以 Mysql8.0为例,简要说明功能:
Natural Language Full-Text Searches
IN NATURAL LANGUAGE MODE 是默认修饰符,MATCH() 函数对文本集合执行字符串的自然语言搜索。 集合是一组包含在 FULLTEXT 索引中的一个或多个列。搜索字符串作为AGAINST() 的参数提供。对于表中的每一行,MATCH() 返回一个相关值;
SELECT
title,
MATCH ( title ) AGAINST ( '大数据' IN NATURAL LANGUAGE MODE ) AS score
FROM
mf_docs
WHERE
MATCH ( title ) AGAINST ( '大数据' IN NATURAL LANGUAGE MODE )
LIMIT 10;
默认按照相关的得分倒序排列:
title | score(相关度得分) |
【德州4项目入选!省级大数据“三优两重”项目公布】日前,省工信厅公布2020年度省级大数据“三优两重”项目名单,我市 4项目入选。据悉,“三优”指山东省优秀大数据产品、优秀大数据解决方案、优秀大数据应用案例,“两重”指山东省重点大数据企业、重点大数据资源。 | 65.07472229003906 |
【重庆市大数据标准化建设实施方案启动】#大数据# 《重庆市大数据标准化建设实施方案(2020-2022年)》日前启动,《方案》提出到2022年,重庆市将参与国家大数据共享开放标准、数据安全标准研制3项以上,参与示范应用5项以上,推动研制大数据标准规范20项以上。via重庆日报 | 50.245452880859375 |
(原)大数据的4V特征:一,Volume 大数据量大,单位为T 或P级。二,Variety 大数据包括日志、视频、图片等多种类型。三,Value 大数据的商业和社会管理价值高, 比如:监控视频。四,Velocity 处理速度快。大数据技术可在媒体机构发布的年度关键词中“小试牛刀”,也可在“ | 46.48194122314453 |
福泉市大数据发展服务中心揭牌成立 1月11日,福泉市大数据发展服务中心揭牌成立。大数据发展服务中心将发挥汇聚、整合、清洗全市数据、挖掘和应用数据资源潜在价值的基础作用,为提升城市规划和城市基础设施管理数字化、精准化水平提供有利平台,实现“用数据说话、用数据决 | 42.943199157714844 |
...... |
...... |
Boolean Full-Text Searches
MySQL可以使用IN BOOLEAN MODE
修饰符执行布尔型全文检索 。也就是我常说的 AND 、OR 逻辑。这里我们举例说明:文本包含“大数据“,排除”北京“
SELECT
title
FROM
mf_docs
WHERE
MATCH ( title ) AGAINST ( '+大数据 -北京' IN BOOLEAN MODE )
LIMIT 10;
Full-Text Searches with Query Expansion
这类场景适合查询短语较为短小,而通常的做法是我们自己维护一批扩
展词或者同义词,进行联合查询。而 Mysql 中的查询扩展是基于第一次相关度查询的结果,进行二次查询。这里以官方举例:
mysql> SELECT * FROM articles
WHERE MATCH (title,body)
AGAINST ('database' IN NATURAL LANGUAGE MODE);
+----+-------------------+------------------------------------------+
| id | title | body |
+----+-------------------+------------------------------------------+
| 1 | MySQL Tutorial | DBMS stands for DataBase ... |
| 5 | MySQL vs. YourSQL | In the following database comparison ... |
+----+-------------------+------------------------------------------+
2 rows in set (0.00 sec)
# 基于第一次的相关度查询的结果,会二次匹配那些记录与上述的两篇文档相似的结果返回
mysql> SELECT * FROM articles
WHERE MATCH (title,body)
AGAINST ('database' WITH QUERY EXPANSION);
+----+-----------------------+------------------------------------------+
| id | title | body |
+----+-----------------------+------------------------------------------+
| 5 | MySQL vs. YourSQL | In the following database comparison ... |
| 1 | MySQL Tutorial | DBMS stands for DataBase ... |
| 3 | Optimizing MySQL | In this tutorial we show ... |
| 6 | MySQL Security | When configured properly, MySQL ... |
| 2 | How To Use MySQL Well | After you went through a ... |
| 4 | 1001 MySQL Tricks | 1. Never run mysqld as root. 2. ... |
+----+-----------------------+------------------------------------------+
6 rows in set (0.00 sec)
环境准备
这里以 Mysql8.0 为软件环境,硬件为真实的物理机(64GB内存,48 Core)。未做任何调优。
表结构: 其中 title 平均长度在1000以内,content 平均长度在 10000以上:
CREATE TABLE mf_docs (
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
titleSimHash VARCHAR(64),
pubTime BIGINT,
title TEXT,
content TEXT,
FULLTEXT (title) WITH PARSER ngram,
FULLTEXT (content) WITH PARSER ngram
) ENGINE=InnoDB CHARACTER SET utf8mb4;
数据量:总数据量在80万,标准的互联网文本数据:
MySQL [fulltext_db]> select count(1) from mf_docs;
+----------+
| count(1) |
+----------+
| 828414 |
+----------+
1 row in set (2.49 sec)
性能对比分析
# 短语查询 title 匹配 ”大数据“
MySQL [fulltext_db]> SELECT count(1) AS num FROM mf_docs WHERE MATCH (title) AGAINST ('"大数据"' IN BOOLEAN MODE);
+------+
| num |
+------+
| 3270 |
+------+
1 row in set (0.19 sec)
# Like 查询 title 匹配 ”大数据“
MySQL [fulltext_db]> SELECT count(1) AS num FROM mf_docs WHERE title like ('%大数据%');
+------+
| num |
+------+
| 3270 |
+------+
1 row in set (3.43 sec)
# 短语查询 content 匹配 ”大数据“
MySQL [fulltext_db]> SELECT count(1) AS num FROM mf_docs WHERE MATCH (content) AGAINST ('"大数据"' IN BOOLEAN MODE);
+-------+
| num |
+-------+
| 30192 |
+-------+
1 row in set (4.29 sec)
# Like 查询 content 匹配 ”大数据“
MySQL [fulltext_db]> SELECT count(1) AS num FROM mf_docs WHERE content like ('%大数据%');
+-------+
| num |
+-------+
| 30190 |
+-------+
1 row in set (17.56 sec)
从以上结果可以看出:
全文检索的性能的远高于 Like查询。性能提高在 4 ~ 30 倍不等。
文本索引长度不同,全文检索的性能差异在 几十倍左右。
Phrase 短语查询性能要低于 一般形式的查询(会引起搜索结果不精准等问题)。
MySQL [fulltext_db]> SELECT count(1) as num FROM mf_docs WHERE MATCH (content) AGAINST ('大数据' IN NATURAL LANGUAGE MODE);
+--------+
| num |
+--------+
| 126903 |
+--------+
1 row in set (0.27 sec)
# 可以看到 NATURAL LANGUAGE MODE 要比 短语查询(4.29 sec)高出百倍性能。
问题分析:在对content字段进行全文检索时,发现有(30192 - 30190) 2 条的数据误差:进行校验,的确存在 ”大数据“,但是无法进行匹配,应该是存在一些特殊字符吧,这里不再进行展开分析。
总结
如果你的场景是在百万级别的数据规模下,同时全文检索字段长度不大的情况下,可以考虑使用 Mysql FullText Searches 机制。同时避免了引入 ES 等额外组件带来的性能开销。
以上是关于MySQL 全文检索的实现的主要内容,如果未能解决你的问题,请参考以下文章