倒排索引是信息检索领域常用的索引技术。它通过将文本分割成单词来构建索引,以便快速查找单词在文档中的出现位置。云数据库 SelectDB 版支持倒排索引,可用于支持文本类型的全文检索、以及普通数值和日期类型的等值或范围查询,能够快速从大量数据中筛选出满足条件的数据。本文介绍云数据库SelectDB版倒排索引的功能,以及如何创建和使用该功能。
索引原理
在云数据库SelectDB版的倒排索引实现中,表的一行对应一个文档,一列对应文档中的一个字段。倒排索引可以根据关键词快速定位包含它的行,从而提高了WHERE子查询的效率。
倒排索引与普通索引的不同之处在于,它的存储层采用独立的倒排文件,与主数据文件Segment只存在逻辑对应关系,而非与主数据文件集成在一起。这使得对索引的更新和删除操作不需要重写主数据文件,从而大幅降低了处理开销。
使用场景
-
加速字符串类型的全文检索。
-
加速字符串、数值、日期时间类型的
=、!=、>、>=、<、<=快速过滤。
功能优势
-
支持完善的逻辑组合。
-
新增索引对
OR、NOT逻辑的下推。 -
支持多个条件的任意
AND、OR、NOT组合。
-
-
灵活、快速的索引管理。
-
支持在创建表时创建倒排索引。
-
支持在已有的表中增加倒排索引。
-
支持删除表中已有的倒排索引。
-
使用限制
-
存在精度问题的浮点数类型FLOAT和DOUBLE不支持倒排索引,原因是浮点数精度不准确。您可以使用精度准确的定点数类型DECIMAL,DECIMAL支持倒排索引。
-
部分复杂数据类型还不支持倒排索引。包括:MAP、STRUCT、JSON、HLL、BITMAP、QUANTILE_STATE、AGG_STATE。JSON类型可通过换成VARIANT类型获得支持。
-
数值类型的字段支持创建倒排索引,但不支持指定parser分词属性,包括:english、chinese、unicode。
-
DUPLICATE和开启Merge-on-Write的UNIQUE表模型支持任意列建倒排索引。但是AGGREGATE和未开启Merge-on-Write的UNIQU模型仅支持Key列建倒排索引,非Key列不能建倒排索引,这是因为这两个模型需要读取所有数据后做合并,因此不能利用索引做提前过滤。
创建索引
创建索引有两种方式,在建表的同时创建索引和为已存在表的某个字段增加索引。
建表时创建索引
此操作为同步过程,建表成功后索引即完成创建。
倒排索引在不同数据模型中有不同的使用限制:
-
Aggregate模型:只能为Key列建立倒排索引。
-
Unique模型:需要开启Merge on Write特性。开启后,可以为任意列建立倒排索引。
-
Duplicate模型:可以为任意列建立倒排索引。
语法
CREATE TABLE [IF NOT EXISTS] [db_name.]<table_name>
(
<column_definition_list>,
[<index_definition_list>]
)
table_properties;
参数说明
建表参数说明
|
参数 |
是否必填 |
描述 |
|
db_name |
否 |
目标数据库名。 |
|
table_name |
是 |
目标表名。 |
|
column_definition_list |
是 |
列定义列表,更多详情,请参见CREATE-TABLE。 |
|
table_properties |
是 |
表的属性定义,如数据模型、分区分桶等。更多详情,请参见数据模型。 |
|
index_definition_list |
否 |
索引定义列表。 |
index_definition_list说明
在创建表时定义索引,可以定义多个索引。其格式为index_definition[, index_definition][, index_definition]...。
index_definition定义
INDEX <index_name>(<column_name>) <index_type> [PROPERTIES("<key>" = "<value>")] [COMMENT '<comment>']
index_definition参数说明
必填参数
|
参数名称 |
参数说明 |
|
index_name |
索引名称。 |
|
column_name |
索引列名称。 |
|
index_type |
索引类型。固定写为 |
选填参数
PROPERTIES
PROPERTIES用来指定索引是否进行分词。它由一个或多个用“,”分割的"<key>" = "<value>"形式的键值对组成。如果您不了解一段连续文本的分词结果,可以使用函数TOKENIZE查看文本分词结果,具体可以参考分词函数章节内容。
|
key |
value |
|
parser |
指定分词器。默认不指定代表不分词。数值类型字段不支持指定parser分词属性。
|
|
parser_mode |
指定分词模式,模式不同,分词粒度不同。 所有分词器默认为粗粒度模式coarse_grained。粗粒度模式,倾向于分出比较长的词,例如 当 如果您还不理解如何分词,请参见分词函数。 |
|
support_phrase |
用于指定索引是否支持MATCH_PHRASE短语查询加速,默认false。
|
|
char_filter |
在分词前对字符串提前处理。目前char_filter_type仅支持char_replace。 char_replace将pattern中每个char替换为一个replacement中的char。
|
|
ignore_above |
指定不分词字符串索引(未指定
|
|
lower_case |
是否将分词结果转换为小写,以便实现不区分大小写匹配。
|
|
stopwords |
指定停用词表,会影响分词器行为。
|
|
dict_compression |
是否对倒排索引的词典启用 ZSTD 字典压缩。
说明
仅4.1.0及以上版本支持此参数。 |
COMMENT
|
参数名称 |
参数说明 |
|
comment |
索引描述。 |
建表并创建索引示例
-- 创建表的同时创建了comment的倒排索引idx_comment
-- USING INVERTED 指定索引类型是倒排索引
-- PROPERTIES("parser" = "english") 指定采用english分词,还支持"chinese"中文分词和"unicode"中英文多语言混合分词,如果不指定"parser"参数表示不分词
CREATE TABLE hackernews_1m
(
`id` BIGINT,
`deleted` TINYINT,
`type` String,
`author` String,
`timestamp` DateTimeV2,
`comment` String,
`dead` TINYINT,
`parent` BIGINT,
`poll` BIGINT,
`children` Array<BIGINT>,
`url` String,
`score` INT,
`title` String,
`parts` Array<INT>,
`descendants` INT,
INDEX idx_comment (`comment`) USING INVERTED PROPERTIES("parser" = "english") COMMENT 'inverted index for comment'
)
DUPLICATE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 10;
已有表增加索引
此操作为异步操作,可通过SHOW ALTER TABLE COLUMN;查看索引增加进度。
语法
ALTER TABLE <table_name> ADD INDEX <index_name>(<column_name>) <index_type> [PROPERTIES("<key>" = "<value>")];
参数说明
此处参数与建表时的参数相同。
示例
添加一个不进行分词的索引。
ALTER TABLE user_tb ADD INDEX index_userId(user_id) USING INVERTED ;
添加一个按照english方式进行分词的索引。
ALTER TABLE user_tb ADD INDEX index_city(city) USING INVERTED PROPERTIES("parser" = "english");
查看索引
查看索引变更进度
通过ALTER和DROP变更索引,是异步过程,可通过以下语句查看变更进度。
SHOW ALTER TABLE COLUMN;
查看表的所有索引
语法
SHOW INDEXES FROM <table_name>;
示例
SHOW INDEXES FROM user_tb;
删除索引
删除索引是异步过程,查看删除进度,请参见查看索引。
删除索引会导致查询性能下降,请谨慎操作。
语法
-- 语法1
DROP INDEX <index_name> ON <table_name>;
-- 语法2
ALTER TABLE <table_name> DROP INDEX <index_name>;
示例
DROP INDEX index_userId ON user_tb;
ALTER TABLE user_tb DROP INDEX index_city;
使用倒排索引
全文检索
语法
SELECT * FROM <table_name> WHERE <column_name> <conditional_logic> '<keywords>';
参数说明
|
参数 |
是否必填 |
描述 |
|
table_name |
是 |
目标表名。 |
|
column_name |
是 |
目标列名。 |
|
conditional_logic |
是 |
匹配逻辑:由全文检索关键字和逻辑关键字自由组合而成。 逻辑关键字: 全文检索关键字:
|
|
keywords |
是 |
目标关键词。 有多个关键词时,需要用空格分隔。 示例: |
示例
-- 检索log_tb表中,logmsg字段包含keyword1的所有行。
SELECT * FROM log_tb WHERE logmsg MATCH_ANY 'keyword1';
-- 检索log_tb表中,logmsg字段包含keyword1或者keyword2的所有行。
SELECT * FROM log_tb WHERE logmsg MATCH_ANY 'keyword1 keyword2';
-- 检索log_tb表中,logmsg字段同时包含keyword1和keyword2的所有行。
SELECT * FROM log_tb WHERE logmsg MATCH_ALL 'keyword1 keyword2';
-- 检索log_tb表中,logmsg字段同时包含keyword1和keyword2的所有行,并且按照keyword1在前,keyword2在后的顺序。
SELECT * FROM log_tb WHERE logmsg MATCH_PHRASE 'keyword1 keyword2';
普通数值和日期类型的等值或范围查询
此种场景下,查询语法与标准查询SQL语法并无差异。
示例
-- 普通等值、范围、IN、NOT IN
SELECT * FROM user_tb WHERE id = 123;
SELECT * FROM user_tb WHERE ts > '2023-01-01 00:00:00';
SELECT * FROM user_tb WHERE op_type IN ('add', 'delete');
查询性能对比
使用hackernews100万条数据,验证使用倒排索引与无倒排索引的查询的性能对比。
环境准备
步骤一:创建目标表。
-
创建数据库。
CREATE DATABASE test_inverted_index; -
切换到创建的数据库。
USE test_inverted_index; -
创建目标表。
CREATE TABLE hackernews_1m ( `id` BIGINT, `deleted` TINYINT, `type` String, `author` String, `timestamp` DateTimeV2, `comment` String, `dead` TINYINT, `parent` BIGINT, `poll` BIGINT, `children` Array<BIGINT>, `url` String, `score` INT, `title` String, `parts` Array<INT>, `descendants` INT, INDEX idx_comment (`comment`) USING INVERTED PROPERTIES("parser" = "english") COMMENT 'inverted index for comment' ) DUPLICATE KEY(`id`) DISTRIBUTED BY HASH(`id`) BUCKETS 10; -- 创建表的同时创建了comment的倒排索引idx_comment -- USING INVERTED 指定索引类型是倒排索引 -- PROPERTIES("parser" = "english") 指定采用english分词,还支持"chinese"中文分词和"unicode"中英文多语言混合分词,如果不指定"parser"参数表示不分词
步骤二:导入数据
将数据导入目标表中。
-
下载数据文件。
wget https://qa-build.oss-cn-beijing.aliyuncs.com/regression/index/hacknernews_1m.csv.gz -
通过Stream Load导入数据。
您可以在云数据库 SelectDB 版的实例详情页面查看云数据库 SelectDB 版实例的连接地址host和端口号port。更多Stream Load详情,请参见Stream Load。
curl --location-trusted -u root: -H "compress_type:gz" -T hacknernews_1m.csv.gz http://<host>:<port>/api/test_inverted_index/hackernews_1m/_stream_load { "TxnId": 2, "Label": "a8a3e802-2329-49e8-912b-04c800a461a6", "TwoPhaseCommit": "false", "Status": "Success", "Message": "OK", "NumberTotalRows": 1000000, "NumberLoadedRows": 1000000, "NumberFilteredRows": 0, "NumberUnselectedRows": 0, "LoadBytes": 130618406, "LoadTimeMs": 8988, "BeginTxnTimeMs": 23, "StreamLoadPutTimeMs": 113, "ReadDataTimeMs": 4788, "WriteDataTimeMs": 8811, "CommitAndPublishTimeMs": 38 } -
执行SQL
count()确认导入数据是否成功。SELECT count() FROM hackernews_1m; +---------+ | count() | +---------+ | 1000000 | +---------+ 1 row in set (0.02 sec)
性能对比
-
使用进行分词的倒排索引与未创建倒排索引统计结果有差异。因为倒排索引对目标列分词后,还会对词进行统一成小写等归一化处理,因此使用倒排索引查询统计的结果会多一点。
-
有些示例性能差异不明显,是因为数据集太小。数据集越大,性能差异越大。
全文检索
-
统计comment列中含有
OLAP的行数。-
基于LIKE统计comment列中含有
OLAP的行数,耗时0.18s。SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLAP%'; +---------+ | count() | +---------+ | 34 | +---------+ 1 row in set (0.18 sec) -
基于倒排索引的全文检索
MATCH_ANY统计comment列中含有OLAP的行数,耗时0.02s。比基于LIKE统计的速度提高了9倍。SELECT count() FROM hackernews_1m WHERE comment MATCH_ANY 'OLAP'; +---------+ | count() | +---------+ | 35 | +---------+ 1 row in set (0.02 sec)
-
-
统计comment列中含有
OLTP的行数。-
基于LIKE统计comment列中含有
OLTP的行数,耗时0.07s。SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLTP%'; +---------+ | count() | +---------+ | 48 | +---------+ 1 row in set (0.07 sec) -
基于倒排索引的全文检索MATCH_ANY统计comment列中含有
OLTP的行数,耗时0.01s。比基于LIKE统计的速度提高了7倍。SELECT count() FROM hackernews_1m WHERE comment MATCH_ANY 'OLTP'; +---------+ | count() | +---------+ | 51 | +---------+ 1 row in set (0.01 sec)
-
-
统计comment列中同时含有
OLAP和OLTP两个词的行数。-
基于LIKE统计,耗时0.13s。
SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLAP%' AND comment LIKE '%OLTP%'; +---------+ | count() | +---------+ | 14 | +---------+ 1 row in set (0.13 sec) -
基于倒排索引的全文检索
MATCH_ALL统计,耗时0.01s。比基于LIKE统计的速度提高了12倍。SELECT count() FROM hackernews_1m WHERE comment MATCH_ALL 'OLAP OLTP'; +---------+ | count() | +---------+ | 15 | +---------+ 1 row in set (0.01 sec)
-
-
统计comment列中含有
OLAP或OLTP的行数。-
基于LIKE查询统计,耗时0.12s。
SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLAP%' OR comment LIKE '%OLTP%'; +---------+ | count() | +---------+ | 68 | +---------+ 1 row in set (0.12 sec) -
基于全文检索统计,耗时0.01s,比基于LIKE查询统计,速度提高了12倍。
SELECT count() FROM hackernews_1m WHERE comment MATCH_ANY 'OLAP OLTP'; +---------+ | count() | +---------+ | 71 | +---------+ 1 row in set (0.01 sec)
-
普通等值、范围查询
-
DataTime类型的列范围查询性能对比。
-
未创建倒排索引前,统计timestamp列大于
2007-08-23 04:17:00的数据。耗时0.03s。SELECT count() FROM hackernews_1m WHERE timestamp > '2007-08-23 04:17:00'; +---------+ | count() | +---------+ | 999081 | +---------+ 1 row in set (0.03 sec) -
为timestamp列增加一个倒排索引。
CREATE INDEX idx_timestamp ON hackernews_1m(timestamp) USING INVERTED; Query OK, 0 rows affected (0.03 sec) -
查看索引创建进度,通过FinishTime和CreateTime的差值,可以看到100万条数据对timestamp列建倒排索引只用了1s。
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) -
索引创建完成后,使用同一条查询语句,统计timestamp列大于
2007-08-23 04:17:00的数据,耗时0.01s。比未创建倒排索引前的查询速度提高了2秒。SELECT count() FROM hackernews_1m WHERE timestamp > '2007-08-23 04:17:00'; +---------+ | count() | +---------+ | 999081 | +---------+ 1 row in set (0.01 sec)
-
-
数值类型等值查询的性能对比。
-
未创建倒排索引前,统计数值列parent等于11189的数据。
SELECT count() FROM hackernews_1m WHERE parent = 11189; +---------+ | count() | +---------+ | 2 | +---------+ 1 row in set (0.01 sec) -
为数值列parent创建不进行分词的倒排索引。
-- 对于数值类型USING INVERTED,不用指定分词 -- ALTER TABLE t ADD INDEX 是第二种建索引的语法 ALTER TABLE hackernews_1m ADD INDEX idx_parent(parent) USING INVERTED; Query OK, 0 rows affected (0.01 sec) -
查看创建索引进度。
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 | +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+ -
索引创建完成后,使用同一条查询语句,统计数值列parent等于11189的数据。
SELECT count() FROM hackernews_1m WHERE parent = 11189; +---------+ | count() | +---------+ | 2 | +---------+ 1 row in set (0.01 sec)
-
-
字符串类型等值查询的性能对比。
-
未创建倒排索引前,统计字符串列author等于faster的数据,耗时0.03s。
SELECT count() FROM hackernews_1m WHERE author = 'faster'; +---------+ | count() | +---------+ | 20 | +---------+ 1 row in set (0.03 sec) -
为字符串列author建不进行分词的倒排索引。
-- 这里只用了USING INVERTED,不对author分词,整个当做一个词处理 ALTER TABLE hackernews_1m ADD INDEX idx_author(author) USING INVERTED; Query OK, 0 rows affected (0.01 sec) -
查看创建索引进度。
-- 100万条author数据增量建索引仅消耗1.5s 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 | +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+ -
索引创建完成后,使用同一条查询语句,统计字符串列author等于faster的数据,耗时0.01s。比未创建倒排索引前的查询速度提高了2秒。
-- 创建索引后,字符串等值匹配也有明显加速 SELECT count() FROM hackernews_1m WHERE author = 'faster'; +---------+ | count() | +---------+ | 20 | +---------+ 1 row in set (0.01 sec)
-
分词函数
分词函数可以将一段连续的文本拆分成一个个独立的词语或短语。它是构建和使用倒排索引的核心,二者是密切相关的,分词质量和方法的选择会直接影响倒排索引的质量和性能。
如果您不了解一段连续文本的分词结果,可以使用函数TOKENIZE查看文本分词结果。TOKENIZE函数中主要包含两个参数:parser和parser_mode,两个参数的详细说明如下。
|
参数 |
描述 |
|
|
指定分词器,默认不指定代表不分词。
|
|
|
指定分词模式,模式不同,分词粒度不同。 所有分词器默认为粗粒度模式coarse_grained。粗粒度模式,倾向于分出比较长的词,例如 当 |
示例如下。
--英文分词效果。
SELECT TOKENIZE('I love CHINA','"parser"="english"');
+------------------------------------------------+
| tokenize('I love CHINA', '"parser"="english"') |
+------------------------------------------------+
| ["i", "love", "china"] |
+------------------------------------------------+
1 row in set (0.02 sec)
--中文分词器实现的细粒度分词效果。
SELECT TOKENIZE('武汉长江大桥','"parser"="chinese","parser_mode"="fine_grained"');
+-----------------------------------------------------------------------------------+
| tokenize('武汉长江大桥', '"parser"="chinese","parser_mode"="fine_grained"') |
+-----------------------------------------------------------------------------------+
| ["武汉", "武汉长江大桥", "长江", "长江大桥", "大桥"] |
+-----------------------------------------------------------------------------------+
1 row in set (0.02 sec)
--中文分词器实现的粗粒度分词效果。
SELECT TOKENIZE('武汉市长江大桥','"parser"="chinese","parser_mode"="coarse_grained"');
+----------------------------------------------------------------------------------------+
| tokenize('武汉市长江大桥', '"parser"="chinese","parser_mode"="coarse_grained"') |
+----------------------------------------------------------------------------------------+
| ["武汉市", "长江大桥"] |
+----------------------------------------------------------------------------------------+
1 row in set (0.02 sec)
--多语言混合分词效果。
SELECT TOKENIZE('I love CHINA 我爱我的祖国','"parser"="unicode"');
+-------------------------------------------------------------------+
| tokenize('I love CHINA 我爱我的祖国', '"parser"="unicode"') |
+-------------------------------------------------------------------+
| ["i", "love", "china", "我", "爱", "我", "的", "祖", "国"] |
+-------------------------------------------------------------------+
1 row in set (0.02 sec)