创建文本索引
tokenizer 参数用于指定分词器:
splitByNonAlpha按非字母数字的 ASCII 字符拆分字符串 (另见函数 splitByNonAlpha) 。splitByString(S)按用户定义的特定分隔符字符串S拆分字符串 (另见函数 splitByString) 。 分隔符可通过可选参数指定,例如tokenizer = splitByString([', ', '; ', '\n', '\\'])。 请注意,每个分隔符字符串都可以由多个字符组成 (如示例中的', ') 。 如果未显式指定,默认分隔符列表 (例如tokenizer = splitByString) 为单个空格[' ']。ngrams(N)将字符串拆分为等长的N-gram (另见函数 ngrams) 。 ngram 的长度可通过 2 到 8 之间的可选整数参数指定,例如tokenizer = ngrams(3)。 如果未显式指定,默认 ngram 大小 (例如tokenizer = ngrams) 为 3。array不执行分词,即每个行值都是一个标记 (另见函数 array) 。sparseGrams(min_length, max_length, min_cutoff_length)— 使用与 sparseGrams 函数相同的算法,将字符串拆分为所有长度为min_length的 ngram,以及若干长度更大、最大可达max_length(含) 的 ngram。如果指定了min_cutoff_length,则只有长度大于或等于min_cutoff_length的 N-gram 会保存到索引中。与仅生成固定长度 N-gram 的ngrams(N)不同,sparseGrams会在指定范围内生成一组可变长度的 N-gram,从而更灵活地表示文本上下文。例如,tokenizer = sparseGrams(3, 5, 4)会从输入字符串生成 3-、4-、5-gram,但只将 4- 和 5-gram 保存到索引中。
splitByString 分词器会按从左到右的顺序应用分隔符。
这可能会产生歧义。
例如,分隔符字符串 ['%21', '%'] 会使 %21abc 被分词为 ['abc'];而如果将这两个分隔符字符串改为 ['%', '%21'],输出则会变为 ['21abc']。
大多数情况下,你会希望优先匹配更长的分隔符。
通常可以通过按长度降序传入分隔符字符串来实现。
如果这些分隔符字符串恰好构成前缀码,则可以按任意顺序传入。preprocessor 是一个表达式,用于在分词前转换输入字符串。
preprocessor 参数的典型用法包括:
- 将输入字符串转换为小写 (或大写) ,以支持不区分大小写的匹配,例如 lower、lowerUTF8;请参见下面的第一个示例。
- UTF-8 规范化,例如 normalizeUTF8NFC、normalizeUTF8NFD、normalizeUTF8NFKC、normalizeUTF8NFKD、toValidUTF8。
- 删除或转换不需要的字符或子字符串,例如 extractTextFromHTML、substring、idnaEncode。
preprocessor 表达式必须将类型为 String 或 FixedString 的输入值转换为相同类型的值。
示例:
INDEX idx(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(col))INDEX idx(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = substringIndex(col, '\n', 1))INDEX idx(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(extractTextFromHTML(col))
preprocessor 表达式只能引用定义 text index 时所基于的列。
不允许使用非确定性函数。
函数 hasToken、hasAllTokens 和 hasAnyTokens 会先使用 preprocessor 转换搜索词,再对其进行分词。
例如:
可选高级参数
可选高级参数
以下高级参数的默认值在几乎所有情况下都适用。
我们不建议修改它们。可选参数
dictionary_block_size (默认值:128) 用于指定字典块的大小 (以行为单位) 。可选参数 dictionary_block_frontcoding_compression (默认值:1) 用于指定字典块是否使用前缀编码进行压缩。可选参数 max_cardinality_for_embedded_postings (默认值:16) 用于指定基数阈值;低于该阈值时,倒排列表会嵌入到字典块中。可选参数 bloom_filter_false_positive_rate (默认值:0.1) 用于指定字典布隆过滤器的误报率。使用文本索引
支持的函数
WHERE 子句中使用了文本函数,则可以使用文本索引:
= and !=
= (equals) 和 != (notEquals) 会匹配给定搜索词的完整内容。
示例:
= 和 !=,但只有在使用 array 分词器时,等值和不等值搜索才有意义 (这会使索引存储整行的值) 。
IN and NOT IN
IN (in) 和 NOT IN (notIn) 与函数 equals 和 notEquals 类似,但它们分别匹配所有 (IN) 或完全不匹配 (NOT IN) 搜索词。
示例:
= 和 != 相同的限制;也就是说,IN 和 NOT IN 只有与 array 分词器配合使用时才有意义。
LIKE、NOT LIKE 和 match
目前,只有当索引分词器为
splitByNonAlpha 或 ngrams 时,这些函数才会使用文本索引进行过滤。LIKE like、NOT LIKE (notLike) 以及 match 函数与文本索引配合使用,ClickHouse 必须能够从搜索词中提取完整的标记。
示例:
support 可以匹配 support、supports、supporting 等。
这种查询属于子串查询,无法通过文本索引加速。
要让 LIKE 查询利用文本索引,必须将 LIKE 模式改写为以下形式:
support 左右两侧的空格可确保该术语能被提取为一个标记。
startsWith 和 endsWith
LIKE 类似,startsWith 和 endsWith 这两个函数只有在能从搜索词中提取出完整标记时,才能使用文本索引。
示例:
clickhouse 会被视为一个标记。
support 不算标记,因为它可以匹配 support、supports、supporting 等形式。
要查找所有以 clickhouse supports 开头的行,请在搜索模式末尾加上一个空格:
endsWith 也应搭配前导空格使用:
hasToken and hasTokenOrNull
hasToken 和 hasTokenOrNull 是与 text 索引配合使用时性能最佳的函数。
hasAnyTokens 和 hasAllTokens
has
mapContains
mapContainsKey 的别名) 用于匹配 map 键中的单个标记。
示例:
operator[]
Array(T) 和 Map(K, V) 配合使用。
文本索引对 Array 和 Map 的支持示例。
为 Array(String) 创建索引
clickhouse) 的帖子,就需要扫描所有记录:
keywords 数组。
为了解决这个性能问题,我们可以为 keywords 定义一个文本索引,构建针对搜索优化的结构,对所有关键词预先处理,从而实现即时查找:
重要:添加文本索引后,必须为现有数据将其重新构建:
为 Map 建立索引
- 查找所有包含限流信息的日志:
- 查找特定 IP 的所有日志:
重要:添加文本索引后,必须为现有数据重新构建索引:
- 查找所有被限流的请求:
- 查找特定 IP 的所有日志:
实现
索引布局
- 一个字典,将每个标记映射到对应的倒排列表;以及
- 一组倒排列表,其中每个倒排列表都表示一组行号。
dictionary_block_size 配置) 。
字典块文件 (.dct) 包含一个分片中所有索引粒度的全部字典块。
索引粒度文件 (.idx)
索引粒度文件为每个字典块保存以下信息:该块的第一个标记、它在字典块文件中的相对偏移量,以及该块中所有标记的布隆过滤器。
这种稀疏索引结构类似于 ClickHouse 的稀疏主键索引)。
如果要查找的标记不在某个字典块中,布隆过滤器可以提前跳过该字典块。
倒排列表文件 (.pst)
所有标记对应的倒排列表都会按顺序存放在倒排列表文件中。
为了节省空间,同时仍支持快速执行交集和并集操作,倒排列表以 roaring bitmaps 的形式存储。
如果某个倒排列表的基数小于 16 (可通过参数 max_cardinality_for_embedded_postings 配置) ,则会将其直接嵌入字典中。
直接读取
- 设置 query_plan_direct_read_from_text_index (默认值:1) ,用于指定是否全局启用直接读取。
- 设置 use_skip_indexes_on_data_read (默认值:1) ,这是直接读取的另一个前置条件。请注意,在 compatibility < 25.10 的 ClickHouse 数据库中,
use_skip_indexes_on_data_read默认处于禁用状态,因此你需要提高 compatibility 设置值,或显式执行SET use_skip_indexes_on_data_read = 1。
ALTER TABLE ... MATERIALIZE INDEX 完成) 。
支持的函数
直接读取优化支持 hasToken、hasAllTokens 和 hasAnyTokens 函数。
这些函数也可以通过 AND、OR 和 NOT 运算符组合使用。
WHERE 子句还可以包含额外的非文本搜索函数过滤器 (针对文本列或其他列) ——在这种情况下,仍会使用直接读取优化,但效果会打折扣 (它仅适用于受支持的文本搜索函数) 。
要判断某个查询是否使用了直接读取,请使用 EXPLAIN PLAN actions = 1 运行该查询。
例如,一个禁用了直接读取的查询
query_plan_direct_read_from_text_index = 1 运行相同查询时
__text_index_<index_name>_<function_name>_<id>。
如果存在此列,则表示使用了直接读取。
示例:Hackernews 数据集
hackernews 表中:
ALTER TABLE 在 comment 列上添加文本索引,然后将其物化:
hasToken、hasAnyTokens 和 hasAllTokens 函数执行查询。
下面的示例将展示标准索引扫描与直接读取优化之间巨大的性能差异。
1. 使用 hasToken
hasToken 用于检查文本是否包含某个特定的单个标记。
我们将搜索区分大小写的标记 ‘ClickHouse’。
禁用直接读取 (标准扫描)
默认情况下,ClickHouse 会使用跳过索引筛选粒度,然后再读取这些粒度的列数据。
我们可以通过禁用直接读取来模拟这种行为。
2. 使用 hasAnyTokens
hasAnyTokens 用于检查文本是否包含给定标记中的至少一个。
我们将搜索包含 ‘love’ 或 ‘ClickHouse’ 的评论。
已禁用直接读取 (标准扫描)
3. 使用 hasAllTokens
hasAllTokens 用于检查文本是否包含给定的所有标记。
我们将搜索同时包含 ‘love’ 和 ‘ClickHouse’ 的评论。
禁用直接读取 (标准扫描)
即使禁用了直接读取,标准跳过索引依然有效。
它将 2870 万行过滤到仅 14.746 万行,但仍必须从该列读取 57.03 MB 数据。
4. 复合搜索:OR、AND、NOT、…
hasAnyTokens(comment, ['ClickHouse', 'clickhouse']) 是更推荐、也更高效的写法。