MySQL(InnoDB剖析):29---全文检索(倒排索引全文索引/全文检索)

Posted 董哥的黑板报

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL(InnoDB剖析):29---全文检索(倒排索引全文索引/全文检索)相关的知识,希望对你有一定的参考价值。

一、引言

演示说明

  • 前面介绍过了,B+树索引的特点是可以通过索引字段的前缀进行查找。例如对于下面的查询B+树是支持的
select * from blog where content like 'xxx%';
  • 但是有时候我们要查询的是博客的内容中含有“xxx”,而不是以“xxx”开头,所以应该是下面的SQL语句
select * from blog where content like '%xxx%';
  • 因此上述SQL语句即便添加了B+树索引也是需要进行索引的扫描来得到结果
  • 全文检索(Full-Text Search)是将存储于数据库中的整本书或整篇文章中的任意内容信息查找出来的技术。它可以根据需要获得全文中有关章、节、段、词等信息,也可以进行各种统计和分析
  • MySQL全文检索历史发展:
    • 在之前的MySQL数据库中,InnoDB不支持全文检索技术。,只有MyISAM存储引擎支持。大多数的用户转向MyISAM存储引擎,还可能需要进行表的拆分,并将需要进行全文检索的数据存储为MyISAM表。这样的确能够解决需求,但是却丧失了InnoDB存储引擎的事务性
    • 从InnoDB 1.2.x版本开始,InnoDB开始支持全文检索,其支持MyISAM存储引擎的全部功能,并且还支持其他一些特性

二、倒排索引

  • 全文检索通常使用倒排索引(inverted index)来实现。倒排索引同B树索引一样,也是一种索引结构
  • 在辅助表(auxiliary table)中存储了单词与单词自身在一个或多个文档中所在位置之间的映射。这通常利用关键数组实现,其拥有两种表现形式:
    • inverted file index:其表现形式为单词,单词所在文档的ID
    • full inverted index:其表现形式为单词,(单词所在文档的ID,在文档中的具体位置)
  • 例如下面是一张表t。其中有DocumentId、Text两个字段

①inverted file index

  • 如果采用inverted file index的关联数组实现,则其存储的内容如下表所示,可以看到:
    • 单词code存在于文档1和4中
    • 单词days存在于文档3和6中
    • 等等......

  • 这样之后,进行全文查询就简单了:直接根据Documents得到包含查询关键字的文档

②full inverted index

  • 如果采用full inverted index的关联数组实现,则其存储的内容如下表所示,可以看到:
    • code单词为(1:6),(4,8):表示code这个单词出现在文档1的第6个单词处,和文档4的第8个单词处
    • 等等......

  • full inverted index还存储了单词所在的位置信息,所以其占用更多的空间,但是能更好地定位数据,并扩充一些其他的搜索特性

三、InnoDB全文检索的实现

  • InnoDB从1.2.x开始支持全文检索的技术,其采用“full inverted index”的方式
  • 在InnoDB存储引擎中,将(DocumentId,Position)视为一个“ilist”。因此在全文检索的表(辅助表,见下)中,有两个列:
    • 一个是word字段。在word字段上有设有索引
    • 另一个是ilist字段
  • 此外,由于InnoDB存储引擎ilist字段中存放了Position信息,故可以进行Proximity Search,而MyISAM存储引擎不支持该特性
  • 当前InnoDB的全文索引还存在以下的限制:
    • 每张表只能有一个全文检索的索引
    • 由多列组合而成的全文检索的索引列必须使用相同的字符集与排序规则
    • 不支持没有单词界定符(delimiter)的语言,如中文、日语、韩语等

Auxiliary Table(辅助表)

  • 正如前面所说的,倒排索引需要将word存放到一张表中,这个表称为Auxiliary Table(辅助表)
  • 在InnoDB存储引擎中,为了提高全文检索的并行性能,共有6张Auxiliary Table,目前每张表根据word的Latin编码进行分区
  • Auxiliary Table是持久的表,存放于磁盘上

innodb_ft_aux_table参数(辅助表的查看)

  • InnoDB允许用户查看指定倒排索引的Auxiliary Table中分词的信息,可以通过这个参数来观察倒排索引的Auxiliary Table
  • 例如下面的SQL语句设置查看test数据库下表fts_a的Auxiliary Table:
