- [innodb索引与算法](#innodb索引与算法) - [索引算法](#索引算法) - [二分查找](#二分查找) - [B+树索引](#b树索引) - [聚簇索引](#聚簇索引) - [非数据页](#非数据页) - [辅助索引](#辅助索引) - [辅助索引查找](#辅助索引查找) - [辅助索引查找分析](#辅助索引查找分析) - [索引创建](#索引创建) - [algorithm](#algorithm) - [lock](#lock) - [online DDL](#online-ddl) - [Cardinality](#cardinality) - [Cardinality](#cardinality-1) - [innodb存储引擎的Cardinality](#innodb存储引擎的cardinality) - [Cardinality采样](#cardinality采样) - [索引使用](#索引使用) - [联合索引](#联合索引) - [覆盖索引](#覆盖索引) - [覆盖索引对于统计场景的好处](#覆盖索引对于统计场景的好处) - [优化器选择不使用索引](#优化器选择不使用索引) - [辅助索引不能覆盖](#辅助索引不能覆盖) - [索引提示](#索引提示) - [Use index](#use-index) - [Multi-Range Read](#multi-range-read) - [multi-range read优势](#multi-range-read优势) - [范围查询和join查询优化](#范围查询和join查询优化) - [拆分键值对](#拆分键值对) - [未开启MRR](#未开启mrr) - [开启MRR优化](#开启mrr优化) - [mrr控制](#mrr控制) - [read\_rnd\_buffer\_size](#read_rnd_buffer_size) - [Index Condition Pushdown(ICP)](#index-condition-pushdownicp) - [关闭ICP](#关闭icp) - [开启ICP](#开启icp) - [innodb hash](#innodb-hash) - [innodb中的哈希](#innodb中的哈希) - [自适应hash索引](#自适应hash索引) - [全文检索](#全文检索) - [倒排索引](#倒排索引) - [inverted file index](#inverted-file-index) - [full inverted index](#full-inverted-index) - [innodb全文检索](#innodb全文检索) - [辅助表](#辅助表) - [FTS Index Cache (全文检索索引缓存)](#fts-index-cache-全文检索索引缓存) - [查看指定倒排索引的辅助表分词信息](#查看指定倒排索引的辅助表分词信息) - [FTS Index Cache更新和写盘时机](#fts-index-cache更新和写盘时机) - [分词删除](#分词删除) - [OPTIMIZE TABLE](#optimize-table) - [innodb\_ft\_cache\_size](#innodb_ft_cache_size) - [FTS Document id](#fts-document-id) - [innodb全文检索示例](#innodb全文检索示例) - [innodb full-text design](#innodb-full-text-design) - [innodb full-text index tables](#innodb-full-text-index-tables) - [辅助索引表(auxiliary index table)](#辅助索引表auxiliary-index-table) - [table\_id hex](#table_id-hex) - [index\_id hex](#index_id-hex) - [公共索引表(common index table)](#公共索引表common-index-table) - [innodb full-text index cache](#innodb-full-text-index-cache) - [innodb\_ft\_cache\_size](#innodb_ft_cache_size-1) - [innodb\_ft\_total\_cache\_size](#innodb_ft_total_cache_size) - [full-text查询](#full-text查询) - [DOC\_ID和FTS\_DOC\_ID](#doc_id和fts_doc_id) - [innodb full text deleteion handle](#innodb-full-text-deleteion-handle) - [innodb full-text index transaction handling](#innodb-full-text-index-transaction-handling) - [match ... against](#match--against) - [natural language](#natural-language) - [relevance](#relevance) - [stopword](#stopword) - [Boolean](#boolean) - [+/-](#-) - [no operator](#no-operator) - [Proximity Search](#proximity-search) - [query expansion](#query-expansion) - [ngram full-text parser](#ngram-full-text-parser) - [配置`ngram_token_size`](#配置ngram_token_size) - [使用ngram parser创建fulltext Index](#使用ngram-parser创建fulltext-index) - [ngram parser space handling](#ngram-parser-space-handling) # innodb索引与算法 innodb存储引擎支持如下集中常见的索引: - B+树索引 - 对于B+树索引,其只能找到被查找数据所在的页,然后数据库把页读入到内存中,再在内存中进行查找 - 全文索引 - 哈希索引 - innodb中哈希索引是自适应的,innodb存储引擎会根据目前表的使用情况自动生成哈希索引 ## 索引算法 ### 二分查找 如前所示,mysql数据页中存在page directory,而page directory中slots是按照主键顺序来进行排放的。 `对于数据的查找,是通过对page directory中的槽进行二分查找得到的`。 ## B+树索引 inoodb中索引大多是B+树的实现,`且具有高扇出的特性`。故而,在数据库中,`B+树的高度一般都在2~4层,即在查找某一行记录时,最多只需要2~4次IO。` innodb中B+树索引可以分为聚簇索引和辅助索引,其都是基于B+树实现,`所有的数据都存放在叶子节点中`。 > 聚簇索引和辅助索引的区别在于叶子节点中是否存放的是整行的记录信息。 ### 聚簇索引 在innodb中,表是由索引组织的,即表中的数据按照主键的顺序存放。 聚簇索引是按照每张表的主键构建一颗B+树,而叶子节点存放的则是整张表的行记录数据,`故而,聚簇索引的叶子节点也被称为数据页`。 > 和B+树一样,每个数据页(叶子节点)都通过双向链表来进行链接。 > `每张表只能拥有一个聚簇索引`,并且,在多数情况下,查询优化器倾向于采用聚簇索引。`因为聚簇索引能够直接在叶子节点获取到行记录。` #### 非数据页 在非数据页中,存放的是key以及指向数据页的页号。 聚簇索引的存储并非是物理上连续的,`而是通过双向链表来维护逻辑上的顺序`。 > 数据页与数据页之间通过双向链表来维护顺序排序,而位于同一个页中的记录之间也是通过双向链表来维护排序的。 对于聚簇索引,其`按照主键排序进行查找`或`按主键进行范围查找`时,查询速度很快。 ### 辅助索引 对于辅助索引,叶子节点并不包含行记录的全部数据。在辅助索引的叶子节点中,叶子节点除了包含辅助索引的key外,`每个叶子节点中的索引行还包含一个书签(bookmark)`。bookmark可以告知inodb存储引擎哪里能找到和索引相对应的行数据。 > 实际上,innodb存储引擎的辅助索引其bookmark就是相应行数据的聚簇索引key`。 辅助索引并不会影响行数据在聚簇索引上的组织,故而每张表可以含有一个聚簇索引和多个辅助索引。 #### 辅助索引查找 当通过辅助索引来查找数据时,innodb会遍历辅助索引的B+树数据结构,并从叶子节点中获取指向的主键索引值,之后再通过主键索引来查找到完整的行记录。 #### 辅助索引查找分析 如果聚簇索引和辅助索引的B+树高度同样为3,那么当根据辅助索引来查找关联的主键时,需要花费3次IO。此时,再根据主键来从聚簇索引中获取对应行数据,需要另外3次IO。 故而,根据辅助索引来获取行记录,大概需要6次IO。 ### 索引创建 通过alter table语法,可以选择索引的创建方式: ```sql alter table tbl_name | add {index|key} [index_name] [index_type] (index_col_name, ...) algorithm [=] {default|inplace|copy} lock [=] {default|none|shared|exclusive} ``` #### algorithm 通过`algorithm`可以指定创建或删除索引的算法: - copy:通过创建临时表的方式,定义新的表结构,并将原表中的数据移动到新的表,最后删除原表,并将临时表重命名 - 该算法将会耗费大量事件,并且在执行时表不可被访问 - inplace:索引创建和删除操作不采用临时表 - default: 按照`old_alter_table`参数来判断是通过inplace还是copy来创建或删除索引,默认`old_alter_table`为off,即是采用inplace算法 #### lock lock部分代表索引创建或删除时,对表的加锁情况: - none:执行索引的创建或删除操作时,对目标表不添加任何的锁,在执行过程中事务仍然能够进行读写操作,不会阻塞。该加锁情况能够获取最大程度的并发度 - share:在执行过程中,对表添加s锁,`s锁会和x锁以及ix锁发生冲突,但是能够兼容s锁和is锁`。 - 故而,在加锁方式为share的场景下,执行索引的创建或删除时,其他事务仍然能够执行读取操作,但是写操作则是会被阻塞。 - 如果存储引擎不支持share模式,会返回相应的错误信息 - exclusive:在exclusive模式下,执行索引的创建和删除操作时,会对目标表添加一个x锁,此时,其他事务对该表的读和写操作都不会被执行 - default: - default模式下首先会判断当前操作能否使用none - 如果不能,则判断是否能够使用shared - 如果仍然不能,那么最后会判断能否使用exclusive #### online DDL innodb存储引擎实现online DDL的原理是在执行索引的创建或删除操作时,将`insert, update, delete`这些DML写操作的日志写入到一个缓存中,等到索引创建完成后,再将这些DML语句重做到表上(日志内容类似于redo log)。 该缓存的大小可以通过`innodb_online_alter_log_max_size`来进行控制,默认大小为`128M`。 如果再执行索引创建和更新的表比较大,并且在`online alter`执行过程中存在大量的写事务,那么当`innodb_online_alter_log_max_size`指定的空间不足以存放时,会抛出错误。 如果发生上述错误,可以考虑增加`inodb_online_alter_log_max_size`参数的值;也可以设置`alter table`的`lock`模式为`share`,那么在执行过程中不会有写事务的发生,不需要进行DML日志的记录。 ### Cardinality 并不是所有的字段都适合创建B+树索引。例如对于性别字段、类型字段等,它们的选择范围都很小,这种字段不适合创建B+树索引。 > 性别字段中只有`M`和`F`两种可选值,故而执行`select * from student where sex = 'F'`这类sql可能会得到表中50%的数据,这种情况下为性别字段添加B+树索引完全没有必要。 而对于`bigint`类型的id字段,其选择范围很广,且没有重复,这类字段适合使用B+树索引。 #### Cardinality 通过`show index`语句可以查看索引是否拥有高选择性,`Cardinality`列代表索引中`不重复记录数量的预估值`。 `Cardinality`是一个预估值,并不是准确的值,且`Cardinality / rows_in_table`的值应该尽可能的接近1。如果该值非常小,那么用户应考虑该索引的创建是否有必要。 #### innodb存储引擎的Cardinality 在生产环境下,索引的操作可能十分频繁,如果每次操作都对cardinality进行统计,那么将会带来巨大的开销。`故而,数据库针对Cardinality的统计都是通过采样来进行的`。 在innodb中,针对cardinality统计信息的更新发生在两个操作中:`insert, update`。innodb针对cardinality的更新策略为: - 表中1/16的数据已经发生变化 - 自上一次统计cardinality信息时起,表中1/16的数据已经发生过变化,此时需要重新统计cardinality - stat_modified_counter >= 2000,000,000 - 如果针对表中某一行数据进行频繁的更新,那么该场景不会适配第一种更新策略,故而在innodb内部维护了一个计数器stat_modified_counter,用于标识发生变化的次数,当计数器的值大于`2000,000,000`时,也需要更新cardinality #### Cardinality采样 默认情况下,innodb会针对8个叶子节点进行采样,采样过程如下: - 取得B+树索引中叶子节点的数量,记为A - 随机获取索引8个叶子节点,统计每个页不同记录的个数,记为P1, P2, ... , P8 - 根据采样信息给出Cardinality的预估值`(P1 + P2 + ... + P8) * A/8` ## 索引使用 ### 联合索引 联合索引代表对表上的多个列进行索引。 示例如下: ```sql create table t ( id bigint not null auto_increment, a int not null, b int not null, primary key(id), index idx_a_b (a, b) ); ``` 对于上述联合索引`(a,b)`,各查询语句使用索引的情况如下: - `where a = xxx and b = xxx`: - 该语句会使用联合索引 - `where a = xxx`: - 该语句同样可以使用联合索引 - `where b = xxx`: - 该语句无法使用联合索引 ### 覆盖索引 innodb存储引擎支持覆盖索引,即通过辅助索引即可得到查询记录,无需再次查询聚簇索引。 > 通常来说,辅助索引中叶子节点的记录大小要远小于聚簇索引中叶子节点的记录大小。故而,在范围统计的场景下(`select count(1)`),辅助索引叶子节点中,一个页包含更多的记录,表扫描时需要读取的页数页更少。 > > 故而,在部分场景下,使用覆盖索引能够节省大量的io操作。 对于辅助索引而言,其叶子节点的记录中包含`(primary key1, primary key2, ..., key1, key2)`等信息,故而,如下语句都可以通过`一次辅助联合索引查询来完成`: - `select key2 from table where key1 = xxx` - 会命中辅助索引,且辅助索引中包含key2的值,故而无需再次查询聚簇索引 - `select primary key2,key2 from table where key1 = xxx` - `select primary key1,key2 from table where key1 = xxx` - `select primary key1, primary key2, key2 from key1 = xxx` #### 覆盖索引对于统计场景的好处 例如,对于表buy_log ```sql create table buy_log ( userid int unsigned not null, buy_date date, index `idx_userid` (userid), index `idx_userid_buy_date` (userid, buy_date) ); ``` 预置如下数据: ```sql insert into buy_log(userid, buy_date) values (1, '2022-01-01'), (2,'2022-03-04'), (3, '2024-12-31'), (4, '2024-07-11'), (5, '2025-01-01'); ``` 执行`explain select count(*) from buy_log;`语句时,由于where条件为空,其并不会命中索引,但是语句执行的结果如下所示: | id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | 1 | SIMPLE | buy\_log | null | index | null | idx\_userid | 4 | null | 5 | 100 | Using index | 上述返回结果中,`possible_keys`列为null,但是实际执行时优化器却选择了`idx_userid`索引,Extra列值为`Using index`,代表优化器选择了覆盖索引。 > 由于覆盖索引中记录大小更小,故而使用覆盖索引能够节省io,提升性能。 并且,对于`explain select count(*) from buy_log where buy_date >= '2023-01-01' and buy_date <= '2024-01-01';`语句,其返回结果如下 | id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | 1 | SIMPLE | buy\_log | null | index | idx\_userid\_buy\_date | idx\_userid\_buy\_date | 8 | null | 5 | 20 | Using where; Using index | 正常来说,`idx_userid_buy_date`索引组成为`(userid, buy_date)`,当根据`buy_date`来进行查找时,不应该命中索引。 但是,由于辅助索引中同一页包含记录更多,故而上述语句仍然使用了`idx_userid_buy_date`覆盖索引。 ### 优化器选择不使用索引 在某些情况下,当使用explain语句进行分析时,会发现优化器没有选择索引去查找数据,而是通过全盘扫描聚簇索引。这种情况通常发生在范围查找,join连接等操作时。 #### 辅助索引不能覆盖 对于辅助索引不能进行覆盖的场景,优化器仅当`查询少量数据`时选择辅助索引。 优化器不选择辅助索引的原因是,`根据辅助索引再次查询聚簇索引时,会带来大量的随机读`。由于辅助索引和聚簇索引的排序不同,在辅助索引中连续的数据,很可能在聚簇索引中是分散的。如果使用辅助索引,那么在根据主键查询聚簇索引时,可能带来大量的随机读,主键可能随机分布在各个数据页中,这样会带来大量的io。 故而,`当辅助索引需要读取大量的记录,并且辅助索引不能覆盖时,优化器会倾向直接在聚簇索引中进行顺序查找。`顺序读通常要远快于随机读。 > 如果mysql server实例部署在固态硬盘上,随机读取的速度很快,同时确认使用辅助索引的性能更佳,可以使用`force index`来强制使用某个索引,使用示例如下: > > ```sql > select * from orderdetails force index (orderID) where orderid > 10000 and order id < 102000 > ``` ### 索引提示 mysql支持索引提示,如下两种场景可能会用到索引提示: - mysql错误的选择了某个索引,导致sql语句运行较慢 - 某个sql可选择索引非常多,此时优化器选择执行计划的耗时可能大于sql语句执行的耗时 mysql数据库中,索引提示的语法如下; ```sql table_name [[as] alias] [index_hint_list] -- 其中,index_hint_list的含义如下 index_hint_list: index_hint[, index_hint]... -- index_hint的含义则如下: index_hint: USE {INDEX|KEY} [{FOR {JOIN | ORDER BY | GROUP BY}}] ([index_list]) | IGNORE {INDEX | KEY} [{FOR {JOIN | ORDER BY | GROUP BY}}] (index_list) | FORCE {INDEX|KEY} [{FOR {JOIN | ORDER BY | GROUP BY}}] (index_list) -- index_list含义如下: index_list: index_name, [, index_name]... ``` 创建`t_demo`示例如下: ```sql create table t_demo ( a int not null, b int not null, key idx_a (a), key idx_b (b) ); insert into t_demo(a,b) values (1,1), (1,2), (2, 3), (2, 4), (1,2); ``` ### Use index 执行如下语句`explain select * from t_demo where a=1 and b = 2;`,其返回执行计划如下: | id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | 1 | SIMPLE | t\_demo | null | index\_merge | idx\_a,idx\_b | idx\_b,idx\_a | 4,4 | null | 1 | 83.33 | Using intersect\(idx\_b,idx\_a\); Using where; Using index | 易知优化器选择了使用`idx_a`和`idx_b`两个索引来完成该查询,且Extra列包含了`Using intersect(b,a)`,代表查询结果根据两个索引得到的结果进行求交集的数学运算。 可以通过`Use Index`来提示使用`idx_a`索引: ```sql explain select * from t_demo use index (idx_a) where a=1 and b = 2; ``` 得到结果如下: | id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | 1 | SIMPLE | t\_demo | null | ref | idx\_a | idx\_a | 4 | const | 3 | 20 | Using where | 上述结果显示执行方案选用了`idx_a`。 > 在使用`use index`语法之后,仍然有可能出现优化器没有使用该索引的情况,此时可以使用`force index`语法,来强制指定使用的索引。 ## Multi-Range Read mysql支持Multi-Range Read优化,用于减少随机的磁盘访问,将随机访问转化为较为顺序的数据访问。对于io-bound的sql查询,其能带来性能的较大提升。 mutli-range read优化适用于`range, ref, eq_ref`类型的查询。 ### multi-range read优势 - MRR能够令数据访问变得较为顺序,在查询辅助索引时,对于得到的查询结果按照主键进行排序,并按照主键排序的顺序进行书签查找 - 减少缓冲池中页被替换的次数 - 批量处理对于key的查询操作 ### 范围查询和join查询优化 对于innodb的范围查询和join查询,MRR工作方式如下 - 将查询到的辅助索引键值存放在缓冲中,此时缓存中的数据按照辅助索引排序 - 将缓存中的键值按照rowId进行排序 - 根据rowId的排序来实际访问数据文件 > 当innodb的缓冲池不足以存放表中所有数据时,频繁的离散读写会导致缓冲池中的页被换出,又被不断读入。 > > `如果按照主键顺序进行访问,可以将该类行为降至最低,页面不会被换出缓冲区后又被读入`。 ### 拆分键值对 除了上述优化外,对某些范围查询,mrr还能够进行拆分优化,这样在拆分过程中直接能过滤掉一些不符合查询条件的数据。 例如: ```sql select * from t where key_part1 >= 1000 and key_part1 < 2000 and key_part2 = 1000; ``` 表t存在`(key_part1, key_part2)`的联合索引。 #### 未开启MRR 如果未开启mrr,那么此时查询类型为`RANGE`,mysql优化器会将key_part位于`[1000, 2000)`范围内的全部数据都取出,即是key_part2不为1000。在去除后,再按key_part2条件对取出数据进行过滤。 > 这将会导致有大量无用数据被取出。如果存在大量数据且key_part2不为1000,那么使用mrr将会有大量性能提升。 #### 开启MRR优化 在使用了MRR优化后,优化器会先对查询条件进行拆分,将`key_part1 >= 1000 and key_part1 < 2000 and key_part2 = 1000`条件拆分为`(1000, 1000), (1001, 1000), ..., (1999, 1000)`,在根据拆分后的条件进行查询。 #### mrr控制 是否开启mrr可以通过参数`optimizer_switch`中的flag来控制,当mrr为`on`时,代表启用mrr优化。 `mrr_cost_based`标记代表`是否通过cost based的方式来选择是否启用mrr`。 如果将`mrr`设置为`on`,`mrr_cost_based`设置为`off`,代表总是启用mrr优化。 #### read_rnd_buffer_size `read_rnd_buffer_size`用来控制键值的缓冲区大小,当大于该值时,则执行器则对已经缓存的数据根据rowId来进行排序,并通过rowId来获取数据。该值默认为`256K`. ## Index Condition Pushdown(ICP) 可以通过`optimizer_switch`中的flag来控制`ICP`是否开启,如`index_condition_pushdown`为`on`,代表icp开启。 例如,表`people`包含`(zip_code, last_name, first_name)`索引,执行如下语句 ```sql select * from people where zipcode = '95054' and last_name like '%asahi%' and address like '%street%'; ``` ### 关闭ICP 如果ICP优化没有开启,那么数据库首先会根据索引查询`zipcode`为95054的记录,然后查询出后再根据查询出的结果过滤where的后两个条件`last_name like '%asahi%' and address like '%street%'`。 ### 开启ICP 当开启ICP后,数据库会将where的部分过滤条件放在存储引擎层,在索引获取数据同时就会进行where条件的过滤。 ## innodb hash 在innodb中,采用除法散列的哈希方法,通过`k % m`将关键字`k`映射到`m`个槽中的一个,即 ``` hash(k) = k % m ``` ### innodb中的哈希 innodb存储引擎采用哈希算法对字典进行查找,冲突机制采用链表方式,哈希函数采用`k % m`的方式进行散列。 对于缓冲池页的哈希表,缓冲池中的page页都有一个chain指针,指向相同哈希值的页。而对于`m`的取值,其规则如下: - m的取值应略大于2倍的缓冲池页数量的质数 - 例如,若`innodb_buffer_pool_size`大小为10M,那么缓冲池页数量为 `10 * 1024 / 16 = 640`,可容纳240个缓冲页,故而,对于缓冲池页内存的哈希表来说,需要分配的`槽个数m`为大于`640 * 2`的质数,即`1399`。 ### 自适应hash索引 自适应hash索引可以通过参数`innodb_adaptive_hash_index`来进行开启或关闭,并且,自适应hash索引的使用情况可以通过`show engine innodb status`来进行查看: ``` ------------------------------------- INSERT BUFFER AND ADAPTIVE HASH INDEX ------------------------------------- Ibuf: size 1, free list len 0, seg size 2, 0 merges merged operations: insert 0, delete mark 0, delete 0 discarded operations: insert 0, delete mark 0, delete 0 0.00 hash searches/s, 320660.58 non-hash searches/s ``` 可以根据`hash searches/s`和`non-hash searches/s`来查看自适应hash索引的使用情况。 ## 全文检索 ### 倒排索引 全文检索通常使用`倒排索引`(inverted index)来实现,和B+树索引一致,倒排索引也是一种索引结构。 倒排索引在辅助表(auxiliary table)中存储了`单词`和`单词自身在一个或多个文档中所在位置`之间的映射关系。通常,倒排索引通过关联数组可以实现,拥有两种表现形式: - inverted file index: {单词,单词所在文档id} - full inverted index: {单词, (单词所在文档id, 在文档中的具体位置)} #### inverted file index 对于inverted file index,其存储的数据结构如下所示: | Number | Text | Documents | | :-: | :-: | :-: | | 1 | code | 1,4 | | 2 | days | 3,6 | | 3 | hot | 1,4 | 其中,`Documents`存储的是包含查询关键字的文档id数组。 > 对于inverted file index,其仅保存文档的id #### full inverted index 对于full inverted index,其存储的是`(文档id,文档中位置)`的`pair`。 full inverted index的存储结构示例如下所示; | Number | Text |Documents | | :-: | :-: | :-: | | 1 | code | (1:6), (4:8) | | 2 | days | (3:2), (6:2) | | 3 | hot | (1:3), (4:4) | > full inverted index 相较于 inverted file index,除了存储文档id之外,还存储单词所在文档的位置信息。 ### innodb全文检索 innodb支持full inverted index形式的全文检索。 在innodb中,将`(DocumentId, Position)`视为`ilist`。故而,在全文检索的表中,存在两个字段,`word`和`ilist`,并在`word`字段设有索引。`并且,innodb在ilist中存放了position信息,故而可以支持proximity search`。 #### 辅助表 innodb中,倒排索引需要将`word`存放在辅助表中,并且,为了提高全文检索的并行能力,共存在`6`张辅助表,每张表根据word的Latin进行分区。 > 辅助表为持久化的表,存放在磁盘中。 #### FTS Index Cache (全文检索索引缓存) FTS Index Cache为红黑树结构,根据`(word, ilist)`进行排序。 在插入数据时,即使插入数据已经更新了对应的数据库表,但是对`全文索引`更新可能仍位于`FTS Index Cache`中,即辅助表尚未被更新。Innodb会批量对辅助表进行更新,而非每次插入后都立即更新索引表。 当对全文检索进行查询时,辅助表首先会将FTS Index Cache中对应word字段合并到辅助表中,然后再在辅助表中执行查询操作。 > 上述FTS Index Cache操作类似Insert Buffer,其能提高innodb的性能,并且其由红黑树排序后再执行批量插入,其产生的辅助表相对较小。 #### 查看指定倒排索引的辅助表分词信息 innodb允许用户查看指定倒排索引辅助表的分词信息,可以通过设置`innodb_ft_aux_table`来查看倒排索引的辅助表。 ```sql set global innodb_ft_aux_table='{schema_name}/{table_name}'; ``` 执行完上述语句后,可以在`information_schema.innodb_ft_index_table`中查询表的分词信息。 #### FTS Index Cache更新和写盘时机 Innodb在事务提交时将分词写入到`FTS Index Cache`中,然后再根据批量更新将`FTS Index Cache`写入到磁盘。 在事务提交时,FTS Index Cache中数据会同步到磁盘的辅助表中。但是,当数据库发生宕机时,FTS Index Search中的数据可能尚未被同步到磁盘中。 在上述宕机情况下,下次数据库重启时,若用户对表进行全文检索操作(插入或查询),innodb会自动读取未完成的文档,并然后进行分词操作,再次将分词结果放入到FTS Index Cache中。 #### 分词删除 对于文档中分词的删除操作,在事务提交时,不删除辅助表中的数据,而只是删除`FTS Index Cache`中的记录。对于被删除的记录,会根记录其FTS Dcoument Id,并且将该id保存在`deleted辅助表`中。 > 在删除文档时,文档内容从一般表中被删除,但是`索引辅助表`中的数据并不会被删除,相反的,还会将`FTS_DOC_ID`添加到`deleted辅助表`中。 #### OPTIMIZE TABLE 由上述内容可知,对文档的DML并不会实际删除`索引中的数据`,只是会在`deleted辅助表`中添加`FTS_DOC_ID`,故而在应用程序运行时,`索引会变得越来越大`。 mysql允许通过`optimize table`命令来实际将已删除记录从索引中删除。 由于`optimize table`语句不仅会删除索引中的数据,还会执行其他操作,例如`Cardinality`重新统计等。 > 如果用户仅希望对倒排索引进行操作,可以设置`innodb_optimize_fulltext_only`参数。 > > 可以执行如下语句 > ```sql > set global innodb_optimize_fulltext_only=1; > optimize table xxx; > ``` 如果被删除文档很多,那么optimize table可能会花费大量时间,这将对程序的运行造成影响。 用户可以通过参数`innodb_ft_num_word_optimize`来限制每次实际删除的分词数量,该参数默认值为2000. #### innodb_ft_cache_size 可以通过`innodb_ft_cache_size`来控制FTS Index Cache的大小,默认大小为`32M`。当缓冲被填满后,会将缓存中`(word, ilist)`数据同步到位于磁盘的辅助表中。 适当增加`innodb_ft_cache_size`参数的值能够提升全文检索的性能,但是在宕机时,位同步到磁盘中的数据可能需要更长时间来进行恢复。 #### FTS Document id 在innodb存储引擎中,为了支持全文检索,必须存在一个字段和辅助表中的`word`进行对应。在innodb中,对应字段为`FTS_DOC_ID` `FST_DOC_ID`字段的字段类型必须为`BIGINT UNSIGNED NOT NULL`,并且innodb存储引擎会自动为该列添加名为`FST_DOC_ID_INDEX`的唯一索引`unique index`。 ### innodb全文检索示例 full-text index基于`text based columns`(char, varchar, text)来进行创建,用于加速针对`text based colums`列的dml操作。 一个full-text index可以定义为`create table`语句的一部分,也可以通过`alter table`或`create index`语句添加到已经存在的表中。 full-text查询通过`match() ... against`预发来触发。 #### innodb full-text design innodb全文索引基于倒排索引进行设计。倒排索引存储了一系列的words;对每个word,都存在一个list与之对应,list中的元素为`包含word的文档`。 为了支持`proximity search`,word在list中出现的位置信息也同样被保存。 #### innodb full-text index tables 当innodb full-text index被创建时,一系列index tables都会同时被创建,示例如下所示。 创建表,并指定fulltext索引: ```sql create table fs_text ( id bigint not null auto_increment, content longtext, primary key (id), fulltext index `idx_fts_fs_text` (content) ); ``` 查询辅助表: ```sql select * from information_schema.innodb_tables where name like 'innodb_demo%' ``` 其中,和full-text表相关的索引如下: | TABLE\_ID | NAME | FLAG | N\_COLS | SPACE | ROW\_FORMAT | ZIP\_PAGE\_SIZE | SPACE\_TYPE | INSTANT\_COLS | TOTAL\_ROW\_VERSIONS | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | 1822 | innodb\_demo/fs\_text | 33 | 6 | 96 | Dynamic | 0 | Single | 0 | 0 | | 1823 | innodb\_demo/fts\_000000000000071e\_being\_deleted | 33 | 4 | 97 | Dynamic | 0 | Single | 0 | 0 | | 1824 | innodb\_demo/fts\_000000000000071e\_being\_deleted\_cache | 33 | 4 | 98 | Dynamic | 0 | Single | 0 | 0 | | 1825 | innodb\_demo/fts\_000000000000071e\_config | 33 | 5 | 99 | Dynamic | 0 | Single | 0 | 0 | | 1826 | innodb\_demo/fts\_000000000000071e\_deleted | 33 | 4 | 100 | Dynamic | 0 | Single | 0 | 0 | | 1827 | innodb\_demo/fts\_000000000000071e\_deleted\_cache | 33 | 4 | 101 | Dynamic | 0 | Single | 0 | 0 | | 1828 | innodb\_demo/fts\_000000000000071e\_00000000000005ad\_index\_1 | 33 | 8 | 102 | Dynamic | 0 | Single | 0 | 0 | | 1829 | innodb\_demo/fts\_000000000000071e\_00000000000005ad\_index\_2 | 33 | 8 | 103 | Dynamic | 0 | Single | 0 | 0 | | 1830 | innodb\_demo/fts\_000000000000071e\_00000000000005ad\_index\_3 | 33 | 8 | 104 | Dynamic | 0 | Single | 0 | 0 | | 1831 | innodb\_demo/fts\_000000000000071e\_00000000000005ad\_index\_4 | 33 | 8 | 105 | Dynamic | 0 | Single | 0 | 0 | | 1832 | innodb\_demo/fts\_000000000000071e\_00000000000005ad\_index\_5 | 33 | 8 | 106 | Dynamic | 0 | Single | 0 | 0 | | 1833 | innodb\_demo/fts\_000000000000071e\_00000000000005ad\_index\_6 | 33 | 8 | 107 | Dynamic | 0 | Single | 0 | 0 | #### 辅助索引表(auxiliary index table) 其中,以`innodb_demo/fts_000000000000071e_00000000000005ad_index`开头的6张表,用于存储倒排索引,并被称为`辅助索引表`。 当新增的文档被分割为`token`时,每个独立的`word`(也可被称为`token`)被插入到辅助索引表中,随着word被插入的还有`postion`和`DOC_ID`。 word按照`第一个字符的字符集排序权重`被排序,并且在六张辅助索引表中进行分区。 > 倒排索引被分区在6张辅助索引表中,用于支持索引的并行创建。默认情况下,2个线程执行`tokenize`, `sort`,`将word和关联数据插入到index tables`操作。 > > 如果想要指定操作上述流程的线程数量,可以对`innodb_ft_sort_pll_degree`参数进行配置。如果要在大表上创建full-text index,可以考虑增加该参数。 ##### table_id hex 辅助索引表命名通过`fts_`开头,并且后缀`index_#`。每个辅助索引表都通过`辅助索引表表名中16进制的值`来和`被索引表的table id`来进行关联。例如,上述示例中,16进制值`071e`代表十进制`1822`,而表`fs_text`的`table_id`刚好为`1822`。 ##### index_id hex 在6张辅助索引表中,除了包含`table_id`的hex外,下划线后还存在一个16进制数部分,该部分为`05ad`,代表十进制`1453`,而索引`idx_fts_fs_text`的`index_id`刚好为`1453`。 > 如果是`file per table tablespace`,那么idnex table将会保存在其自己的tablespace中。 #### 公共索引表(common index table) 除了辅助索引表之外,剩下的表被称为公共索引表,用于处理删除和存储full-text index的状态。 公共索引表和辅助索引表区别如下: - 辅助索引表:辅助索引表用于存储倒排索引的内容,其6张表都是针对索引的,若一张表内拥有两个`fulltext`索引,那么每个fulltext索引都会有其自己的6张辅助索引表 - 公共索引表:公共索引表是针对表的,不管一张表中存在多少`fulltext`索引,都只有一张公共索引表 即使在删除fulltext索引后,common index table仍然会保留,当删除fulltext索引后,为该索引创建的`FTS_DOC_ID`字段也会被保留,因为删除FTS_DOC_ID列需要对先前被索引的表进行重构。 公共索引表用于管理`FTS_DOC_ID`列: - `fts_*_deleted`和`fts_*_deleted_cache`: - 该表用于保存`文档已被删除,但是数据仍然没有从full-text index中移除`的文档id(DOC_ID)。 - `fts_*_deleted_cache`是`fts_*_deleted`的内存保本 - `fts_*_being_deleted`和`fts_*_being_deleted_cache`: - 该表用于保存`文档已被删除,并且文档数据正在从full-text index中被移除`的文档id(DOC_ID) - `fts_*_deleted_cache`是`fts_*deleted`的内存版本 - `fts_*_config`: - 存储full-text index的内部状态, - 其会存储`FTS_SYNCED_DOC_ID`,用于标识`已经被转化并且刷新到磁盘中`的文档。当mysql应用发生崩溃并且重启恢复时,`FS_SYNCED_DOC_ID`会标识还没有被刷新到磁盘中的文档。故而所有未被刷新到磁盘的文档都会被重新reparsed,并且添加到full-text index cache中 #### innodb full-text index cache 当文档被插入时,其会被执行`tokenize`操作,之后得到的`word`和`word关联的数据`将会被插入到full-text index中。 - 在上述过程中,即使对于大小很小的文档,也会产生对`辅助索引表`的大量小插入,会并发访问辅助索引表,产生竞争 为了避免上述问题,innodb使用full-text index cache来对index table insertions进行缓存。该内存缓存结构将会缓存插入操作,直到cache被填满并将其批量刷新到磁盘(即刷新到辅助索引表)。 > 可以在`information_schema.innodb_ft_index_cahce`来查看最近新插行的数据。 `通过cache来缓存插入,并在cache满后批量刷新到磁盘`,该策略能够避免频繁访问辅助索引表,避免在插入和更新时并发访问带来的问题。 除此之外,批量插入还能避免针对相同word的多次插入,进而将重复条目最小化。 - 在使用innodb full-text index cache时,对于相同word的插入将会被合并为一条entry并插入,其不仅能提高插入性能,同时也能令库中的辅助索引表尽可能的小。 ##### innodb_ft_cache_size `innodb_ft_cache_size`用于指定full-text index cache的大小(针对每一张表),大小的指定将会影响full-text index cache被刷新的频率。 ##### innodb_ft_total_cache_size `innodb_ft_total_cache_size`用于限制所有表`full-text index cache`大小。 ##### full-text查询 full-text index cache中存储的信息和辅助索引表中相同。但是,full-text cache中只存储近期插入的行。在执行查询操作时,已经被刷新到磁盘的数据并并不会被重新带到缓存中。 对full-text中数据的查询如下: - 直接查询辅助索引表中的数据 - 查询full-text index cache中的数据 - 将辅助索引表中查询的数据和full-text index cache中查询的数据进行合并 #### DOC_ID和FTS_DOC_ID innodb使用`DOC_ID`作为唯一文档标识符,`DOC_ID`将word和word出现的文档相关联。该关联关系需要被索引表中的`FTS_DOC_ID`字段,如果`FTS_DOC_ID`未在被索引表中定义,那么innodb会在fulltext索引创建时自动添加隐藏的`FTS_DOC_ID`字段。 示例如下: ```sql 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; ``` 如果使用`create fulltext`语法向表中添加fulltext索引时,会出现警告信息,报告innodb正在对table进行`rebuilding`操作。 ```sql mysql> CREATE FULLTEXT INDEX idx ON opening_lines(opening_line); Query OK, 0 rows affected, 1 warning (0.19 sec) Records: 0 Duplicates: 0 Warnings: 1 mysql> SHOW WARNINGS; +---------+------+--------------------------------------------------+ | Level | Code | Message | +---------+------+--------------------------------------------------+ | Warning | 124 | InnoDB rebuilding table to add column FTS_DOC_ID | +---------+------+--------------------------------------------------+ ``` 当在`create table`时创建了fulltext idnex,并且没有在建表语句中指定`FTS_DOC_ID`字段,innodb会添加一个隐藏的`FTS_DOC_ID`字段,并且不会返回warning信息。 > 当使用`alter table ... add fulltex`语法添加fulltext索引时,同样会返回相同的异常信息。 比起在表中已经存在数据后向表中添加fulltext索引,在`create table`时指定fulltext index开销更小。innodb会创建一个隐藏的`FTS_DOC_ID`字段,并为`FTS_DOC_ID`字段创建一个唯一索引`FTS_DOC_ID_INDEX。 > 如果想要自己创建`FTS_DOC_ID`字段,该字段类型必须为`BIGINT UNSIGNED NOT NUL`,示例如下所示: > ```sql > mysql> CREATE TABLE opening_lines ( > FTS_DOC_ID BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, > opening_line TEXT(500), > author VARCHAR(200), > title VARCHAR(200) > ) ENGINE=InnoDB; > ``` > 对`FTS_DOC_ID`字段,目前已使用的最大值和新`FTS_DOC_ID`字段值的最大间隔为`65535` 为了避免对table的rebuild,`FTS_DOC_ID`字段在删除full-text index后仍然会被保留。 #### innodb full text deleteion handle 在对一个包含full-text index column的record进行删除操作时,将会导致大量对辅助索引表的`small deletion`操作,这些操作可能会导致对辅助索引表的并行访问以及竞争。 为了避免上述问题,当record从table中被删除时,被删除文档的`DOC_ID`将会被添加到`FTS_*_DELETED`表中,并且被删除的记录仍然会存在于`full-text index`中。 在执行查询操作返回之前,`FTS_*_DELETED`表中包含的信息将会被用于过滤`被删除的DOC_ID`。上述设计将会令删除速度变得更快。 该设计也会导致fulltext index的内容大小持续增加,如果要移除被删除record对应的full-text index内容,可以执行`optimize table`语句。如果开启`innodb_optimize_fulltext_only=on`,其仅会针对fulltext index进行优化。 ##### innodb full-text index transaction handling 由于full-text index拥有`caching`和批量处理的特性,full-text index有其对应的独特事务处理。 `具体来说,对full-text index的更新和插入在事务提交时才会被处理,即full-text search只对已提交的数据可见。` 示例如下所示 ```sql mysql> CREATE TABLE opening_lines ( id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, opening_line TEXT(500), author VARCHAR(200), title VARCHAR(200), FULLTEXT idx (opening_line) ) ENGINE=InnoDB; mysql> BEGIN; 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'); mysql> SELECT COUNT(*) FROM opening_lines WHERE MATCH(opening_line) AGAINST('Ishmael'); +----------+ | COUNT(*) | +----------+ | 0 | +----------+ mysql> COMMIT; mysql> SELECT COUNT(*) FROM opening_lines -> WHERE MATCH(opening_line) AGAINST('Ishmael'); +----------+ | COUNT(*) | +----------+ | 1 | +----------+ ``` 如上述示例所示,在事务提交前,对full-text index的insertion和update操作都不可见,insertion和update操作直到事务提交时才会被执行。 ### match ... against mysql数据库支持`fulltext search`查询,其语法如下: ```sql match (col1, col2, ...) against (expr [search_modifier]) -- search modifier定义如下 search_modifier: { in natural language mode | in natural language mode with query expansion | in boolean mode | with query expansion } ``` mysql通过`match against`语法支持全文索引的查询,match指定了需要被查询的列,against指定了使用何种方式去查询。 #### natural language 全文索引查询默认采用natural language的方式进行查询,其代表查询带有指定word的文档。 对于如下语句: ```sql select * from fts_a where body like '%Pease%' ``` 上述查询显然无法使用B+树索引,如果换为全文索引,那么可以使用如下sql语句进行查询: ```sql select * from fts_a where match(body) against ('Porridge' in natural language mode) ``` 而`natural language mode`为默认的全文检索查询模式,故而`in natural language mode`修饰符可以省略,省略后如下: ```sql select * from fts_a where match(body) against ('Porridge') ``` ##### relevance 在where条件中使用match函数,其返回结果是通过`relevance`进行降序排序的,`即相关性最高的结果放在第一位`。 相关性的值为非负的浮点数值,0代表没有相关性,根据mysql官方文档可知,相关性计算依据如下四个条件: - word是否在文档中出现 - word在文档中出现次数 - word在索引列中的数量 - 多少个文档中包含word 如果用户想要查看相关性,可以使用如下语句: ```sql select fts_doc_id, body, match(body) against ('porridge' in natural language mode) as relevance from fts_a` ``` ##### stopword 对innodb的全文检索,还应该考虑如下因素: - 如果查询的word在`stop word`中,那么忽略该字符串查询 - 如果查询的`word`位于stopword中,那么不对该词进行查询,例如`the`,`对于stopword其相关性为0` - 查询`word`的长度是否位于区间`[innodb_ft_min_token_size`, `innodb_ft_max_token_size]`之间 - `innodb_ft_min_token_size`和`innodb_ft_max_token_size`用于控制查询字符串的长度,当长度小于`innodb_ft_min_token_size`或大于`innodb_ft_max_token_size`时,会忽略该词的搜索。 - `innodb_ft_min_token_size`的默认值为3,`innodb_ft_max_token_size`的默认值为84 #### Boolean innodb支持`in boolean mode`修饰符,当使用该修饰符时,查询字符串的前后字符会拥有特殊含义。 示例如下: ```sql select * from fts_a where match(body) against ('+Pease -hot' in boolean mode) ``` 上述示例代表文档中要包含`Pease`但是不包含`hot`。 boolean全文检索支持如下的操作符种类: - `+`代表word必须存在 - `-`代表word必须被排除 - 如果不存在操作符,代表该word是可选的,但是word出现时relevance更高 - `@distance`代表查询的多个单词之间,间距是否在distance之间,distance单位为字节。该全文检索的查询也被称为`Proximity Search`。 - 例如`match (body) against ('"Pease pot"@30' in boolean mode)`代表字符串`Pease`和`Pot`之间距离在30字节之内 - `>`表示出现该word增加相关性 - `<`表示出现该word降低相关性 - `~`表示允许出现该单词,但是出现时相关性为负 - `*`表示以该单词开头的单词,例如`lik*`表示可以为`lik`,`like`,`likes` - `"`表示短语 - full-text engine将会把短语分割为多个word,并且对`words`执行全文检索。并且,非单词字符不需要完全匹配,短语搜索只需要包含和短语完全相同的单词,例如`test phrase`可以匹配`test, phrase`。 ##### +/- ```sql select * from fts_a where match(body) against ('+Pease +hot' in boolean mode) ``` 上述示例返回既有`Pease`又有`hot`的文档 ##### no operator ```sql select * from fts_a where match(body) against ('Pease hot' in boolean mode) ``` 上述示例返回有`Pease`或有`hot`的文档 ##### Proximity Search ```sql select * from fts_a where match(body) against ('"Pease pot" @10' in boolean mode) ``` #### query expansion innodb支持全文检索的拓展查询。有时用户的查询关键词太短,此时用户需要implied knowledge。 例如,用户在对单词`database`进行查询时,还希望查询的不仅是包含`database`的文档,还包含`MySQL、Oracle、DB2`等单词,此时,可以使用query expansion来启用全文检索的implied knowledge。 通过`with query expansion`或`in natural language mode with query expansion`可以开启blind query expansion,该查询分为两阶段: - 根据搜索的单词进行全文索引查询 - 根据第一阶段的结果再进行分词,并且按分词再进行全文索引查找 由于query expansion全文检索可能带来非常多的非相关性查询结果,因此在使用时用户应该相当小心。 ### ngram full-text parser mysql中内置的full-text parser使用空格作为word之间的分隔符,对部分语言如`中文,日文`并不适用。为了解决该限制,mysql提供了`ngram`full-text parser,其支持中文、日语、汉语。 > ngram full-text parser支持innodb和myisam存储引擎。 `ngram`代表给定文本中`n`个字符的连续序列,`ngram parser`会将一个文本`tokenize`为一系列连续的`n characters`序列。 > 例如,根据不同的`n`,通过gram parser,可以将字符串`abcd` tokenize为如下结果 > ``` > n=1 'a' 'b' 'c' 'd' > n=2 'ab' ‘bc' 'cd' > n=3 ‘abc' 'bcd' > n=4 'abcd' > ``` ngram parser是内置的server plugin,其会在server启动时自动加载。 #### 配置`ngram_token_size` ngram parser默认的ngram token size1为2,例如,当ngram token size为2时,ngram parser将字符串`“abc def”`转化为4个token`"ab bc de ef"`。 `ngram_token_size`参数可用于配置ngram token size。其最小值为1,最大值为10. 通常,ngram token size被设置为想要查找的最长token长度,当ngram token size越小时,将会产生更小的full-text search index,查询也会变得更快。 #### 使用ngram parser创建fulltext Index 如下示例显示了如何创建ngram parser对应的full-text index: ```sql mysql> USE test; mysql> CREATE TABLE articles ( id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, title VARCHAR(200), body TEXT, FULLTEXT (title,body) WITH PARSER ngram ) ENGINE=InnoDB CHARACTER SET utf8mb4; mysql> SET NAMES utf8mb4; INSERT INTO articles (title,body) VALUES ('数据库管理','在本教程中我将向你展示如何管理数据库'), ('数据库应用开发','学习开发数据库应用程序'); mysql> SET GLOBAL innodb_ft_aux_table="test/articles"; mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_INDEX_CACHE ORDER BY doc_id, position; ``` 如果要向已经存在的表中添加`full-text index`且使用ngram parser,示例如下: ```sql CREATE TABLE articles ( id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, title VARCHAR(200), body TEXT ) ENGINE=InnoDB CHARACTER SET utf8mb4; ALTER TABLE articles ADD FULLTEXT INDEX ft_index (title,body) WITH PARSER ngram; # Or: CREATE FULLTEXT INDEX ft_index ON articles (title,body) WITH PARSER ngram; ``` #### ngram parser space handling ngram parser在处理时消除空格,示例如下: - `ab cd`会被转化为`ab`和`cd` - `a bc`会被转化为`bc`