MySQL InnoDB搜索索引的Stopwords

InnoDB搜索索引的Stopwords
InnoDB的默认禁止词列表相对较短,因为来自技术、文学和其他来源的文档经常使用短词作为关键字或重要短语。例如,你可能搜索“是”或“不是”,并期望得到一个合理的结果,而不是让所有这些词都被忽略
InnoDB默认的stopword列表可以通过查询INFORMATION_SCHEMA查看。INNODB_FT_DEFAULT_STOPWORD表。

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD;
+-------+
| value |
+-------+
| a     |
| about |
| an    |
| are   |
| as    |
| at    |
| be    |
| by    |
| com   |
| de    |
| en    |
| for   |
| from  |
| how   |
| i     |
| in    |
| is    |
| it    |
| la    |
| of    |
| on    |
| or    |
| that  |
| the   |
| this  |
| to    |
| was   |
| what  |
| when  |
| where |
| who   |
| will  |
| with  |
| und   |
| the   |
| www   |
+-------+
36 rows in set (0.01 sec)

为了对所有InnoDB表定义了一个自定义的stopword列表,那么使用与innodb_ft_default_stopword表相同的结构来定义你自定义的stopword表,然后向表中插入stopwords,并且在创建全文索引之前以db_name/table_name的形式设置innodb_ft_server_stopword_table选项的值。自定义的stopword表必须有一个varchar类型的value列。下面的例子演示了如何为innodb创建一个新的全局stopword表。

mysql> CREATE TABLE my_stopwords(value VARCHAR(30)) ENGINE = INNODB;
Query OK, 0 rows affected (0.21 sec)


mysql> INSERT INTO my_stopwords(value) VALUES ('Ishmael');
Query OK, 1 row affected (0.12 sec)


mysql> CREATE TABLE opening_lines (
    -> id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
    -> opening_line TEXT(500),
    -> author VARCHAR(200),
    -> title VARCHAR(200)
    -> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.14 sec)


mysql> INSERT INTO opening_lines(opening_line,author,title) VALUES
    -> ('Call me Ishmael.','Herman Melville','Moby-Dick'),
    -> ('A screaming comes across the sky.','Thomas Pynchon','Gravity\'s Rainbow'),
    -> ('I am an invisible man.','Ralph Ellison','Invisible Man'),
    -> ('Where now? Who now? When now?','Samuel Beckett','The Unnamable'),
    -> ('It was love at first sight.','Joseph Heller','Catch-22'),
    -> ('All this happened, more or less.','Kurt Vonnegut','Slaughterhouse-Five'),
    -> ('Mrs. Dalloway said she would buy the flowers herself.','Virginia Woolf','Mrs. Dalloway'),
    -> ('It was a pleasure to burn.','Ray Bradbury','Fahrenheit 451');
Query OK, 8 rows affected (0.04 sec)
Records: 8  Duplicates: 0  Warnings: 0



mysql> SET GLOBAL innodb_ft_server_stopword_table = 'mysql/my_stopwords';
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE FULLTEXT INDEX idx ON opening_lines(opening_line);
Query OK, 0 rows affected, 1 warning (0.91 sec)
Records: 0  Duplicates: 0  Warnings: 1

默认情况下,长度小于3个字符或大于84个字符的单词不会出现在InnoDB全文搜索索引中。最大和最小字长值可以通过innodb_ft_max_token_size和innodb_ft_min_token_size变量进行配置。这种默认行为不适用于ngram解析器插件。ngram令牌大小由
ngram_token_size选项定义。

通过查询INFORMATION_SCHEMA.INNODB_FT_INDEX_TABLE中的单词,验证指定的stopword (‘Ishmael’)没有出现。

mysql> SET GLOBAL innodb_ft_aux_table='mysql/opening_lines';
Query OK, 0 rows affected (0.01 sec)