set global innodb_ft_aux_table='test/fts_a';

  • 设置之后,就可以通过查看information_schema数据库下的innodb_ft_index_table表来得到表fts_a中的分词信息

FTS Index Cache(全文检索索引缓存)

  • 然而在InnoDB存储引擎的全文索引中,还有另外一个重要的概念FTS Index Cache(全文检索索引缓存),其用来提高全文检索的性能
  • FTS Index Cache是一个红黑树结构,其根据(word,ilist)进行排序
  • 这意味着插入的数据已经更新了对应的表,但是对全文索引的更新可能在分词操作后还在FTS Index Cache中,Auxiliary Table可能还没有更新
  • FTS Index Cache的更新:
    • InnoDB会批量对FTS Index Cache进行更新,而不是每次插入后更新一次Auxiliary Table
    • 对全文检索进行查询时,Auxiliary Table首先会将在FTS Index Cache中对应的word字段合并到Auxiliary Table中,然后再进行查询
  • 这种合并(merge)操作非常类似之前介绍的Insert Buffer的功能,不同的是Insert Buffer是一个持久的对象,并且其是B+树结构。然而FTS Index Cache的作用又和Insert Buffer是类似的,它提高了InnoDB存储引擎的性能并且由于其根据红黑树排序后进行批量插入,其产生的Auxiliary Table相对较小

innodb_ft_cache_size参数

  • 该参数用来控制FTS Index Cache的大小,默认值为32M
  • 当该缓存满时,会将其中的(word,ilist)分词信息同步到磁盘的Auxiliary Table中
  • 增大该参数可以提高全文检索的性能,但是在宕机时,未同步到磁盘中的索引信息可能需要更长的时间进行恢复

事务提交时FTS Index Cache的更新

  • 对于其他数据库,例如Oracle 11g,用户可以选择手动在事务提交时,或者固定间隔时间将倒排索引的更新刷新到磁盘
  • 对于InnoDB来说,其总是在事务提交时将分词写入到FTS Index Cache。然后通过批量更新写入到磁盘。虽然InnoDB通过一种延时的、批量的写入方式来提高数据库的性能,但是上述操作仅在事务提交时发生

数据库关闭时、宕机时FTS Index Cache与Auxiliary Table的更新

  • 数据库关闭时:在FTS Index Cache中的数据库会同步到磁盘上的Auxiliary Table中
  • 数据库宕机时:一些FTS InDEX Cache中的数据库可能未被同步到磁盘上。那么下次重启时,当用户对表进行全文检索(查询或者插入操作)时,InnoDB会自动读取未完成的文档,然后进行分词操作,再将分词的结果放入到FTS Index Cache中

FTS Document ID(FTS_DOC_ID列)

  • FTS Document ID是另外一个重要的概念
  • 在InnoDB存储引擎中,为了支持全文检索,必须有一个列与word进行映射:
    • 在InnoDB中这个列被命名为FTS_DOC_ID
    • 类型必须是bigint unsigned not null
    • 并且InnoDB自动会在该列上加入一个名为FTS_DOC_ID_INDEX的unique index索引
  • 上述这些操作都是由InnoDB存储引擎自动完成,用户也可以在建表时自动添加FTS_DOC_ID,以及相应的Unique Index。由于列名为FTS_DOC_ID的列具有特殊意义,因此创建时必须注意相应的类型,否则mysql数据库会抛出错误
  • 例如下面自己手动创建一个FTS_DOC_ID列,但类型是int,而非bigint,因此抛出了错误
create table fts_a(
    FTS_DOC_ID INT UNSIGNED AUTO_INCREMENT NOT NULL,
    body TEXT,
    primary key(FTS_DOC_ID)
);

  • 当修改FTS_DOC_ID列的类型之后就可以创建成功了
create table fts_a(
    FTS_DOC_ID BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
    body TEXT,
    primary key(FTS_DOC_ID)
);

