倒排索引是資訊檢索領域常用的索引技術。它通過將文本分割成單詞來構建索引,以便快速尋找單詞在文檔中的出現位置。ApsaraDB for 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。
|
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匯入資料。
您可以在ApsaraDB for SelectDB的執行個體詳情頁面查看ApsaraDB for 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)