An inverted index is a common indexing technique in information retrieval. It works by tokenizing text into terms to build an index, enabling the quick retrieval of documents containing those terms.ApsaraDB for SelectDB supports inverted indexes. You can use this feature to perform full-text searches on text data types and run equality and range queries on numeric and date data types, which allows you to quickly filter large datasets for specific data.This topic describes the inverted index feature in ApsaraDB for SelectDB, including how to create and use it.
How it works
In Alibaba Cloud SelectDB, the inverted index treats a table row as a document and a column as a field. This enables the inverted index to quickly locate rows containing a specific keyword, improving the performance of queries that use a WHERE clause.
Unlike a regular index, an inverted index is stored in a separate inverted file. This file is logically mapped to the segment file but is not integrated with it. This approach avoids rewriting the segment file for index updates and deletions, significantly reducing processing overhead.
Scenarios
-
Accelerates full-text search on string data types.
-
Accelerates
=, !=, >, >=, <, <=filtering for string, numeric, and datetime types.
Benefits
-
Comprehensive support for logical operators.
-
Added support for index pushdown of
OR and NOTlogic. -
Supports any
AND, OR, and NOTcombination of multiple conditions.
-
-
Flexible and fast index management.
-
Create an inverted index when creating a table.
-
Add an inverted index to an existing table.
-
Delete an inverted index from a table.
-
Limitations
-
The
FLOATandDOUBLEfloating-point data types do not support an inverted index due to precision issues. Instead, use theDECIMALfixed-point data type, which supports an inverted index. -
Some complex data types do not support an inverted index, including
MAP,STRUCT,JSON,HLL,BITMAP,QUANTILE_STATE, andAGG_STATE. To use an inverted index withJSONdata, convert the column to theVARIANTdata type. -
You can create an inverted index on a field of a numeric type, but you cannot specify a
parser, such asenglish,chinese, orunicode. -
The
DUPLICATEmodel and theUNIQUEmodel with Merge on Write enabled support an inverted index on any column. In contrast, theAGGREGATEmodel and theUNIQUEmodel with Merge on Write disabled support an inverted index only on a Key column. Because these models must read and merge all data, the system cannot use the index for pre-filtering.
Create an index
You can create an inverted index during table creation or on a column in an existing table.
Create index on table creation
This is a synchronous operation. Index creation completes when the table is successfully created.
Inverted indexes have the following limitations in different data models:
-
For the aggregate key model, you can create an inverted index only on key columns.
-
For the unique key model, the Merge on Write feature must be enabled. Once enabled, you can create an inverted index on any column.
-
For the duplicate key model, you can create an inverted index on any column.
Syntax
CREATE TABLE [IF NOT EXISTS] [db_name.]<table_name>
(
<column_definition_list>,
[<index_definition_list>]
)
table_properties;
Parameters
Table creation parameters
|
Parameter |
Required |
Description |
|
db_name |
No |
The name of the database that will contain the table. |
|
table_name |
Yes |
The name of the table to create. |
|
column_definition_list |
Yes |
A list of column definitions. For more information, see CREATE-TABLE. |
|
table_properties |
Yes |
The table properties, such as the data model, partitioning and bucketing. For more information, see data model. |
|
index_definition_list |
No |
A list of index definitions. |
index_definition_list
You can define one or more indexes when you create a table. Use the following format: index_definition[, index_definition][, index_definition]....
index_definition
INDEX <index_name>(<column_name>) <index_type> [PROPERTIES("<key>" = "<value>")] [COMMENT '<comment>']
Parameters
Required parameters
|
Parameter |
Description |
|
index_name |
The name of the index. |
|
column_name |
The name of the column to index. |
|
index_type |
The index type. Set this to |
Optional parameters
Properties
The PROPERTIES clause specifies tokenization options for the index. It consists of one or more comma-separated key-value pairs in the format "<key>" = "<value>". If you are unsure how a given text will be tokenized, use the TOKENIZE function to view the output. For more information, see Tokenization functions.
|
Key |
Value |
|
parser |
Specifies the tokenizer. If this property is omitted, no tokenization occurs. The
|
|
parser_mode |
Specifies the word tokenization mode, which determines the tokenization granularity. All tokenizers use the coarse_grained mode by default. This mode tends to segment text into longer words. For example, When To learn more about how tokenization works, see Tokenization functions. |
|
support_phrase |
Specifies whether the index supports accelerated
|
|
char_filter |
Pre-processes strings before tokenization. Currently,
|
|
ignore_above |
Specifies a length limit for non-tokenized string values (when no
|
|
lower_case |
Specifies whether to convert tokenized terms to lowercase for case-insensitive matching.
|
|
stopwords |
Specifies a list of stopwords, which affects the tokenizer's behavior.
|
|
dict_compression |
Specifies whether to enable Zstandard (ZSTD) dictionary compression for the inverted index's dictionary.
Note
This parameter is supported only in versions 4.1.0 and later. |
Comment
|
Parameter |
Description |
|
comment |
A description of the index. |
Example: Create a table with an index
-- Create a table and an inverted index named idx_comment on the comment column.
-- USING INVERTED sets the index type to inverted index.
-- PROPERTIES("parser" = "english") sets the tokenizer to "english". Other supported tokenizers include "chinese" for Chinese text and "unicode" for mixed-language text. If the "parser" property is omitted, no tokenization occurs.
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;
Add an index
This operation is an asynchronous operation. You can check the index creation progress by using SHOW ALTER TABLE COLUMN;.
Syntax
ALTER TABLE <table_name> ADD INDEX <index_name>(<column_name>) <index_type> [PROPERTIES("<key>" = "<value>")];
Parameters
The parameters are the same as those for creating a table.
Examples
Add an index without tokenization.
ALTER TABLE user_tb ADD INDEX index_userId(user_id) USING INVERTED ;
Add an index that uses english for tokenization.
ALTER TABLE user_tb ADD INDEX index_city(city) USING INVERTED PROPERTIES("parser" = "english");
View inverted indexes
View index change progress
Modifying an inverted index using the ALTER and DROP commands is an asynchronous process. To view its progress, use the following statement.
SHOW ALTER TABLE COLUMN;
View all inverted table indexes
Syntax
SHOW INDEXES FROM <table_name>;
Example
SHOW INDEXES FROM user_tb;
Drop an index
Dropping an index is an asynchronous process. To check the progress, see Query the information about inverted indexes.
Dropping an index can decrease query performance. Proceed with caution.
Syntax
-- Syntax 1
DROP INDEX <index_name> ON <table_name>;
-- Syntax 2
ALTER TABLE <table_name> DROP INDEX <index_name>;
Example
DROP INDEX index_userId ON user_tb;
ALTER TABLE user_tb DROP INDEX index_city;
Inverted index
Full-text search
Syntax
SELECT * FROM <table_name> WHERE <column_name> <conditional_logic> '<keywords>';
Parameters
|
Parameter |
Required |
Description |
|
table_name |
Yes |
The name of the table. |
|
column_name |
Yes |
The name of the column. |
|
conditional_logic |
Yes |
A combination of search operators and logical operators. Logical operators: Search operators:
|
|
keywords |
Yes |
The search keywords. Separate multiple keywords with spaces. Example: |
Examples
-- Retrieve rows where logmsg contains keyword1.
SELECT * FROM log_tb WHERE logmsg MATCH_ANY 'keyword1';
-- Retrieve rows where logmsg contains keyword1 or keyword2.
SELECT * FROM log_tb WHERE logmsg MATCH_ANY 'keyword1 keyword2';
-- Retrieve rows where logmsg contains both keyword1 and keyword2.
SELECT * FROM log_tb WHERE logmsg MATCH_ALL 'keyword1 keyword2';
-- Retrieve rows where logmsg contains the exact phrase "keyword1 keyword2".
SELECT * FROM log_tb WHERE logmsg MATCH_PHRASE 'keyword1 keyword2';
Equality and range queries for numeric and date
The query syntax is standard SQL syntax.
Example
-- Equality, range, IN, and NOT IN queries
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');
Query performance comparison
This topic uses a hackernews dataset with 1 million rows to compare query performance with and without an inverted index.
Prerequisites
Step 1: Create a table.
-
Create a database.
CREATE DATABASE test_inverted_index; -
Switch to the created database.
USE test_inverted_index; -
Create the target table.
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; -- Creates a table and an inverted index named idx_comment on the comment column. -- USING INVERTED specifies that the index type is an inverted index. -- PROPERTIES("parser" = "english") specifies that the English tokenizer is used. You can also specify "chinese" for Chinese text or "unicode" for mixed-language text. If you do not specify the "parser" parameter, no tokenization is performed.
Step 2: Import data
-
Download the data file.
wget https://qa-build.oss-cn-beijing.aliyuncs.com/regression/index/hacknernews_1m.csv.gz -
Import the data using Stream Load.
On the instance details page for ApsaraDB for SelectDB, you can view the endpoint host and port number of an ApsaraDB for SelectDB instance. For more information about Stream Load, see 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 } -
Run a
count()query to verify the data import.SELECT count() FROM hackernews_1m; +---------+ | count() | +---------+ | 1000000 | +---------+ 1 row in set (0.02 sec)
Performance comparison
-
Count results may differ between queries that use an inverted index with a tokenizer and those that do not. This is because the inverted index tokenizes the column data and normalizes the terms (for example, by converting them to lowercase), which can cause queries that use the index to match more rows.
-
The performance difference in some examples may not be significant because the dataset is small. The larger the dataset, the greater the performance improvement.
Full-text search
-
Count rows where the
commentcolumn containsOLAP.-
Counting the number of rows in the
commentcolumn that containOLAPby using theLIKEoperator takes 0.18s.SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLAP%'; +---------+ | count() | +---------+ | 34 | +---------+ 1 row in set (0.18 sec) -
The
MATCH_ANYfull-text search based on an inverted index counts the number of rows in the comment column that containOLAPin 0.02s. This is 9 times faster than using theLIKEoperator.SELECT count() FROM hackernews_1m WHERE comment MATCH_ANY 'OLAP'; +---------+ | count() | +---------+ | 35 | +---------+ 1 row in set (0.02 sec)
-
-
Count rows where the
commentcolumn containsOLTP.-
This operation counts the number of rows in the
commentcolumn that containOLTPby using theLIKEoperator and takes 0.07s.SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLTP%'; +---------+ | count() | +---------+ | 48 | +---------+ 1 row in set (0.07 sec) -
The
MATCH_ANYfull-text search based on an inverted index counts the number of rows in thecommentcolumn that containOLTPin 0.01s. This is 7 times faster than using theLIKEoperator.SELECT count() FROM hackernews_1m WHERE comment MATCH_ANY 'OLTP'; +---------+ | count() | +---------+ | 51 | +---------+ 1 row in set (0.01 sec)
-
-
Count rows where the
commentcolumn contains bothOLAPandOLTP.-
Using
LIKE, the query takes 0.13s.SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLAP%' AND comment LIKE '%OLTP%'; +---------+ | count() | +---------+ | 14 | +---------+ 1 row in set (0.13 sec) -
Using full-text search with
MATCH_ALL, the query takes 0.01s, making it 13 times faster than usingLIKE.SELECT count() FROM hackernews_1m WHERE comment MATCH_ALL 'OLAP OLTP'; +---------+ | count() | +---------+ | 15 | +---------+ 1 row in set (0.01 sec)
-
-
Count rows where the
commentcolumn contains eitherOLAPorOLTP.-
Using
LIKE, the query takes 0.12s.SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLAP%' OR comment LIKE '%OLTP%'; +---------+ | count() | +---------+ | 68 | +---------+ 1 row in set (0.12 sec) -
Using full-text search with
MATCH_ANY, the query takes 0.01s, making it 12 times faster than usingLIKE.SELECT count() FROM hackernews_1m WHERE comment MATCH_ANY 'OLAP OLTP'; +---------+ | count() | +---------+ | 71 | +---------+ 1 row in set (0.01 sec)
-
Equality and range queries
-
Compare the performance of a range query on a
DateTimeV2column.-
Without an inverted index, a query to count rows where
timestampis greater than2007-08-23 04:17:00takes 0.03s.SELECT count() FROM hackernews_1m WHERE timestamp > '2007-08-23 04:17:00'; +---------+ | count() | +---------+ | 999081 | +---------+ 1 row in set (0.03 sec) -
Add an inverted index to the
timestampcolumn.CREATE INDEX idx_timestamp ON hackernews_1m(timestamp) USING INVERTED; Query OK, 0 rows affected (0.03 sec) -
Check the index creation progress. The difference between FinishTime and CreateTime shows that creating the inverted index for 1 million rows on the timestamp column took only 1 second.
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) -
After an inverted index is created, running the same query to count data where the timestamp column is greater than
2007-08-23 04:17:00takes 0.01 seconds. This is an improvement of 2 seconds compared to the query speed without an inverted index.SELECT count() FROM hackernews_1m WHERE timestamp > '2007-08-23 04:17:00'; +---------+ | count() | +---------+ | 999081 | +---------+ 1 row in set (0.01 sec)
-
-
Compare the performance of an equality query on a numeric column.
-
Without an inverted index, a query to count rows where the parent column equals 11189 takes 0.01s.
SELECT count() FROM hackernews_1m WHERE parent = 11189; +---------+ | count() | +---------+ | 2 | +---------+ 1 row in set (0.01 sec) -
Create an inverted index on the numeric
parentcolumn without a tokenizer.-- For numeric types, you do not need to specify a tokenizer when using INVERTED. -- ALTER TABLE ... ADD INDEX is an alternative syntax for creating an index. ALTER TABLE hackernews_1m ADD INDEX idx_parent(parent) USING INVERTED; Query OK, 0 rows affected (0.01 sec) -
Check the index creation progress.
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 | +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+ -
Run the same query again. The query time remains 0.01s, showing no significant change for this dataset.
SELECT count() FROM hackernews_1m WHERE parent = 11189; +---------+ | count() | +---------+ | 2 | +---------+ 1 row in set (0.01 sec)
-
-
Compare the performance of an equality query on a string column.
-
Without an inverted index, a query to count rows where the author column equals 'faster' takes 0.03s.
SELECT count() FROM hackernews_1m WHERE author = 'faster'; +---------+ | count() | +---------+ | 20 | +---------+ 1 row in set (0.03 sec) -
Create an inverted index on the
authorcolumn without a tokenizer.-- In this example, only USING INVERTED is specified. The values in the author column are not tokenized, and each value is treated as a single term. ALTER TABLE hackernews_1m ADD INDEX idx_author(author) USING INVERTED; Query OK, 0 rows affected (0.01 sec) -
Check the index creation progress.
-- It takes only 1.5s to incrementally create an index on the author column with 1 million rows of data. 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 | +-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+ -
After creating the index, the query takes only 0.01s, making it 3 times faster.
-- After the index is created, string equality matching is also significantly accelerated. SELECT count() FROM hackernews_1m WHERE author = 'faster'; +---------+ | count() | +---------+ | 20 | +---------+ 1 row in set (0.01 sec)
-
Tokenize function
The TOKENIZE function breaks a text string into a sequence of terms. Tokenization is a core component for building and using an inverted index. The quality of tokenization directly impacts index performance.
To see how a text string is tokenized, use the TOKENIZE function to view the result. The TOKENIZE function has two main parameters: parser and parser_mode. The following table describes these parameters.
|
Parameter |
Description |
|
|
Specifies the tokenizer to use. If this parameter is omitted, the function does not perform tokenization.
|
|
|
Specifies the tokenization mode, which determines the tokenization granularity. All tokenizers use the coarse_grained mode by default. This mode tends to segment text into longer words. For example, the string When |
Examples:
-- English tokenization result.
SELECT TOKENIZE('I love CHINA','"parser"="english"');
+------------------------------------------------+
| tokenize('I love CHINA', '"parser"="english"') |
+------------------------------------------------+
| ["i", "love", "china"] |
+------------------------------------------------+
1 row in set (0.02 sec)
-- Fine-grained tokenization result from the Chinese tokenizer.
SELECT TOKENIZE('武汉长江大桥','"parser"="chinese","parser_mode"="fine_grained"');
+-----------------------------------------------------------------------------------+
| tokenize('武汉长江大桥', '"parser"="chinese","parser_mode"="fine_grained"') |
+-----------------------------------------------------------------------------------+
| ["武汉", "武汉长江大桥", "长江", "长江大桥", "大桥"] |
+-----------------------------------------------------------------------------------+
1 row in set (0.02 sec)
-- Coarse-grained tokenization result from the Chinese tokenizer.
SELECT TOKENIZE('武汉市长江大桥','"parser"="chinese","parser_mode"="coarse_grained"');
+----------------------------------------------------------------------------------------+
| tokenize('武汉市长江大桥', '"parser"="chinese","parser_mode"="coarse_grained"') |
+----------------------------------------------------------------------------------------+
| ["武汉市", "长江大桥"] |
+----------------------------------------------------------------------------------------+
1 row in set (0.02 sec)
-- Mixed-language tokenization result.
SELECT TOKENIZE('I love CHINA 我爱我的祖国','"parser"="unicode"');
+-------------------------------------------------------------------+
| tokenize('I love CHINA 我爱我的祖国', '"parser"="unicode"') |
+-------------------------------------------------------------------+
| ["i", "love", "china", "我", "爱", "我", "的", "祖", "国"] |
+-------------------------------------------------------------------+
1 row in set (0.02 sec)