使用全文索引进行全文搜索
全文索引是一种实验性的二级索引类型,为 String 或 FixedString 列提供快速文本搜索功能。全文索引的主要思想是存储从“词项”到包含这些词项的行的映射。“词项”是字符串列的分词单元。例如,字符串单元“I will be a little late”默认被分词为六个词项“I”、“will”、“be”、“a”、“little”和“late”。另一种分词器是 n-gram。例如,3-gram 分词的结果将是 21 个词项“I w”、“ wi”、“wil”、“ill”、“ll ”、“l b”、“ be”等等。输入字符串的分词粒度越细,生成的全文索引就越大,但也越有用。
全文索引是实验性的,尚不应在生产环境中使用。它们将来可能会以向后不兼容的方式进行更改,例如在其 DDL/DQL 语法或性能/压缩特性方面。
用法
要使用全文索引,首先在配置中启用它们
SET allow_experimental_full_text_index = true;
可以使用以下语法在字符串列上定义全文索引
CREATE TABLE tab
(
`key` UInt64,
`str` String,
INDEX inv_idx(str) TYPE full_text(0) GRANULARITY 1
)
ENGINE = MergeTree
ORDER BY key
在 ClickHouse 的早期版本中,相应的索引类型名称为 inverted
。
其中 N
指定分词器
full_text(0)
(或更短:full_text()
)将分词器设置为“tokens”,即沿空格分割字符串,full_text(N)
,其中N
在 2 到 8 之间,将分词器设置为“ngrams(N)”
每个倒排列表的最大行数可以指定为第二个参数。此参数可用于控制倒排列表的大小,以避免生成巨大的倒排列表文件。存在以下变体
full_text(ngrams, max_rows_per_postings_list)
:使用给定的 max_rows_per_postings_list(假设它不为 0)full_text(ngrams, 0)
:对每个倒排列表的最大行数没有限制full_text(ngrams)
:使用默认的最大行数,即 64K。
作为一种跳跃索引类型,全文索引可以在表创建后删除或添加到列中
ALTER TABLE tab DROP INDEX inv_idx;
ALTER TABLE tab ADD INDEX inv_idx(s) TYPE full_text(2);
要使用索引,不需要特殊的函数或语法。典型的字符串搜索谓词会自动利用索引。例如,考虑
INSERT INTO tab(key, str) values (1, 'Hello World');
SELECT * from tab WHERE str == 'Hello World';
SELECT * from tab WHERE str IN ('Hello', 'World');
SELECT * from tab WHERE str LIKE '%Hello%';
SELECT * from tab WHERE multiSearchAny(str, ['Hello', 'World']);
SELECT * from tab WHERE hasToken(str, 'Hello');
全文索引也适用于 Array(String)
、Array(FixedString)
、Map(String)
和 Map(String)
类型的列。
与其他二级索引一样,每个列 part 都有自己的全文索引。此外,每个全文索引在内部都分为“段”。段的存在和大小通常对用户是透明的,但段大小决定了索引构建期间的内存消耗(例如,当合并两个 part 时)。配置参数“max_digestion_size_per_segment”(默认值:256 MB)控制从底层列读取的数据量,之后会创建一个新段。增加此参数会增加索引构建的中间内存消耗,但也提高了查找性能,因为平均而言,评估查询时需要检查的段更少。
Hacker News 数据集的全文搜索
让我们看看全文索引在具有大量文本的大型数据集上的性能提升。我们将使用流行的 Hacker News 网站上 2870 万行的评论。这是没有全文索引的表
CREATE TABLE hackernews (
id UInt64,
deleted UInt8,
type String,
author String,
timestamp DateTime,
comment String,
dead UInt8,
parent UInt64,
poll UInt64,
children Array(UInt32),
url String,
score UInt32,
title String,
parts Array(UInt32),
descendants UInt32
)
ENGINE = MergeTree
ORDER BY (type, author);
这 2870 万行位于 S3 中的 Parquet 文件中 - 让我们将它们插入到 hackernews
表中
INSERT INTO hackernews
SELECT * FROM s3Cluster(
'default',
'https://datasets-documentation.s3.eu-west-3.amazonaws.com/hackernews/hacknernews.parquet',
'Parquet',
'
id UInt64,
deleted UInt8,
type String,
by String,
time DateTime,
text String,
dead UInt8,
parent UInt64,
poll UInt64,
kids Array(UInt32),
url String,
score UInt32,
title String,
parts Array(UInt32),
descendants UInt32');
考虑以下在 comment
列中搜索词项 ClickHouse
(及其各种大小写形式)的简单搜索
SELECT count()
FROM hackernews
WHERE hasToken(lower(comment), 'clickhouse');
请注意,执行查询需要 3 秒
┌─count()─┐
│ 1145 │
└─────────┘
1 row in set. Elapsed: 3.001 sec. Processed 28.74 million rows, 9.75 GB (9.58 million rows/s., 3.25 GB/s.)
我们将使用 ALTER TABLE
在 comment
列的小写形式上添加全文索引,然后将其物化(这可能需要一段时间 - 等待它物化)
ALTER TABLE hackernews
ADD INDEX comment_lowercase(lower(comment)) TYPE full_text;
ALTER TABLE hackernews MATERIALIZE INDEX comment_lowercase;
我们运行相同的查询...
SELECT count()
FROM hackernews
WHERE hasToken(lower(comment), 'clickhouse')
...并注意到查询执行速度快了 4 倍
┌─count()─┐
│ 1145 │
└─────────┘
1 row in set. Elapsed: 0.747 sec. Processed 4.49 million rows, 1.77 GB (6.01 million rows/s., 2.37 GB/s.)
我们还可以搜索一个或多个词项,即析取或合取
-- multiple OR'ed terms
SELECT count(*)
FROM hackernews
WHERE multiSearchAny(lower(comment), ['oltp', 'olap']);
-- multiple AND'ed terms
SELECT count(*)
FROM hackernews
WHERE hasToken(lower(comment), 'avx') AND hasToken(lower(comment), 'sve');
与其他二级索引不同,全文索引(目前)映射到行号(行 ID),而不是 granule ID。这种设计的原因是为了性能。在实践中,用户经常一次搜索多个词项。例如,过滤器谓词 WHERE s LIKE '%little%' OR s LIKE '%big%'
可以通过形成词项“little”和“big”的行 ID 列表的并集来直接使用全文索引进行评估。这也意味着提供给索引创建的参数 GRANULARITY
没有意义(将来可能会从语法中删除)。