mysql> SELECT word FROM INFORMATION_SCHEMA.INNODB_FT_INDEX_TABLE;
+-----------+
| word      |
+-----------+
| across    |
| all       |
| burn      |
| buy       |
| call      |
| comes     |
| dalloway  |
| first     |
| flowers   |
| happened  |
| herself   |
| invisible |
| less      |
| love      |
| man       |
| more      |
| mrs       |
| now       |
| now       |
| now       |
| pleasure  |
| said      |
| screaming |
| she       |
| sight     |
| sky       |
| the       |
| the       |
| this      |
| was       |
| was       |
| when      |
| where     |
| who       |
| would     |
+-----------+
35 rows in set (0.00 sec)

要在一个表一个表的基础上创建stopword列表,需要创建其他stopword表,并在创建全文索引之前使用innodb_ft_user_stopword_table选项指定想要使用的stopword表。

MySQL 全文搜索与查询扩展

全文搜索与查询扩展
全文搜索支持查询扩展(特别是它的变体盲查询扩展)。当搜索短语太短时,这通常很有用,因为这通常意味着用户依赖于全文搜索引擎所缺乏的隐含知识。例如,用户搜索数据库可能意味着MySQL、Oracle、DB2和RDBMS都是应与数据库匹配并应返回的短语。这是隐含知识。

盲查询扩展(也称为自动关联反馈)是通过添加查询扩展或以自然语言模式在搜索短语之后添加查询扩展来实现的。它的工作原理是执行两次搜索,其中第二次搜索的搜索短语是原始搜索短语与第一次搜索中几个相关度最高的文档相连接。因此,如果其中一个文档包含单词”database”和单词”MySQL”,那么第二次搜索将找到包含单词”MySQL”的文档,即使它们不包含单词”database”。下面的示例显示了这种差异:

mysql> SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('database' IN NATURAL LANGUAGE MODE);
+----+------------------------------+-------------------------------+
| id | title                        | body                          |
+----+------------------------------+-------------------------------+
|  6 | Database, Database, Database | database database database    |
|  3 | Optimizing Your Database     | In this database tutorial ... |
|  1 | MySQL Tutorial               | This database tutorial ...    |
+----+------------------------------+-------------------------------+
3 rows in set (0.01 sec)


mysql> SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('database' WITH QUERY EXPANSION);
+----+------------------------------+-------------------------------------+
| id | title                        | body                                |
+----+------------------------------+-------------------------------------+
|  3 | Optimizing Your Database     | In this database tutorial ...       |
|  6 | Database, Database, Database | database database database          |
|  1 | MySQL Tutorial               | This database tutorial ...          |
|  5 | MySQL Security               | When configured properly, MySQL ... |
|  8 | MySQL Full-Text Indexes      | MySQL fulltext indexes use a ..     |
|  2 | How To Use MySQL             | After you went through a ...        |
|  4 | MySQL vs. YourSQL            | When comparing databases ...        |
|  7 | 1001 MySQL Tricks            | 1. Never run mysqld as root. 2. ... |
+----+------------------------------+-------------------------------------+
8 rows in set (0.01 sec)

另一个例子是搜索Georges Simenon写的关于Maigret的书,当用户不知道如何拼写Maigret时。对麦格里和不情愿证人的搜索发现,只有麦格里和不情愿证人没有查询扩展。带有查询扩展的搜索在第二次搜索时发现所有带有单词Maigret的书籍。因为盲查询扩展会通过返回不相关的文档而显著增加噪声,所以只在搜索短语很短的时候使用盲查询扩展。

MySQL单词搜索相关度排名

一个单词搜索的相关度排名,这个例子演示了一个单词搜索的相关度排名计算。

mysql> CREATE TABLE articles (
    -> id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
    -> title VARCHAR(200),
    -> body TEXT,
    -> FULLTEXT (title,body)
    -> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.56 sec)

