倒排索引

倒排索引是信息检索领域常用的索引技术。它通过将文本分割成单词来构建索引,以便快速查找单词在文档中的出现位置。云数据库 SelectDB 版支持倒排索引,可用于支持文本类型的全文检索、以及普通数值和日期类型的等值或范围查询,能够快速从大量数据中筛选出满足条件的数据。本文介绍云数据库SelectDB版倒排索引的功能,以及如何创建和使用该功能。

功能介绍

云数据库SelectDB版的倒排索引实现中,表的一行对应一个文档,一列对应文档中的一个字段。倒排索引可以根据关键词快速定位包含它的行,从而提高了WHERE子查询的效率。

倒排索引与普通索引的不同之处在于,它的存储层采用独立的倒排文件,与主数据文件Segment只存在逻辑对应关系,而非与主数据文件集成在一起。这使得对索引的更新和删除操作不需要重写主数据文件,从而大幅降低了处理开销。

云数据库SelectDB版倒排索引的功能如下。

  • 支持字符串类型的全文检索。

    • 支持字符串全文检索。全文检索有以下关键字,均可与逻辑关键字AND、OR、NOT组合使用。

      • MATCH_ALL:同时匹配多个关键字。

      • MATCH_ANY:匹配任意一个关键字。

      • MATCH_PHRASE:匹配短语词组。

    • 支持字符串数组类型的全文检索。

    • 支持英文、中文以及Unicode多语言分词。

  • 支持字符串、数值、日期时间类型的=、!=、>、>=、<、<=快速过滤。

  • 支持完善的逻辑组合。

    • 新增索引对OR、NOT逻辑的下推。

    • 支持多个条件的任意AND、OR、NOT组合。

  • 灵活、快速的索引管理。

    • 支持在创建表时创建倒排索引。

    • 支持在已有的表中增加倒排索引。

    • 支持删除表中已有的倒排索引。

分词函数

分词函数可以将一段连续的文本拆分成一个个独立的词语或短语。它是构建和使用倒排索引的核心,二者是密切相关的,分词质量和方法的选择会直接影响倒排索引的质量和性能。

如果您不了解一段连续文本的分词结果,可以使用函数TOKENIZE查看文本分词结果。

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"="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)

创建索引

创建索引有两种方式,在建表的同时创建索引和为已存在表的某个字段增加索引。

建表时创建索引

此操作为同步过程,建表成功后索引即完成创建。

重要

倒排索引在不同数据模型中有不同的使用限制:

  • 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

索引类型。固定写为USING INVERTED

选填参数
PROPERTIES

PROPERTIES用来指定索引是否进行分词。它由一个或多个用“,”分割的"<key>" = "<value>"形式的键值对组成。

key

value

parser

指定分词器。默认不指定代表不分词。

  • english:英文分词,适合英文字段,用空格和标点符号分词,性能高。

  • chinese:中文分词,适合包含中文的字段,性能比english分词低。

  • unicode:多语言混合类型分词,适用于中英文混合、多语言混合的情况。它能够对邮箱前缀和后缀、IP地址以及字符数字混合进行分词,并且可以对中文按字符分词。

parser_mode

指定分词模式,默认coarse_grained。目前parser=chinese时支持如下模式:

  • fine_grained:细粒度模式,倾向于分出比较短的词,比如'武汉市长江大桥'会分成'武汉','武汉市','市长','长江','长江大桥','大桥'6个词。

  • coarse_grained:粗粒度模式,倾向于分出比较长的词,例如'武汉市长江大桥'会分成'武汉市','长江大桥'2个词。

support_phrase

用于指定索引是否支持MATCH_PHRASE短语查询加速,默认false。

  • true为支持,但是索引需要更多的存储空间。

  • false为不支持,更省存储空间,可以用MATCH_ALL查询多个关键字。

char_filter

在分词前对字符串提前处理。目前char_filter_type仅支持char_replace。

char_replace将pattern中每个char替换为一个replacement中的char。

  • char_filter_pattern:需要被替换掉的字符数组。

  • char_filter_replacement:替换后的字符数组,可以不用配置,默认为一个空格字符。

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");

查看索引

查看索引变更进度

通过ALTERDROP变更索引,是异步过程,可通过以下语句查看变更进度。

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

匹配逻辑:由全文检索关键字和逻辑关键字自由组合而成。

逻辑关键字:AND、OR、NOT

全文检索关键字:

  • MATCH_ALL:同时匹配多个关键字。

  • MATCH_ANY:匹配任意一个关键字。

  • MATCH_PHRASE:匹配短语词组。

keywords

目标关键词。

有多个关键词时,需要用空格分隔。

示例:keyword1 keyword2 keyword3

示例

-- 检索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万条数据,验证使用倒排索引与无倒排索引的查询的性能对比。

环境准备

步骤一:创建目标表。

  1. 创建数据库。

    CREATE DATABASE test_inverted_index;
  2. 切换到创建的数据库。

    USE test_inverted_index;
  3. 创建目标表。

    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"参数表示不分词

步骤二:导入数据

将数据导入目标表中。

  1. 下载数据文件。

    wget https://qa-build.oss-cn-beijing.aliyuncs.com/regression/index/hacknernews_1m.csv.gz
  2. 通过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
    }
  3. 执行SQLcount()确认导入数据是否成功。

    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列中同时含有OLAPOLTP两个词的行数。

    • 基于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列中含有OLAPOLTP的行数。

    • 基于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类型的列范围查询性能对比。

    1. 未创建倒排索引前,统计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)
    2. 为timestamp列增加一个倒排索引。

      CREATE INDEX idx_timestamp ON hackernews_1m(timestamp) USING INVERTED;
      Query OK, 0 rows affected (0.03 sec)
    3. 查看索引创建进度,通过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)
    4. 索引创建完成后,使用同一条查询语句,统计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)
  • 数值类型等值查询的性能对比。

    1. 未创建倒排索引前,统计数值列parent等于11189的数据。

      SELECT count() FROM hackernews_1m WHERE parent = 11189;
      +---------+
      | count() |
      +---------+
      |       2 |
      +---------+
      1 row in set (0.01 sec)
    2. 为数值列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)
    3. 查看创建索引进度。

      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 |
      +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+
    4. 索引创建完成后,使用同一条查询语句,统计数值列parent等于11189的数据。

      SELECT count() FROM hackernews_1m WHERE parent = 11189;
      +---------+
      | count() |
      +---------+
      |       2 |
      +---------+
      1 row in set (0.01 sec)
  • 字符串类型等值查询的性能对比。

    1. 未创建倒排索引前,统计字符串列author等于faster的数据,耗时0.03s。

      SELECT count() FROM hackernews_1m WHERE author = 'faster';
      +---------+
      | count() |
      +---------+
      |      20 |
      +---------+
      1 row in set (0.03 sec)
    2. 为字符串列author建不进行分词的倒排索引。

      -- 这里只用了USING INVERTED,不对author分词,整个当做一个词处理
      ALTER TABLE hackernews_1m ADD INDEX idx_author(author) USING INVERTED;
      Query OK, 0 rows affected (0.01 sec)
    3. 查看创建索引进度。

      -- 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 |
      +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+
      
    4. 索引创建完成后,使用同一条查询语句,统计字符串列author等于faster的数据,耗时0.01s。比未创建倒排索引前的查询速度提高了2秒。

      -- 创建索引后,字符串等值匹配也有明显加速
      SELECT count() FROM hackernews_1m WHERE author = 'faster';
      +---------+
      | count() |
      +---------+
      |      20 |
      +---------+
      1 row in set (0.01 sec)