Deleted auxiliary table

  • 文档中分词的插入操作是在事务提交时完成的,然而对于删除操作,其在事务提交时不删除磁盘Auxiliary Table中的记录,而只是删除FTS Index Cache中的记录。对于Auxiliary Table中被删除的记录,InnoDB会记录其FTS Document ID,并将其保存在Deleted auxiliary table中
  • 在设置参数innodb_ft_aux_table后,用户同样可以访问information_schema数据库下的表innodb_ft_being_deleted观察删除的FTS Document ID

OPTIMIZE TABLE命令

  • 由于文档的DML操作实际并不删除索引中的数据,相反还会在对应的DELETED表中插入记录,因此随着应用程序的允许,索引会变得非常大,即使索引列中的有些数据已经被删除,查询也不会使用到。为了,InnoDB存储引擎提供了一种方式,允许用户手动地将已删除的记录从索引中彻底删除,该命令就是OPTIMIZE TABLE
ooptimize table table_name;

innodb_optimize_fulltext_only参数

  • OPTIMIZE TABLE命令还会进行一些其他操作,如Cardinality的重新统计,若用户希望仅对倒排索引进行操作,那么可以通过这个参数进行设置
  • 例如:
set global innodb_optimize_fulltext_only=1;

optimize table table_name;

innodb_ft_num_word_optimize参数

  • 若被删除的文档非常多,那么OPTIMIZE TABLE操作可能需要占用非常多的时间,这会影响到应用程序的并发性,并极大地降低用户的响应时间
  • 用户可以根据这个参数来限制每次实际删除的分词数量。该参数默认值为2000

stopword列表

  • stopword列表,其表示该列表中的word不需要对其进行索引分词操作。例如,对于the这个单词,由于不具有具体的意义,因此将其视为stopword
  • InnoDB默认有一张stopword列表,其在information_schema数据库下,表名为innodb_ft_default_stopword,默认共有36个stopword

innodb_ft_server_stopword_table参数

  • 用户可以使用这个参数来自定义stopword列表
  • 例如:用户在test数据库下创建一个名为user_stopword的表,将该表设为stopword表
create table user_stopword(
    value varchar(30)
)ENGINE=InnoDB;

set global innodb_ft_server_stopword_table="test/user_stopword";

演示案例

  • 创建一个表fts_a:
create table fts_a(
    FTS_DOC_ID BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
    body TEXT,
    primary key(FTS_DOC_ID)
);

  • 插入一些数据
insert into fts_a select NULL,'Pease porridge in the pot';

insert into fts_a select NULL,'Pease porridge hot,pease porridge cold';

insert into fts_a select NULL,'Nine days old';

insert into fts_a select NULL,'Some like it hot,some like it cold';

insert into fts_a select NULL,'Nine days old';

insert into fts_a select NULL,'I like code days';

  • 由于body字段是进行全文检索的字段,因此创建一个类型为FULLTEXT的索引
create fulltext index idx_fts on fts_a(body);

  • 查看数据
select * from fts_a;

  • 设置参数innodb_ft_aux_table,然后在表中查看分词对应的信息:
    • 每个word都对应一个DOC_ID和POSITION
    • 此外,还记录了FITST_DOC_ID、LAST_DOC_ID以及DOC_COUNT。分别代表了该word第一次出现的文档ID、最后一次出现的文档ID、以及该word在多少个文档中存在
-- fts_a表在数据库demo下
set global innodb_ft_aux_table='demo/fts_a';

select * from information_schema.innodb_ft_index_table;

  • 若执行下面的语句可以删除FTS_DOC_ID为6的文档
delete from demo.fts_a where FTS_DOC_ID=6;

  • 前面说过,InnoDB并不会直接删除索引中对应的记录,而是将删除的文档ID插入到DELETED表,因此用户可以进行如下的查询:
select * from information_schema.innodb_ft_deleted;

  • 可以看到删除的文档ID插入到了表innodb_ft_deleted表中,若用户想要彻底删除倒排索引中该文档的分词信息,可以运行下面的语句:
set global innodb_optimize_fulltext_only=1;

optimize table demo.fts_a;

  • 若运行optimize table命令之后可以将记录彻底删除,并且彻底删除的文档ID会记录到表innodb_ft_being_deleted中:
select * from information_schema.innodb_ft_deleted;