mysql> INSERT INTO articles (title,body) VALUES
    -> ('MySQL Tutorial','This database tutorial ...'),
    -> ("How To Use MySQL",'After you went through a ...'),
    -> ('Optimizing Your Database','In this database tutorial ...'),
    -> ('MySQL vs. YourSQL','When comparing databases ...'),
    -> ('MySQL Security','When configured properly, MySQL ...'),
    -> ('Database, Database, Database','database database database'),
    -> ('1001 MySQL Tricks','1. Never run mysqld as root. 2. ...'),
    -> ('MySQL Full-Text Indexes', 'MySQL fulltext indexes use a ..');
Query OK, 8 rows affected (0.02 sec)
Records: 8  Duplicates: 0  Warnings: 0



mysql> SELECT id, title, body, MATCH (title,body) AGAINST ('database' IN BOOLEAN MODE) AS score FROM articles ORDER BY score DESC;
+----+------------------------------+-------------------------------------+---------------------+
| id | title                        | body                                | score               |
+----+------------------------------+-------------------------------------+---------------------+
|  6 | Database, Database, Database | database database database          |  1.0886961221694946 |
|  3 | Optimizing Your Database     | In this database tutorial ...       | 0.36289870738983154 |
|  1 | MySQL Tutorial               | This database tutorial ...          | 0.18144935369491577 |
|  2 | How To Use MySQL             | After you went through a ...        |                   0 |
|  4 | MySQL vs. YourSQL            | When comparing databases ...        |                   0 |
|  5 | MySQL Security               | When configured properly, MySQL ... |                   0 |
|  7 | 1001 MySQL Tricks            | 1. Never run mysqld as root. 2. ... |                   0 |
|  8 | MySQL Full-Text Indexes      | MySQL fulltext indexes use a ..     |                   0 |
+----+------------------------------+-------------------------------------+---------------------+
8 rows in set (0.00 sec)

总共有8条记录,其中3条与数据库搜索词匹配。第一条记录(id 6)包含搜索词6次,相关度排名为1.0886961221694946。这个排名值计算使用TF的价值6(数据库搜索词出现6次记录id 6)和IDF值为0.42596873216370745,计算如下(8是记录的总数和3是包含搜索词的记录数量)

${IDF} = log10( 8 / 3 ) = 0.42596873216370745

mysql> select log10( 8 / 3 ) ;
+---------------------+
| log10( 8 / 3 )      |
+---------------------+
| 0.42596873216370745 |
+---------------------+
1 row in set (0.00 sec)

然后将TF和IDF值输入到排名公式中

${rank} = ${TF} * ${IDF} * ${IDF}

在MySQL命令行客户端执行计算将返回一个排名值1.088696164686938。

mysql> SELECT 6*log10(8/3)*log10(8/3);
+-------------------------+
| 6*log10(8/3)*log10(8/3) |
+-------------------------+
|       1.088696164686938 |
+-------------------------+
1 row in set (0.00 sec)

您可能会注意到SELECT … MATCH … AGAINST语句和MySQL命令行客户端所计算的排名值有差别(1.0886961221694946对1.088696164686938)。区别在于InnoDB内部是如何执行整数和浮点数/双精度类型转换的(以及相关的精度和四舍五的决定),以及它们在其他地方是如何执行的,比如在MySQL命令行客户端或其他类型的计算器中。

多词搜索的相关度排名
这个示例演示了基于前面示例中使用的articles表和数据计算多单词全文搜索的相关度排名。

如果你搜索的是一个以上的单词,那么相关度排名值就是每个单词相关度排名值的总和,如下公式所示:

${rank} = ${TF} * ${IDF} * ${IDF} + ${TF} * ${IDF} * ${IDF}

执行两个搜索词(‘mysql tutorial’)搜索将返回以下结果:

