【MySQL技术内幕】32-全文检索

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shenchaohao12321/article/details/83421705

1、概述

全文检索( Full-Text Search)是将存储于数据库中的整本书或整篇文章中的任意内容信息查找出来的技术。它可以根据需要获得全文中有关章、节、段、句、词等信息,也可以进行各种统计和分析。
在之前的 MySQL数据库中, InnoDB存储引擎并不支持全文检索技术。大多数的用户转向 MyISAM存储引擎,这可能需要进行表的拆分,并将需要进行全文检索的数据存储为 MyISAM表。这样的确能够解决逻辑业务的需求,但是却丧失了 InnoDB存储引擎的事务性,而这在生产环境应用中同样是非常关键的。
从 InnoDB1.2.x版本开始, InnoDB存储引擎开始支持全文检索,其支持 MyISAM存储引擎的全部功能,并且还支持其他的一些特性。

2、倒排索引

全文检索通常使用倒排索引(inverted index)来实现。倒排索引同B+树索引一样,也是一种索引结构。它在辅助表(auxiliary table)中存储了单词与单词自身在一个或多个文档中所在位置之间的映射。这通常利用关联数组实现,其拥有两种表现形式:

  • inverted file index,其表现形式为{单词,单词所在文档的ID}
  • full inverted index,其表现形式为{单词,(单词所在文档的⑩,在具体文档中的位置)

例如,对于下面这个例子,表t存储的内容如表所示。

全文检索表t
DocumentId Text
1 Pease porridge hot, pease porridge cold
2 Pease porridge in the pot
3 Nine days old
4 Some like it hot, some like it cold
5 Some like it in the pot
6 Nine days old

DocumentId表示进行全文检索文档的ld,Text表示存储的内容,用户需要对存储的这些文档内容进行全文检索。例如,查找出现过Some单词的文档ld,又或者查找单个文档中出现过两个Some单词的文档ld,等等。
对于 inverted file index的关联数组,其存储的内容如下表所示。

inverted file index的关联数组
Number Text Documents Number Text Documents
1 code 1,4 8 old 3,6
2 days 3,6 9 pease 1,2
3 hot 1,4 10 prridge 1,2
4 in 2,5 11 pot 2,5
5 it 4,5 12 some 4,5
6 like 4,5 13 the 2,5
7 nine 3.6      

可以看到单词code存在于文档1和4中,单词days存在与文档3和6中。之后再要进行全文查询就简单了,可以直接根据 Documents得到包含查询关键字的文档。对于 inverted file index,其仅存取文档ld,而 full inverted index存储的是对(pair),即(DocumentId,Position),因此其存储的倒排索引如下表所示。

full inverted index的关联数组
Number Text Documents Number Text Documents
1 code (1:6),(4:8) 8 old (3:3),(6:3)
2 days (3:2),(6:2) 9 pease (1:1,4),(2:1)
3 hot (1:3),(4:4) 10 prridge (1:2,5),(2:2)
4 in (2:3),(5:4) 11 pot (2:5),(5:6)
5 it (4:3,7),(5:3) 12 some (4:1,5),(5:1)
6 like (4:2,6),(5:2) 13 the (2:4),(5:5)
7 nine (3:1),(6:1)      

full inverted index还存储了单词所在的位置信息,如code这个单词出现在(1:6),即文档1的第6个单词为code。相比之下, full nverted index占用更多的空间,但是能更好地定位数据,并扩充一些其他的搜索特性。

3、InnoDB全文检索

InnoDB存储引擎从1.2.x版本开始支持全文检索的技术,其采用 full inverted index的方式。在 InnoDB存储引擎中,将( DocumentId, Position)视为一个“ilist”。因此在全文检索的表中,有两个列,一个是word字段,另一个是itst字段,并且在word字段上有设有索引。此外,由于 InnoDB存储引擎在ilist字段中存放了 Position信息,故可以进行 Proximity Search,而 MyISAM存储引擎不支持该特性。
正如之前所说的那样,倒排索引需要将word存放到一张表中,这个表称为Auxiliary Table(辅助表)。在 InnoDB存储引擎中,为了提高全文检索的并行性能,共有6张 Auxiliary Table,目前每张表根据word的 Latin编码进行分区。
Auxiliary Table是持久的表,存放于磁盘上。然而在 InnoDB存储引擎的全文索引中,还有另外一个重要的概念 FTS Index Cache(全文检索索引缓存),其用来提高全文检索的性能。
FTS Index Cache是一个红黑树结构,其根据(word,ist)进行排序。这意味着插入的数据已经更新了对应的表,但是对全文索引的更新可能在分词操作后还在 FTS Index Cache中, Auxiliary Table可能还没有更新。 InnoDB存储引擎会批量对 Auxiliary Table进行更新,而不是每次插入后更新一次 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存储引擎允许用户查看指定倒排索引的 Auxiliary Table中分词的信息,可以通过设置参数 innodb ft aux table来观察倒排索引的 Auxiliary Table。下面的SQL语句设置查看test架构下表ftsa的 Auxiliary Table:

mysql>SET GLOBAL innodb_ft_aux_table='test/fts_a';
Query OK, 0 rows affected (0.00 sec)

在上述设置完成后,就可以通过查询 information schema架构下的表 INNODB_FT_INDEX_TABLE得到表fts_a中的分词信息。
对于其他数据库,如 Oracle 11g,用户可以选择手工在事务提交时,或者固定间隔时间时将倒排索引的更新刷新到磁盘。对于InnoDB存储引擎而言,其总是在事务提交时将分词写人到 FTS Index Cache,然后再通过批量更新写入到磁盘。虽然 InnoDB存储引擎通过一种延时的、批量的写入方式来提高数据库的性能,但是上述操作仅在事务提交时发生。
当数据库关闭时,在 FTS Index Cache中的数据库会同步到磁盘上的 Auxiliary Table中。然而,如果当数据库发生宕机时,一些 FTS Index Cache中的数据库可能未被同步到磁盘上。那么下次重启数据库时,当用户对表进行全文检索(查询或者插入操作)时,InnoDB存储引擎会自动读取未完成的文档,然后进行分词操作,再将分词的结果放入到FTS Index Cache中。
参数 innodb ft cache size用来控制 FTS Index Cache的大小,默认值为32M。当该缓存满时,会将其中的(word,i分词信息同步到磁盘的 Auxiliary Table中。增大该参数可以提高全文检索的性能,但是在宕机时,未同步到磁盘中的索引信息可能需要更长的时间进行恢复。
FTS Document ID是另外一个重要的概念。在 InnoDB存储引擎中,为了支持全文检索,必须有一个列与word进行映射,在 InnoDB中这个列被命名为 FTS_DOC_ID,其类型必须是 BIGINT UNSIGNED NOT NULI,并且 InnoDB存储引擎自动会在该列上加入一个名为FTS_DOC_ID_INDEX的Unique Index。上述这些操作都由 InnoDB存储引擎自己完成,用户也可以在建表时自动添加 FTS_DOC_ID,以及相应的 Unique Index。
由于列名为 FTS_DOC_ID的列具有特殊意义,因此创建时必须注意相应的类型,否则MySQL数据库会抛出错误,如:

mysql> CREATE TABLE fts_a(
FTS_DOC_ID INT UNSIGNED AUTO_INCREMENT NOT NULL,
body TEXT,
PRIMARY KEY (FTS_DOC_ID)
);
ERROR 1166(42000): Incorrect column name ' FTS_DOC_ID'

可以看到,由于用户手动定义的列 FTS_DOC_ID的类型是INT,而非 BIG INT,因此在创建的时候抛出了 Incorrect column name FTS_DOC_ID,因此需将该列修改为对应的数据类型。
文档中分词的插入操作是在事务提交时完成,然而对于删除操作,其在事务提交时,不删除磁盘 Auxiliary Table中的记录,而只是删除 FTS Cache Index中的记录。对于 Auxiliary Table中被删除的记录, InnoDB存储引擎会记录其 FTS Document ID,并将其保存在 DELETED auxiliary table中。在设置参数 innodb_ft_aux_table后,用户同样可以访问 information schema架构下的表 INNODB_FT_DELETED来观察删除的FTS Document Id。
由于文档的DML操作实际并不删除索引中的数据,相反还会在对应的 DELETED表中插入记录,因此随着应用程序的允许,索引会变得非常大,即使索引中的有些数据已经被删除,查询也不会选择这类记录。为此, InnoDB存储引擎提供了一种方式,允许用户手工地将已经删除的记录从索引中彻底删除,该命令就是 OPTIMIZE TABLE。因为OPTIMIZE TABLE还会进行一些其他的操作,如 Cardinality的重新统计,若用户希望仅对倒排索引进行操作,那么可以通过参数innodb_optimize_fulltext_only进行设置,如:

mysql>SET GLOBAL innodb_optimize_fulltext_only=1;
mysql>OPTIMIZE TABLE fts_a;

若被删除的文档非常多,那么 OPTIMIZE TABLE操作可能需要占用非常多的时间,这会影响应用程序的并发性,并极大地降低用户的响应时间。用户可以通过参数 innodb_ft_num_word_optimize来限制每次实际删除的分词数量。该参数的默认值为2000。
下面来看一个具体的例子,首先通过如下代码创建表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,'Some like it in the pot';
INSERT INTO fts_a SELECT NULL, 'Nine days old';
INSERT INTO fts_a SELECT NULL,'I like code days';
CREATE FULLTEXT INDEX idx_fts ON fts_a(body);

上述代码创建了表fsa,由于body字段是进行全文检索的字段,因此创建一个类型为 FULLTEXT的索引。这里首先导入数据,然后再进行倒排索引的创建,这也是比较推荐的一种方式。
通过设置参数 innodb_ft_aux_table来查看分词对应的信息:

mysql> SET GLOBAL innodb_ft_aux_table='test/fts_a';
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM information_schema.INNODB_FT_INDEX_TABLE;

可以看到每个word都对应了一个 DOC_ID和POSITION。此外,还记录了 FIRST_DOC_ID、LAST_DOC_ID以及DOC_COUNT,分别代表了该word第一次出现的文档ID,最后一次出现的文档ID,以及该word在多少个文档中存在。
若这时执行下面的SQL语句,会删除FTS_ DOC_ID为7的文档:

mysql> DELETE FROM test fts_a WHERE FTS_DOC_ID=7;
Query OK, 1 row affected (0.00 sec)

由于之前的介绍, InnoDB存储引擎并不会直接删除索引中对应的记录,而是将删除的文档ID插人到 DELETED表,因此用户可以进行如下的查询:

mysql> SELECT FROM INNODB FT DELETED;
DOC_ID
7
1 row in set (0.00 sec)

可以看到删除的文档ID插人到了表 INNODB_FT_DELETED中,若用户想要彻底删除倒排索引中该文档的分词信息,那么可以运行如下的SQL语句:

通过上面的例子可以看到,运行命令 OPTIMIZE TABLE可将记录进行彻底的删除,并且彻底删除的文档ID会记录到表 INNODB_FT_BEING_DELETED中。此外,由于7这个文档ID已经被删除,因此不允许再次插人这个文档ID,否则数据库会抛出如下异常:

mysql> INSERT INTO test.fts_a select 7,'I like this days';
ERROR 182 (HY000): Invalid InnoDB FTS Doc ID

stopword列表( stopword list)是本小节最后阐述的一个概念,其表示该列表中的word不需要对其进行索引分词操作。例如,对于the这个单词,由于其不具有具体的意义,因此将其视为 stopword。 InnoDB存储引擎有一张默认的 stopword列表,其在 information schema架构下,表名为 INNODB_FT_DEFAULT_STOPWORD,默认共有36个 stopword。此外用户也可以通过参数 innodb_ft_server_stopword_table来自定义stopword列表。如:

mysql> CREATE TABLE user_stopword(
value VARCHAR(30)
) ENGINE INNODB;
Query OK, 0 rows affected (0.03 sec)
mysql> SET GLOBAL innodb_ft_server_stopword_table ="test/user_stopword";
Query OK, 0 rows affected (0.00 sec)

当前 InnoDB存储引擎的全文检索还存在以下的限制:

  • 每张表只能有一个全文检索的索引。
  • 由多列组合而成的全文检索的索引列必须使用相同的字符集与排序规则
  • 不支持没有单词界定符( delimiter)的语言,如中文、日语、韩语等。

4、全文检索

MySQL数据库支持全文检索( Full-Text Search)的查询,其语法为:

MySQL数据库通过 MATCH()… AGAINST()语法支持全文检索的查询, MATCH指定了需要被查询的列, AGAINST指定了使用何种方法去进行查询。下面将对各种查询模式进行详细的介绍。

1、Natural Language

全文检索通过 MATCH函数进行查询,默认采用 Natural Language模式,其表示查询带有指定word的文档。对于创建的表fts_a,查询body字段中带有 Pease的文档,若不使用全文索引技术,则允许使用下述SQL语句

mysql> SELECT * FROM fts_a WHERE body LIKE '%Pease%';

显然上述SQL语句不能使用B+树索引。若采用全文检索技术,可以用下面的SQL语句进行查询:

由于 NATURAL LANGUAGE MODE是默认的全文检索查询模式,因此用户可以省略查询修饰符,即上述SQL语句可以写为:

SELECT FROM fts_a WHERE MATCH(body) AGAINST ('Porridge');

观察上述SQL语句的查询计划,可得:

可以看到,在vype这列显示了 fulltext,即表示使用全文检索的倒排索引,而key这列显示了 idx_fts,表示索引的名字。可见上述查询使用了全文检索技术。同时,若表没有创建倒排索引,则执行 MATCH函数会抛出类似如下错误:

在WHRE条件中使用 MATCH函数,查询返回的结果是根据相关性(Relevance)进行降序排序的,即相关性最高的结果放在第一位。相关性的值是一个非负的浮点数字,0表示没有任何的相关性。根据MSQL官方的文档可知,其相关性的计算依据以下四个条件:

  • word是否在文档中出现。
  • wod在文档中出现的次数
  • word在索引列中的数量
  • 多少个文档包含该word

对于上述查询,由于 Porridge在文档2中出现了两次,因而具有更高的相关性,故第一个显示。
为了统计 MATCH函数得到的结果数量,可以使用下列SQL语句:

上述SQL语句也可以重写为:

 

上述两句SQL语句虽然得到的逻辑结果是相同的,但是从内部运行来看,第二句SQL的执行速度可能更快些。这是因为第一句SQL语句还需要进行相关性的排序统计,而在第二句SQL中是不需要的。
此外,用户可以通过SQL语句查看相关性:

对于 InnoDB存储引擎的全文检索,还需要考虑以下的因素:

  • 查询的word在stopword列中,忽略该字符串的查询。
  • 查询的word的字符长度是否在区间[ innodb ft min token size, innodb_ft_max_token_size]内。

如果词在 stopword中,则不对该词进行查询,如对the这个词进行查询,结果如下所示:

可以看到,the虽然在文档1、5中出现,但由于其是 stopword,故其相关性为0参数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。

2、Boolean

MySQL数据库允许使用 IN BOOLEAN MODE修饰符来进行全文检索。当使用该修饰符时,査询字符串的前后字符会有特殊的含义,例如下面的语句要求查询有字符串Pease但没有hot的文档,其中+和-分别表示这个单词必须出现,或者一定不存在。

Boolean全文检索支持以下几种操作符:

  • +表示该word必须存在。
  • -表示该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。
  • "表示短语。

接着将根据上述的操作符及之前创建的表tsa来进行具体的介绍。下面的SQL语句返回有 pease又有hot的文档:

 

可以看到文档1中单词 Pease和pot的距离为22字节,因此第一条@30的查询可以返回结果,而之后@10的条件不能返回任何结果。如:

上述SQL语句查询根据是否有单词like或pot进行相关性统计,并且出现单词pot后相关性需要增加。文档4虽然出现两个like单词,但是没有pot,因此相关性没有文档1和5高。
下面的査询增加了“<some”的条件,最后得到的结果:

可以发现文档5的相关性变为了负,这是因为虽然其中存在like单词,但是也存在some单词,所以根据查询条件,其相关性变为了负相关。
接着来看下面的SQL语句:

可以看到最后结果中的文档包含以po开头的单词,如 porridge,pot。
最后是关于短语的SQL查询,如:

可以看到第一条SQL语句没有使用""将like和hot视为一个短语,而只是将其视为两个单词,因此结果共返回4个文档。而第二条SQL语句使用"like hot'",因此查询的是短语,故仅文档4符合查询条件。

3、Query Expansion

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

  • 第一阶段:根据搜索的单词进行全文索引查询。
  • 第二阶段:根据第一阶段产生的分词再进行一次全文检索的查询。

接着来看一个具体的例子,首先根据如下代码创建测试表 articles:

在这个例子中,并没有显示创建 FTS_DOC_ID列,因此 InnoDB存储引擎会自动建立该列,并添加唯一索引。此外,表 articles的全文检索索引是根据列ite和body的联合索引。接着根据 database关键字进行的全文检索查询。

可以看到,查询返回了3条记录,body字段包含 database关键字。接着开启 Query Expansion,观察最后得到的结果如下所示

可以看到最后得到8条结果,除了之前包含 database的记录,也有包含title或body字段中包含MySQL、DB2的文档。这就是 Query Expansion。
由于 Query Expansion的全文检索可能带来许多非相关性的查询,因此在使用时,用户可能需要非常谨慎。
 

猜你喜欢

转载自blog.csdn.net/shenchaohao12321/article/details/83421705