select * from information_schema.innodb_ft_being_deleted;

  • 此外,由于6这个文档ID已经被删除了,所以不允许再次插入这个文档ID,否则数据库会抛出下面的异常
insert into demo.fts_a select 6,'I like this days';

innodb_ft_min_token_size、innodb_ft_max_token_size参数

  • 这两个参数控制InnoDB查询字符的长度:
  • 当长度小于innodb_ft_min_token_size,或者长度大于innodb_ft_max_token_size时,会忽略该词的搜索
  • 在InnoDB中,参数innodb_ft_min_token_size的默认值为3,参数innodb_ft_max_token_size的默认值为84

四、全文检索的语法

  • 全文检索的查询,其语法如下:
    • MATCH:指定了需要被查询的列
    • AGAINST:指定了使用何种方法来进行查询
    • 查询模式NATURAL LANGUAGE、BOOLEAN MODE、QUERY EXPANSION在下面介绍

Natural Language查询模式

  • 此模式为全文检索的默认模式,表示查询带有指定word的文档

演示案例:

  • 例如对上面创建的表fts_a,查询body字段中带有Pease的文档,若不使用全文检索技术,使用下述SQL语句来进行查询:
    • 显然下述SQL语句不能使用B+树检索
select * from fts_a where body like '%Pease%';

  • 如果使用全文检索技术,查询body中带有Porridge的文档,则SQL语句如下:
select * from fts_a where match(body) against ('Porridge' in natural language mode);

-- 由于NATURAL LANGUAGE MODE是默认的全文检索模式,因此用户可以简略为下述SQL语句
select * from fts_a where match(body) against ('Porridge');

  • 观察上述SQL语句的查询计划,可得:
    • 在type列显示了fulltext,即表示全文检索的倒排索引
    • key列显示了idx_fs,表示索引的名字
explain select * from fts_a where match(body) against ('Porridge');

  •  在where条件中使用MATCH函数,查询返回的结果是根据相关性进行降序排序的,即相关性最高的结果放在第一位。相关性的值是一个非负的浮点数字,0表示没有任何的相关性。根据MySQL官方文档可知,其相关性的计算依据以下4个条件:
    • word是否在文档中出现
    • word在文档中出现的次数
    • word在索引列中的数量
    • 多少个文档包含该word
  • 在上述查询中,由于Porridge在文档2种出现了两次,因而具有更高的相关性,故第一个显示
  • 为了统计MATCH函数得到的结果数量,可以执行下面的SQL语句:
select count(*) from fts_a where match(body) against ('Porridge' in natural language mode);

  • 上述SQL语句也可以更改为下面的形式:
select count(if(match(body) against ('Porridge' in natural language mode),1,NULL))
as count from fts_a;

 

  • 上述两个SQL语句虽然得到的逻辑结果是相同的,但是从内部运行来看,第二句SQL的执行速度更快些。因为第一句SQL语句还需要进行相关性的排序统计,而在第二句SQL中是不需要的
  • 此外,用户可以通过SQL语句查看相关性:
select fts_doc_id,body,match(body) against ('Porridge' in natural language mode)
as Relevance from fts_a;

  • 对于InnoDB的全文检索,还需要考虑以下的因素:
    • 查询的word在stopword列中,忽略该字符串的查询
    • 查询的word的字符长度是否在区间[innodb_ft_min_tokennnnnnn_size,innodb_ft_max_token_size]内
  • 如果词在stopword中,则不对该词进行查询。例如下面对the这个词进行查询,结果如下:
    • the虽然在文档1中出现,但由于其是stopword,故其相关性为0
select fts_doc_id as id,body,match(body) against('the' in natural language mode)
as r1 from fts_a;

 

Boolean查询模式

  • 当使用该修饰符时,查询字符串的前后字符会有特殊的含义
  • Boolean全文检索支持以下几种操作符:
    • +:表示该word必须存在
    • 1:表示该word必须被排除
    • (no operator):表示该word是可选的,但是如果出现,其相关性会更高
    • @distance:表示查询的多个单词之间的距离是否在distance之间,distance的单位是字节。这种全文检索的查询也称为Proximity Search。如MATCH(body) AGAINST ('"Pease pot"@30' IN BOOLEAN MODE)表示字符串Pease和pot之间的距离需在30字节内
    • >:表示出现该单词时增加相关性
    • <:表示出现该单词时降低相关性
    • ~:表示允许出现该单词,但是出现时相关性为负(全文检索查询允许负相关性)
    • *:表示以该单词开头的单词,如lik*,表示可以是lik、like,或者likes
    • '':表示短语