mysql> SELECT id, title, body, MATCH (title,body) AGAINST ('mysql tutorial' IN BOOLEAN MODE) AS score FROM articles ORDER BY score DESC;
+----+------------------------------+-------------------------------------+----------------------+
| id | title                        | body                                | score                |
+----+------------------------------+-------------------------------------+----------------------+
|  1 | MySQL Tutorial               | This database tutorial ...          |   0.7405621409416199 |
|  3 | Optimizing Your Database     | In this database tutorial ...       |   0.3624762296676636 |
|  5 | MySQL Security               | When configured properly, MySQL ... | 0.031219376251101494 |
|  8 | MySQL Full-Text Indexes      | MySQL fulltext indexes use a ..     | 0.031219376251101494 |
|  2 | How To Use MySQL             | After you went through a ...        | 0.015609688125550747 |
|  4 | MySQL vs. YourSQL            | When comparing databases ...        | 0.015609688125550747 |
|  7 | 1001 MySQL Tricks            | 1. Never run mysqld as root. 2. ... | 0.015609688125550747 |
|  6 | Database, Database, Database | database database database          |                    0 |
+----+------------------------------+-------------------------------------+----------------------+
8 rows in set (0.00 sec)

在第一条记录(id 1)中,“mysql”出现一次,“tutorial”出现两次。“mysql”有六条匹配记录,“tutorial”有两条匹配记录。当将这些值插入到用于多个单词搜索的排名公式中时,MySQL命令行客户端返回预期的排名值

mysql> SELECT (1*log10(8/6)*log10(8/6)) + (2*log10(8/2)*log10(8/2));
+-------------------------------------------------------+
| (1*log10(8/6)*log10(8/6)) + (2*log10(8/2)*log10(8/2)) |
+-------------------------------------------------------+
|                                    0.7405621541938003 |
+-------------------------------------------------------+
1 row in set (0.00 sec)

与单个单词搜索一样,使用select … match … against语句和MySQL命令行工具执行的结果有差别。

MySQL 生成列索引

MySQL支持在生成列上创建索引,例如:

mysql> create table t1(f1 int,gc int as (f1+1) stored,index(gc));
Query OK, 0 rows affected (0.11 sec)

mysql> insert into t1(f1) values(1),(2),(3),(4),(5),(6),(7),(8),(9),(10);
Query OK, 10 rows affected (0.03 sec)
Records: 10  Duplicates: 0  Warnings: 0

mysql> select * from t1;
+------+------+
| f1   | gc   |
+------+------+
|    1 |    2 |
|    2 |    3 |
|    3 |    4 |
|    4 |    5 |
|    5 |    6 |
|    6 |    7 |
|    7 |    8 |
|    8 |    9 |
|    9 |   10 |
|   10 |   11 |
+------+------+
10 rows in set (0.00 sec)

生成列gc,它的定义为表达式f1+1。这个列gc也创建了索引因此在生成执行计划时优化器可以使用这个索引。下面的查询where条件引用了列gc并且优化器会考虑使用这个索引是否可以生成更有效的执行计划:

mysql> explain select * from t1 where gc>9\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: range
possible_keys: gc
          key: gc
      key_len: 5
          ref: NULL
         rows: 2
     filtered: 100.00
        Extra: Using index condition

优化器可以在生成的列上使用索引来生成执行计划,即使在查询中没有按名称直接引用这些列的情况下也是如此。如果where,order by 或group by子句引用的表达式与某些被索引的生成列相匹配就会出现这种情况。下面的查询没有直接引用生成列gc,但使用的表达式与生成列gc的定义匹配:

mysql> explain select * from t1 where f1+1>9\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: range
possible_keys: gc
          key: gc
      key_len: 5
          ref: NULL
         rows: 2
     filtered: 100.00
        Extra: Using index condition

优化器识别出了表达式f1+1与生成列gc相匹配并且gc列创建了索引,所以优化器在生成执行计划时考虑使用了该索引。

实际上,优化器已经用与表达式匹配的生成列的名称gc替换了表达式f1 + 1。这在执行show warnings命令所显示的扩展解释信息中可以很明显地看到重写查询语句确实用生成列替换了表达式。

