3.3.3 倒排索引¶
1 索引原理¶
倒排索引,是信息检索领域常用的索引技术,将文本分成一个个词,构建词到文档编号的索引,可以快速查找一个词在哪些文档出现。
从 2.0.0
版本开始, Doris
支持倒排索引,可以用来进行文本类型的全文检索、普通数值日期类型的等值范围查询,快速从海量数据中过滤出满足条件的行。
在 Doris
的倒排索引实现中, Table
的一行对应一个文档、一列对应文档中的一个字段,因此利用倒排索引可以根据关键词快速定位包含它的行,达到 WHERE
子句加速的目的。
与 Doris
中其他索引不同的是,在存储层倒排索引使用独立的文件,跟数据文件一一对应、但物理存储上文件相互独立。这样的好处是可以做到创建、删除索引不用重写数据文件,大幅降低处理开销。
2 使用场景¶
倒排索引的使用范围很广泛,可以加速等值、范围、全文检索(关键词匹配、短语系列匹配等)。一个表可以有多个倒排索引,查询时多个倒排索引的条件可以任意组合。
倒排索引的功能简要介绍如下:
-
加速字符串类型的全文检索
-
支持关键词检索,包括同时匹配多个关键字
MATCH_ALL
、匹配任意一个关键字MATCH_ANY
-
支持短语查询
MATCH_PHRASE
-
支持指定词距
slop
-
支持短语加前缀
MATCH_PHRASE_PREFIX
-
-
支持分词正则查询
MATCH_REGEXP
-
支持英文、中文以及
Unicode
多种分词
-
-
加速普通等值、范围查询,覆盖原来
BITMAP
索引的功能,代替BITMAP
索引-
支持字符串、数值、日期时间类型的
=
,!=
,>
,>=
,<
,<=
快速过滤 -
支持字符串、数字、日期时间数组类型的
=
,!=
,>
,>=
,<
,<=
-
-
支持完善的逻辑组合
-
不仅支持
AND
条件加速,还支持OR NOT
条件加速 -
支持多个条件的任意
AND OR NOT
逻辑组合
-
-
灵活高效的索引管理
-
支持在创建表上定义倒排索引
-
支持在已有的表上增加倒排索引,而且支持增量构建倒排索引,无需重写表中的已有数据
-
支持删除已有表上的倒排索引,无需重写表中的已有数据
-
Tip
倒排索引的使用有下面一些限制:
-
存在精度问题的浮点数类型
FLOAT
和DOUBLE
不支持倒排索引,原因是浮点数精度不准确。解决方案是使用精度准确的定点数类型DECIMAL
,DECIMAL
支持倒排索引。 -
部分复杂数据类型还不支持倒排索引,包括:
MAP
、STRUCT
、JSON
、HLL
、BITMAP
、QUANTILE_STATE
、AGG_STATE
。其中MAP
、STRUCT
会逐步支持,JSON
类型可以换成VARIANT
类型获>
得支持。其他几个类型因为其特殊用途暂不需要支持倒排索引。 -
DUPLICATE
和开启Merge-on-Write
的UNIQUE
表模型支持任意列建倒排索引。但是AGGREGATE
和未开启Merge-on-Write
的UNIQUE
模型仅支持Key
列建倒排索引,非Key
列>
不能建倒排索引,这是因为这两个模型需要读取所有数据后做合并,因此不能利用索引做提前过滤。
如果要查看某个查询倒排索引效果,可以通过 Query Profile
中的相关指标进行分析。
-
InvertedIndexFilterTime
是倒排索引消耗的时间-
InvertedIndexSearcherOpenTime
是倒排索引打开索引的时间 -
InvertedIndexSearcherSearchTime
是倒排索引内部查询的时间
-
-
RowsInvertedIndexFiltered
是倒排过滤掉的行数,可以与其他几个Rows
值对比分析BloomFilter
索引过滤效果
3 使用语法¶
3.1 建表时定义倒排索引¶
在建表语句中 COLUMN
的定义之后是索引定义:
SQL | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
语法说明如下:
-
idx_column_name(column_name)
是必须的,column_name
是建索引的列名,必须是前面列定义中出现过的,idx_column_name
是索引名字,必须表级别唯一,建议命名规范:列名前面加前缀idx_
-
USING INVERTED
是必须的,用于指定索引类型是倒排索引 -
PROPERTIES
是可选的,用于指定倒排索引的额外属性,目前支持的属性如下:-
parser
指定分词器-
默认不指定代表不分词
-
english
是英文分词,适合被索引列是英文的情况,用空格和标点符号分词,性能高 -
chinese
是中文分词,适合被索引列主要是中文的情况,性能比English
分词低 -
unicode
是多语言混合类型分词,适用于中英文混合、多语言混合的情况。它能够对邮箱前缀和后缀、IP
地址以及字符数字混合进行分词,并且可以对中文按字符分词。
分词的效果可以通过
TOKENIZE SQL
函数进行验证,具体参考后续章节。 -
-
parser_mode
用于指定分词的模式,目前
parser = chinese
时支持如下几种模式:-
fine_grained
:细粒度模式,倾向于分出比较短、较多的词,比如‘武汉市长江大桥’会分成‘武汉’,‘武汉市’,‘市长’,‘长江’,‘长江大桥’,‘大桥’6
个词 -
coarse_grained
:粗粒度模式,倾向于分出比较长、较少的词,比如‘武汉市长江大桥’会分成‘武汉市’,‘长江大桥’2
个词 -
默认
coarse_grained
-
-
support_phrase
用于指定索引是否支持
MATCH_PHRASE
短语查询加速-
true
为支持,但是索引需要更多的存储空间 -
false
为不支持,更省存储空间,可以用MATCH_ALL
查询多个关键字 -
默认
false
例如下面的例子指定中文分词,粗粒度模式,支持短语查询加速。
SQL 1
INDEX idx_name(column_name) USING INVERTED PROPERTIES("parser" = "chinese", "parser_mode" = "coarse_grained", "support_phrase" = "true")
-
-
char_filter
用于指定在分词前对文本进行预处理,通常用于影响分词行为
char_filter_type
:指定使用不同功能的char_filter
(目前仅支持char_replace
)char_replace
将pattern
中每个char
替换为一个replacement
中的char
-
char_filter_pattern
:需要被替换掉的字符数 -
char_filter_replacement
:替换后的字符数组,可以不用配置,默认为一个空格字符
例如下面的例子将点和下划线替换成空格,达到将点和下划线作为单词分隔符的目的,影响分词行为。
SQL 1
INDEX idx_name(column_name) USING INVERTED PROPERTIES("parser" = "unicode", "char_filter_type" = "char_replace", "char_filter_pattern" = "._", "char_filter_replacement" = " ")
-
-
ignore_above
用于指定不分词字符串索引(没有指定
parser
)的长度限制-
长度超过
ignore_above
设置的字符串不会被索引。对于字符串数组,ignore_above
将分别应用于每个数组元素,长度超过ignore_above
的字符串元素将不被索引。 -
默认为
256
,单位是字节
-
-
lower_case
是否将分词进行小写转换,从而在匹配的时候实现忽略大小写
-
true
:转换小写 -
false
:不转换小写 -
从
2.0.7
和2.1.2
版本开始默认为true
,自动转小写,之前的版本默认为false
-
-
stopwords
指明使用的停用词表,会影响分词器的行为
默认的内置停用词表包含一些无意义的词:
is
、the
、a
等。在写入或者查询时,分词器会忽略停用词表中的词。none
:使用空的停用词表
-
-
COMMENT
是可选的,用于指定索引注释
3.2 已有表增加倒排索引¶
-
ADD INDEX
支持
CREATE INDEX
和ALTER TABLE ADD INDEX
两种语法,参数跟建表时索引定义相同SQL 1 2 3 4
-- 语法 1 CREATE INDEX idx_name ON table_name(column_name) USING INVERTED [PROPERTIES(...)] [COMMENT 'your comment']; -- 语法 2 ALTER TABLE table_name ADD INDEX idx_name(column_name) USING INVERTED [PROPERTIES(...)] [COMMENT 'your comment'];
-
BUILD INDEX
CREATE / ADD INDEX
操作只是新增了索引定义,这个操作之后的新写入数据会生成倒排索引,而存量数据需要使用BUILD INDEX
触发:SQL 1 2 3 4
-- 语法 1,默认给全表的所有分区 BUILD INDEX BUILD INDEX index_name ON table_name; -- 语法 2,可指定 Partition,可指定一个或多个 BUILD INDEX index_name ON table_name PARTITIONS(partition_name1, partition_name2);
通过
SHOW BUILD INDEX
查看BUILD INDEX
进度:SQL 1 2 3 4 5
SHOW BUILD INDEX [FROM db_name]; -- 示例 1,查看所有的 BUILD INDEX 任务进展 SHOW BUILD INDEX; -- 示例 2,查看指定 table 的 BUILD INDEX 任务进展 SHOW BUILD INDEX where TableName = "table1";
通过
CANCEL BUILD INDEX
取消BUILD INDEX
:SQL 1 2
CANCEL BUILD INDEX ON table_name; CANCEL BUILD INDEX ON table_name (job_id1,jobid_2,...);
Tip
BUILD INDEX
会生成一个异步任务执行,在每个 BE
上有多个线程执行索引构建任务,通过 BE
参数 alter_index_worker_count
可以设置,默认值是 3
。
2.0.12
和 2.1.4
之前的版本 BUILD INDEX
会一直重试直到成功,从这两个版本开始通过失败和超时机制避免一直重试。 3.0
存算分离模式暂不支持此命令。
-
一个
tablet
的多数副本BUILD INDEX
失败后,整个BUILD INDEX
失败结束 -
时间超过
alter_table_timeout_second()
,BUILD INDEX
超时结束 -
用户可以多次触发
BUILD INDEX
,已经BUILD
成功的索引不会重复BUILD
3.3 已有表删除倒排索引¶
SQL | |
---|---|
1 2 3 4 |
|
Tip
DROP INDEX
会删除索引定义,新写入数据不会再写索引,同时会生成一个异步任务执行索引删除操作,在每个 BE
上有多个线程执行索引构建任务,通过 BE
参数 alter_index_worker_count
可以设置,默认值是 3
。
3.4 利用倒排索引加速查询¶
SQL | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
3.5 分词函数¶
如果想检查分词实际效果或者对一段文本进行分词行为,可以使用 TOKENIZE
函数进行验证。
TOKENIZE
函数的第一个参数是待分词的文本,第二个参数是创建索引指定的分词参数。
SQL | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
4 使用示例¶
用 HackerNews 100
万条数据展示倒排索引的创建、全文检索、普通查询,包括跟无索引的查询性能进行简单对比。
4.1 建表¶
SQL | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
4.2 导入数据¶
通过 Stream Load
导入数据
Bash | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
SQL
运行 count()
确认导入数据成功
SQL | |
---|---|
1 2 3 4 5 6 7 |
|
4.3 查询¶
-
全文检索
-
用
LIKE
匹配计算comment
中含有OLAP
的行数,耗时0.18s
SQL 1 2 3 4 5 6 7
mysql> SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLAP%'; +---------+ | count() | +---------+ | 34 | +---------+ 1 row in set (0.18 sec)
-
用基于倒排索引的全文检索
MATCH_ANY
计算comment
中含有'OLAP'
的行数,耗时0.02s
,加速9
倍,在更大的数据集上效果会更加明显这里结果条数的差异,是因为倒排索引对
comment
分词后,还会对词进行进行统一成小写等归一化处理,因此MATCH_ANY
比LIKE
的结果多一些SQL 1 2 3 4 5 6 7
mysql> SELECT count() FROM hackernews_1m WHERE comment MATCH_ANY 'OLAP'; +---------+ | count() | +---------+ | 35 | +---------+ 1 row in set (0.02 sec)
-
同样的对比统计
'OLTP'
出现次数的性能,0.07s vs 0.01s
,由于缓存的原因LIKE
和MATCH_ANY
都有提升,倒排索引仍然有7
倍加速SQL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
mysql> SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLTP%'; +---------+ | count() | +---------+ | 48 | +---------+ 1 row in set (0.07 sec) mysql> SELECT count() FROM hackernews_1m WHERE comment MATCH_ANY 'OLTP'; +---------+ | count() | +---------+ | 51 | +---------+ 1 row in set (0.01 sec)
-
同时出现
'OLAP'
和'OLTP'
两个词,0.13s vs 0.01s
,13
倍加速要求多个词同时出现时(
AND
关系)使用MATCH_ALL 'keyword1 keyword2 ...'
SQL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
mysql> SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLAP%' AND comment LIKE '%OLTP%'; +---------+ | count() | +---------+ | 14 | +---------+ 1 row in set (0.13 sec) mysql> SELECT count() FROM hackernews_1m WHERE comment MATCH_ALL 'OLAP OLTP'; +---------+ | count() | +---------+ | 15 | +---------+ 1 row in set (0.01 sec)
-
任意出现
'OLAP'
和'OLTP'
其中一个词,0.12s vs 0.01s
,12
倍加速只要求多个词任意一个或多个出现时(
OR
关系)使用MATCH_ANY 'keyword1 keyword2 ...'
SQL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
mysql> SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLAP%' OR comment LIKE '%OLTP%'; +---------+ | count() | +---------+ | 68 | +---------+ 1 row in set (0.12 sec) mysql> SELECT count() FROM hackernews_1m WHERE comment MATCH_ANY 'OLAP OLTP'; +---------+ | count() | +---------+ | 71 | +---------+ 1 row in set (0.01 sec)
-
-
普通等值、范围查询
-
DataTime
类型的列范围查询SQL 1 2 3 4 5 6 7
mysql> SELECT count() FROM hackernews_1m WHERE timestamp > '2007-08-23 04:17:00'; +---------+ | count() | +---------+ | 999081 | +---------+ 1 row in set (0.03 sec)
-
为
timestamp
列增加一个倒排索引SQL 1 2 3 4
-- 对于日期时间类型 USING INVERTED,不用指定分词 -- CREATE INDEX 是第一种建索引的语法,另外一种在后面展示 mysql> CREATE INDEX idx_timestamp ON hackernews_1m(timestamp) USING INVERTED; Query OK, 0 rows affected (0.03 sec)
SQL 1 2
mysql> BUILD INDEX idx_timestamp ON hackernews_1m; Query OK, 0 rows affected (0.01 sec)
-
查看索引创建进度,通过
FinishTime
和CreateTime
的差值,可以看到100
万条数据对timestamp
列建倒排索引只用了1s
SQL 1 2 3 4 5 6 7
mysql> SHOW ALTER TABLE COLUMN; +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+ | JobId | TableName | CreateTime | FinishTime | IndexName | IndexId | OriginIndexId | SchemaVersion | TransactionId | State | Msg | Progress | Timeout | +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+ | 10030 | hackernews_1m | 2023-02-10 19:44:12.929 | 2023-02-10 19:44:13.938 | hackernews_1m | 10031 | 10008 | 1:1994690496 | 3 | FINISHED | | NULL | 2592000 | +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+ 1 row in set (0.00 sec)
SQL 1 2 3 4 5 6 7 8
-- 若 table 没有分区,PartitionName 默认就是 TableName mysql> SHOW BUILD INDEX; +-------+---------------+---------------+----------------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+ | JobId | TableName | PartitionName | AlterInvertedIndexes | CreateTime | FinishTime | TransactionId | State | Msg | Progress | +-------+---------------+---------------+----------------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+ | 10191 | hackernews_1m | hackernews_1m | [ADD INDEX idx_timestamp (`timestamp`) USING INVERTED], | 2023-06-26 15:32:33.894 | 2023-06-26 15:32:34.847 | 3 | FINISHED | | NULL | +-------+---------------+---------------+----------------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+ 1 row in set (0.04 sec)
-
索引创建后,范围查询用同样的查询方式,
Doris
会自动识别索引进行优化,但是这里由于数据量小性能差别不大SQL 1 2 3 4 5 6 7
mysql> SELECT count() FROM hackernews_1m WHERE timestamp > '2007-08-23 04:17:00'; +---------+ | count() | +---------+ | 999081 | +---------+ 1 row in set (0.01 sec)
-
在数值类型的列
Parent
进行类似timestamp
的操作,这里查询使用等值匹配SQL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
mysql> SELECT count() FROM hackernews_1m WHERE parent = 11189; +---------+ | count() | +---------+ | 2 | +---------+ 1 row in set (0.01 sec) -- 对于数值类型 USING INVERTED,不用指定分词 -- ALTER TABLE t ADD INDEX 是第二种建索引的语法 mysql> ALTER TABLE hackernews_1m ADD INDEX idx_parent(parent) USING INVERTED; Query OK, 0 rows affected (0.01 sec) -- 执行 BUILD INDEX 给存量数据构建倒排索引 mysql> BUILD INDEX idx_parent ON hackernews_1m; Query OK, 0 rows affected (0.01 sec) mysql> SHOW ALTER TABLE COLUMN; +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+ | JobId | TableName | CreateTime | FinishTime | IndexName | IndexId | OriginIndexId | SchemaVersion | TransactionId | State | Msg | Progress | Timeout | +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+ | 10030 | hackernews_1m | 2023-02-10 19:44:12.929 | 2023-02-10 19:44:13.938 | hackernews_1m | 10031 | 10008 | 1:1994690496 | 3 | FINISHED | | NULL | 2592000 | | 10053 | hackernews_1m | 2023-02-10 19:49:32.893 | 2023-02-10 19:49:33.982 | hackernews_1m | 10054 | 10008 | 1:378856428 | 4 | FINISHED | | NULL | 2592000 | +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+ mysql> SHOW BUILD INDEX; +-------+---------------+---------------+----------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+ | JobId | TableName | PartitionName | AlterInvertedIndexes | CreateTime | FinishTime | TransactionId | State | Msg | Progress | +-------+---------------+---------------+----------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+ | 11005 | hackernews_1m | hackernews_1m | [ADD INDEX idx_parent (`parent`) USING INVERTED], | 2023-06-26 16:25:10.167 | 2023-06-26 16:25:10.838 | 1002 | FINISHED | | NULL | +-------+---------------+---------------+----------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+ 1 row in set (0.01 sec) mysql> SELECT count() FROM hackernews_1m WHERE parent = 11189; +---------+ | count() | +---------+ | 2 | +---------+ 1 row in set (0.01 sec)
-
对字符串类型的
author
建立不分词的倒排索引,等值查询也可以利用索引加速SQL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
mysql> SELECT count() FROM hackernews_1m WHERE author = 'faster'; +---------+ | count() | +---------+ | 20 | +---------+ 1 row in set (0.03 sec) -- 这里只用了 USING INVERTED,不对 author 分词,整个当做一个词处理 mysql> ALTER TABLE hackernews_1m ADD INDEX idx_author(author) USING INVERTED; Query OK, 0 rows affected (0.01 sec) -- 执行 BUILD INDEX 给存量数据加上倒排索引: mysql> BUILD INDEX idx_author ON hackernews_1m; Query OK, 0 rows affected (0.01 sec) -- 100 万条 author 数据增量建索引仅消耗 1.5s mysql> SHOW ALTER TABLE COLUMN; +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+ | JobId | TableName | CreateTime | FinishTime | IndexName | IndexId | OriginIndexId | SchemaVersion | TransactionId | State | Msg | Progress | Timeout | +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+ | 10030 | hackernews_1m | 2023-02-10 19:44:12.929 | 2023-02-10 19:44:13.938 | hackernews_1m | 10031 | 10008 | 1:1994690496 | 3 | FINISHED | | NULL | 2592000 | | 10053 | hackernews_1m | 2023-02-10 19:49:32.893 | 2023-02-10 19:49:33.982 | hackernews_1m | 10054 | 10008 | 1:378856428 | 4 | FINISHED | | NULL | 2592000 | | 10076 | hackernews_1m | 2023-02-10 19:54:20.046 | 2023-02-10 19:54:21.521 | hackernews_1m | 10077 | 10008 | 1:1335127701 | 5 | FINISHED | | NULL | 2592000 | +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+ mysql> SHOW BUILD INDEX order by CreateTime desc limit 1; +-------+---------------+---------------+----------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+ | JobId | TableName | PartitionName | AlterInvertedIndexes | CreateTime | FinishTime | TransactionId | State | Msg | Progress | +-------+---------------+---------------+----------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+ | 13006 | hackernews_1m | hackernews_1m | [ADD INDEX idx_author (`author`) USING INVERTED], | 2023-06-26 17:23:02.610 | 2023-06-26 17:23:03.755 | 3004 | FINISHED | | NULL | +-------+---------------+---------------+----------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+ 1 row in set (0.01 sec) -- 创建索引后,字符串等值匹配也有明显加速 mysql> SELECT count() FROM hackernews_1m WHERE author = 'faster'; +---------+ | count() | +---------+ | 20 | +---------+ 1 row in set (0.01 sec)
-