演示案例

  • 以上面的fts_a表为例。下面的SQL语句返回有pease又有hot的文档:
select * from fts_a where match(body) against ('+Pease +hot' in boolean mode);

  • 下面的SQL语句返回有pease但没有hot的文档:
select * from fts_a where match(body) against ('+Pease -hot' in boolean mode);

  • 下面的语句返回有pease或有hot的文档
select * from fts_a where match(body) against ('Pease hot' in boolean mode);

  • 下面的语句进行Proximity Search(有错,待续):
    • 下面的Pease与pot距离为22字节,照理说第二个SQL语句应该显示为空,但是还是xianshi
select fts_doc_id,body from fts_a where match(body) against ('"Pease pot" @30' in boolean mode);

select fts_doc_id,body from fts_a where match(body) against ('"Pease pot" @10' in boolean mode);

  • 下面的语句根据是否有单词like或pot进行相关性统计,并且出现单词pot后相关性需要增加。文档4虽然出现两个like单词,但是没有pot,因此相关性没有文档1和文档8高
select fts_doc_id,body,match(body) against ('like>pot' in boolean mode)
as Relevance from fts_a;

  • 下面的查询增加了“<some”的条件,最后得到的结果为:
    • 可以看到文档4变为了负,因为其中存在like单词,也存在some单词
select fts_doc_id,body,match(body) against ('like>pot <some' in boolean mode)
as Relevance from fts_a;

  • 接着运行下面的语句,查询文档中以“po”开头的单词:
select * from fts_a where match(body) against('po*' in boolean mode);

  • 下面的SQL语句查询关于短语,例如:
    • 第一条SQL语句没有使用" "将like和hot视为一个短语,而只是将其视为两个单词,因此结果共返回4个文档
    • 第二条SQL语句使用“like hot”,因此查询的是短语,故仅文档9符合查询条件
select * from fts_a where match(body) against ('like hot' in boolean mode);

select * from fts_a where match(body) against ('"like hot"' in boolean mode);

Query Expansion查询模式

  • 这种查询通常在查询的关键词太短,用户需要implied knowledge(隐含知识)时进行
  • 例如,对于单词“database”的查询,用户可能希望查询的不仅仅是包含database的文档,可能还指哪些包含MySQL、Oracle、DB2、RDBMS的单词。而这时可以使用Query Expansion模式来开启全文检索的implied knowledge
  • 通过在查询短语中添加WITH QUERY EXOANSION或IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION可以开启blind query expansion(又称为automatic relevance feedback)
  • 该查询分为两个阶段:
    • 第一阶段:根据搜索的单词进行全文索引查询
    • 第二阶段:根据第一阶段产生的分词再进行一次全文检索的查询

演示案例

  • 创建一个测试表articles:
    • 在这个表中没有显示创建FTS_DOC_ID列,因此InnoDB会自动创建该列,并添加唯一索引
    • 此外,表的全文检索索引是根据列title和body的联合索引

  • 接着根据database关键字进行的全文检索查询。可以看到,查询返回了3条记录,body字段包含database关键字

  • 接着开启Query Expansion,观察最后得到的结果如下:
    • 可以看到最后得到8条结果,除了之前包含database的记录,也有包含title或body字段中包含MySQL、DB2的文档。这就是Query Expansion

  • 由于Query Expansion的全文检索可能带来许多非相关性的查询,因此在使用时,需要非常谨慎

以上是关于MySQL(InnoDB剖析):29---全文检索(倒排索引全文索引/全文检索)的主要内容,如果未能解决你的问题,请参考以下文章

mySql---剖析InnoDB索引原理

MySQL中InnoDB全文检索

深入剖析 MySQL 自增锁

Mysql INNODB和MyIsAM实现全文检索

Mysql系列——详细剖析数据库中的存储引擎

MySql5.7InnoDB全文索引(针对中文搜索)