mysql> show warnings\G;
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select `mysql`.`t1`.`f1` AS `f1`,`mysql`.`t1`.`gc` AS `gc` from `mysql`.`t1` where (`mysql`.`t1`.`gc` > 9)
1 row in set (0.00 sec)

优化器使用生成列索引有以下限制和条件:
.对于一个查询语句的表达式是否成生成列定义相匹配,那么表达式必须完全相同并且有相同的结果类型。例如,如果生成列表达式是f1+1,如果查询使用1+f1或者如查f1+1(一个整数表达式)与字符进行比较而不是数字那么优化器将不会认为是匹配的。

.对于这些操作:=,< ,<=,>,>=,between和in优化器处理是,对于不是between和in之外的其它操作符而言,任何一个操作数都可以被匹配的生成列所替换。对于between和in操作符,只有第一个参数可以被匹配的生成列替换,并且其它的参数必须要有相同的结果类型。between和in操作符目前还不支持对JSON的调用。

.生成列必须定义为至少包含一个函数调用或前一项中提到的一个运算符的表达式。表达式不能只是简单的引用其它列。例如,gc int as (f1) stored,这个生成列的定义只是简单的引用了一个列,因此在生成列gc上的索引不会被优化器考虑。

.为了比较字符串与调用JSON函数返回带引号的字符串的被索引的生成列,JSON_UNQUOTE()函数需要在生成列定义中用来删除函数返回值的引号(对于直接比较字符串与函数值,JSON比较器句柄会删了引号,但在索引查找时不会发生)。

.如果优化器未能选择所需的索引,则可以使用索引提示强制优化器做出不同的选择。

MySQL InnoDB的索引扩展

索引扩展,InnoDB通过将主键列附加到每个辅助索引中来自动扩展该索引。创建如下表结构:

mysql> CREATE TABLE t1 (
    -> i1 INT NOT NULL DEFAULT 0,
    -> i2 INT NOT NULL DEFAULT 0,
    -> d DATE DEFAULT NULL,
    -> PRIMARY KEY (i1, i2),
    -> INDEX k_d (d)
    -> ) ENGINE = InnoDB;

Query OK, 0 rows affected (0.14 sec)

表t1在列(i1,i2)上定义了主键。同时也在列(d)上定义了一个辅助索引,但InnoDB扩展了这个索引并且将它视为(d,i1,i2)来处理。

在决定如何使用以及是否使用该索引时,优化器会考虑扩展辅助索引的主键列。这可以产生更高效的查询执行计划和更好的性能。

优化器可以使用扩展的二级索引来进行ref、range和index_merge索引访问,进行松散索引扫描,进行连接和排序优化,以及进行MIN()/MAX()优化。

下面的示例将显示优化器是否使用扩展辅助索引来影响执行计划 向表t1插入以下数据:


mysql> INSERT INTO t1 VALUES (1, 1, '1998-01-01'), (1, 2, '1999-01-01'), (1, 3, '2000-01-01'), (1, 4, '2001-01-01'),
    ->(1, 5, '2002-01-01'), (2, 1, '1998-01-01'), (2, 2, '1999-01-01'), (2, 3, '2000-01-01'),  (2, 4, '2001-01-01'),
    ->(2, 5, '2002-01-01'), (3, 1, '1998-01-01'), (3, 2, '1999-01-01'), (3, 3, '2000-01-01'), (3, 4, '2001-01-01'),
    ->(3, 5, '2002-01-01'), (4, 1, '1998-01-01'), (4, 2, '1999-01-01'), (4, 3, '2000-01-01'), (4, 4, '2001-01-01'),
    ->(4, 5, '2002-01-01'), (5, 1, '1998-01-01'), (5, 2, '1999-01-01'),  (5, 3, '2000-01-01'), (5, 4, '2001-01-01'),
    ->(5, 5, '2002-01-01');
