今日研读了《高性能MySQL》一书中的第 7.10 章节:MySQL高级特性之全文索引,从中学习到了一些专业技能知识。总结如下。
全文索引是为 “通过关键字的匹配来进行相似度的查询过滤” 的场景而设计的。
MyISAM的全文索引作用对象是一个"全文集合",这可能是某个数据表的一列,也可能是多个列。具体的,对数据表的某一条记录,MySQL会将需要索引的列全部拼接成一个字符串,然后进行索引。
MyISAM对全文索引的支持有很多的限制,例如表级别锁对性能的影响、数据文件的崩溃、崩溃后的恢复等,这使得MyISAM的全文索引对于很多应用场景并不合适。
MyISAM的全文索引是一类特殊的 B-Tree 索引,共有两层。第一层是所有关键字,然后对于每一个关键字的第二层,包含的是一组相关的"文档指针",全文索引不会索引对象中的所有词语,它会根据如下规则过滤一些词语:
全文索引并不会存储关键字具体匹配在哪一行,如果需要根据不同的列来进行组合查询,那么不需要根据每一列来建立多个这类索引。这也意味着不能在像 MATCH AGAINST 的子句中指定哪个列的相关性更重要:
通常构建一个网站的搜索引擎是需要这样的功能,例如,用户希望优先搜索出那些在标题中出现过的文档对象。如果需要这样的功能,则需要编写更复杂的查询。
自然语言搜索引擎将计算每一个文档对象和查询的相关度。相关度是基于匹配的关键词个数,以及关键词在文档中出现的次数。注意是:在整个索引中出现次数越少的词语,匹配时的相关度就越高。相反,非常长常见的单词将不会被搜索,即使不再停用词列表中出现,如果一次词语在超过50%的记录中都出现了,那么自然语言搜索将不会搜索这类词语。
如果进行全文搜索的数据集过小,可能无法返回结果。原因在于,每个搜索关键词都可能在一半以上的记录里面出现过。
全文索引的语法和不同查询略有不同。可以根据 WHERE 子句中的 MATCH(columns) AGAINST('keyword')
来区分查询是否使用全文索引,示例语句如:
SELECT id, title, RIGHT(description, 25), MATCH(title, description) AGAINST('factory casualties') AS relevance
FROM sakila.film_text
WHERE MATCH(title, description) AGAINST('factory casualties');
上面的语句中,MySQL将搜索词 ‘factory casualties’ 分成两个独立的关键词在 title 和 description 字段组成的全文索引上进行搜索。搜索结果将根据关键词的相似度排序列出(和普通查询不同,这类查询自动按照相似度进行排序。注意,在使用全文索引进行排序的时候,MySQL无法在使用索引排序)。
函数 MATCH() 将返回关键词匹配的相关度,是一个浮点数字。在一个查询中使用两次 MATCH() 函数并不会有额外的消耗,MySQL会自动识别并只进行一次搜索。不过,如果将 MATCH() 函数放到 ORDER BY 子句中,MySQL将会使用文件排序。
在 MATCH() 函数中指定的列必须和在全文索引中指定的列完全相同,否则就无法使用全文索引。这是因为全文索引不会记录关键字是来自那一列的(这也意味着无法使用全文索引来查询某个关键字是否在某一列中存在)。
在布尔搜索中,用户可以自定义某个被搜索的词语的相关性。布尔搜索通过停用词列表过滤掉哪些"噪声"词,除此之外,布尔搜索还要求搜索关键词长度必须大于 ft_min_word_len,同时小于 ft_max_word_len。与自然语言的全文索引不同,布尔全文索引的搜索结果是未经排序的。
布尔搜索通常一些前缀修饰符来定制搜索:
还可以使用其他的操作,例如使用括号分组,构建一些更复杂的搜索查询。这类似于编程语言中的正则表达式模式。示例语句,搜索出既包含词 “factory” 又包含 “casualties” 的记录:
SELECTid, title, RIGHT(description, 25)
FROM sakila.film_text
WHERE MATCH(title, description) AGAINST('+factory +casualties' IN BOOLEAN MODE);
类似的还可以在查询中使用括号进行"短语搜索",让返回结果精确匹配指定的短语:
SELECT id, title, RIGHT(description, 25)
FROM sakila.film_text
WHERE MATCH(title, description) AGAINST('"spirited casualties"' IN BOOLEAN MODE);
短语搜索的速度会比较慢。只使用全文索引是无法判断是否精确匹配短语的,通常还需要查询原文确定记录中是否包含完整的短语。由于需要进行会表过滤,索引速度会很慢。要完成上面的查询,MySQL需要先从索引中找出所有同时包含 “spirited” 和 “casualties” 的索引条目,然后取出这些记录再判断是否是精确匹配短语。
MySQL全文索引中只有一种判断相关性的方法:词频。索引也不会记录索引词在字符串中的位置,索引位置也有无法用在相关性上。MySQL的全文索引也没有提供其它可选的相关性排序算法。
数据量的大小也是一个问题:MySQL的全文索引只有全部在内存中的时候,性能才非常好。如果内存无法状态全部索引,那么搜索速度可能会非常慢(尤其在使用精确短语搜索时)。相比其他的索引类型,"写"操作的代价会更大:
全文索引还会影响查询优化器的工作:
这些限制影响着查询,下面给出一个直观的例子(字段 content 上有全文索引,字段 author 上有普通索引):
...
WHERE MATCH(content) AGAINST ('High Performance MySQL')
AND author = 123;
即使这里 author 字段创建了普通索引,也依然不会被使用。因为这里使用了 MATCH AGAINST,而且恰好上面有全文索引,所以MySQL优先选择使用全文索引,即先搜索所有的文档,查找是否有包含关键词的文档,然后返回记录看看作者是否是 123。所以这里也就没有使用 author 字段上的索引。
一个替代方案是将 author 列包含到全文索引中。当然不是直接包含,在 author 列的值前面附上一个不常见的前缀,然后将这个带前缀的值存放到一个单独的 filters 列中,并单独维护该列(也与可以使用触发器来做维护工作):
...
WHERE MATCH(content, filters)
AGAINST ('High Performance MySQL +author_id_123' IN BOOLEAN MODE);
在这个案例中,如果 author 列的选择性非常高,那么MySQL能够根据作者信息很快地将需要过滤的文档记录限制在一个很小的范围内,这个查询的效率也就会非常好。如果 author 列的选择性很低,那么这个替代方案的效率会比前面的那个更糟,所以使用的时候要谨慎。
虽然只有MyISAM表支持全文索引,但如果仍然希望使用InnoDB或其它引擎,可以将原表复制到一个备库,再将备库上的表改成MyISAM并建上相应的全文索引。如果不希望在另一个服务器上完成查询,还可以对表进行垂直拆分,将需要索引的列放到一个单独的MyISAM表中。
全文索引的日常维护通常能够大大提升性能。“双B-Tree” 的特殊结构、在某些文档中比其他文档要包含多得多的关键字,这都使得全文索引比起普通索引有更多的碎片问题。所以需要经常使用 OPTIMIZE TABLE 来较少碎片。如果应用是 I/O 密集型的,那么定期地进行全文索引重建可以让性能提升很多。
如果希望全文索引能够高效地工作,还需要保证索引缓存足够大,从而保证所有的全文索引都能够缓存在内存中。通常,可以为全文索引设置单独的键缓存(Key cache),保证不会被其他的索引缓存挤出内存。
提供一个好的停用词列表;忽略一些太短的单词(索引单词的最小长度可以通过参数 ft_min_word_len 配置。修改该参数可以过滤更多的单词,让查询速度更快,但是也会降低精确度)。停用词表和允许最小词长都可以通过减少索引词语来提升全文索引的效率,但是同时也会降低搜索的精确度。
这些都需要根据实际的应用场景找到合适的平衡点。如果希望同时获得好的性能和好的搜索质量,那么需要自己定制这些参数。一个好的办法是通过日志系统来研究用户的搜索行为,看看一些异常的查询,包括没有结果返回的查询或者返回过多结果的用户查询。通过这些用户行为和被搜索的内容来判断应该如何调整索引策略。
另外要注意,当调整 “允许最小词长” 后,需要通过 OPTIMIZE TABLE 来重建索引才会生效。另一个参数 ft_max_word_len 也一样。
当向一个有全文索引的表中导入大量数据的时候,最好通过命令 DISABLE KEYS 来禁用全文索引,然后在导入结束后使用 ENABLE KEYS 来建立全文索引。因为全文索引的更新时一个消耗很大的操作,所以上面的细节会帮你节省大量的时间。另外,这样还顺便为全文索引做了一次碎片整理工作。