Query OK, 25 rows affected (0.05 sec)
Records: 25  Duplicates: 0  Warnings: 0

假设执行下面的查询:

SET optimizer_switch = 'use_index_extensions=off';
explain select count(*) from t1 where i1=3 and d= '2000-01-01' ;

在这种情况下,优化器不能使用主键,因为主键包含列(i1、i2),并且查询没有引用i2。相反,优化器可以使用列(d)上的辅助索引k_d,执行计划取决于是否使用扩展索引。

当优化器不考虑索引扩展时,它将索引k_d仅视为(d)

mysql> SET optimizer_switch = 'use_index_extensions=off';
Query OK, 0 rows affected (0.00 sec)

mysql> explain select count(*) from t1 where i1=3 and d= '2000-01-01' \G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ref
possible_keys: PRIMARY,k_d
          key: PRIMARY
      key_len: 4
          ref: const
         rows: 5
     filtered: 20.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

当优化器考虑到索引扩展时,它将k_d视为(d, i1, i2)。在这种情况下,它可以使用最左边的索引前缀(d, i1)来生成更好的执行计划

mysql> SET optimizer_switch = 'use_index_extensions=on';
Query OK, 0 rows affected (0.00 sec)

mysql> explain select count(*) from t1 where i1=3 and d= '2000-01-01' \G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ref
possible_keys: PRIMARY,k_d
          key: k_d
      key_len: 8
          ref: const,const
         rows: 1
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

在这两种情况下,key表示优化器将使用辅助索引k_d,但是EXPLAIN输出显示了使用扩展索引所带来的这些改进:
.key_len从4字节变成了8字节,指示键查找使用了列d和i1,不仅仅是d。
.ref的值从const变成了const,const,因为键查找使用两个键的列而不是一个。
.rows:从5减到1,指示InnoDB将会检查更少的行来生成查询结果。
.Extra值从Using where;Using index变成了Using index。这意味着查询记录只需要使用索引而不用查询数据行记录。

可以使用show status来查看优化器在使用与不使用扩展索引时的差异:

mysql> flush table t1;
Query OK, 0 rows affected (0.01 sec)

mysql> flush status;
Query OK, 0 rows affected (0.03 sec)

上面的flush table和flush status语句用来清除表的缓存和清除状数据统计数据。

不使用索引扩展时show status产生的结果如下:

mysql> SET optimizer_switch = 'use_index_extensions=off';
Query OK, 0 rows affected (0.01 sec)

mysql> select count(*) from t1 where i1=3 and d= '2000-01-01';
+----------+
| count(*) |
+----------+
|        1 |
+----------+
1 row in set (0.00 sec)

mysql> show status like 'handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 0     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 5     |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
7 rows in set (0.00 sec)

使用索引扩展时,show status产生的结果如下,其中handler_read_next的值从5减到1,指示使用这个索引更有效率:

mysql> flush table t1;
Query OK, 0 rows affected (0.01 sec)

mysql> flush status
    -> ;
Query OK, 0 rows affected (0.02 sec)

mysql> SET optimizer_switch = 'use_index_extensions=on';
Query OK, 0 rows affected (0.00 sec)

mysql> select count(*) from t1 where i1=3 and d= '2000-01-01';
+----------+
| count(*) |
+----------+
|        1 |
+----------+
1 row in set (0.00 sec)

mysql> show status like 'handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 0     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 1     |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
7 rows in set (0.01 sec)

系统变量optimizer_switch的use_index_extensions标志允许优化器在决定如何使用InnoDB表的辅助索引时使不使用主键列。默认情况下,use_index_extensions是启用的。为了检查禁用索引扩展是否可以提高性能可以执行以下语句:

mysql> SET optimizer_switch = 'use_index_extensions=off';
Query OK, 0 rows affected (0.01 sec)
Proudly powered by WordPress | Indrajeet by Sus Hill.