テキスト索引の作成
テキスト索引は、compatibility 設定に関係なく、ClickHouse バージョン >= 26.2 であれば使用できます。
Query
- String と FixedString
- Array(String) と Array(FixedString)
- Map (mapKeys および mapValues 関数経由)
- JSON (JSONAllPaths および
JSONAllValues関数経由)
Array(Nullable(String or FixedString)) も含まれます。
また、既存のテーブルにテキスト索引を追加するには:
Query
Query
Query
tokenizer 引数では、トークナイザーを指定します。
splitByNonAlphaは、ASCII の英数字以外の文字で String を分割します (関数 splitByNonAlpha を参照) 。splitByString(S)は、ユーザー定義の区切り StringSで String を分割します (関数 splitByString を参照) 。 区切り文字は省略可能なパラメータで指定できます。たとえば、tokenizer = splitByString([', ', '; ', '\n', '\\'])のように指定します。 各 String は複数文字で構成することもできます (例では', ') 。 明示的に指定しない場合 (たとえばtokenizer = splitByString) 、デフォルトの区切り文字リストは単一の空白文字[' ']です。asciiCJKは、Unicode の単語境界規則を使用して String をトークンに分割します (Unicode Text Segmentation (UAX #29) に類似) 。 ASCII の英数字とアンダースコアは、コネクタ (文字に対する ASCII:、同種の文字に対する.および') を含むトークンを構成します。非 ASCII の Unicode 文字は、CJK 文字を含め、1 文字のトークンになります。ngrams(N)は、String を同じ長さのN-gram に分割します (関数 ngrams を参照) 。 ngram の長さは、1 から 8 までの省略可能な整数パラメータで指定できます。たとえば、tokenizer = ngrams(3)のように指定します。 明示的に指定しない場合 (たとえばtokenizer = ngrams) 、デフォルトの ngram サイズは 3 です。sparseGrams(min_length, max_length, min_cutoff_length)は、min_length文字以上max_length文字以下 (両端を含む) の可変長 n-gram に String を分割します (関数 sparseGrams を参照) 。 明示的に指定しない限り、min_lengthとmax_lengthのデフォルト値は 3 と 100 です。 パラメータmin_cutoff_lengthを指定した場合、長さがmin_cutoff_length以上の n-gram のみが返されます。ngrams(N)と比べると、sparseGramsトークナイザーは可変長の N-gram を生成するため、元のテキストをより柔軟に表現できます。 たとえば、tokenizer = sparseGrams(3, 5, 4)では、内部的には入力 String から 3-gram、4-gram、5-gram を生成しますが、返されるのは 4-gram と 5-gram のみです。arrayはトークン化を行いません。つまり、各行の値がトークンになります (関数 array を参照) 。
splitByString トークナイザーは、分割区切り文字を左から右の順に適用します。
そのため、曖昧さが生じることがあります。
たとえば、区切り String ['%21', '%'] を指定すると、%21abc は ['abc'] としてトークン化されます。一方、区切り String の順序を ['%', '%21'] に入れ替えると、出力は ['21abc'] になります。
多くの場合、より長い区切り文字が優先的に一致するようにするのが望ましいでしょう。
通常は、区切り String を長さの降順で渡すことでこれを実現できます。
区切り String がたまたま prefix code を構成している場合は、任意の順序で渡せます。Query
Response
asciiCJK トークナイザーの使用を推奨します。
:::
プリプロセッサ引数 (任意) 。プリプロセッサとは、トークン化の前に入力文字列へ適用される式を指します。
プリプロセッサ引数の一般的な用途としては、次のようなものがあります
- 大文字/小文字の変換、または大文字小文字を区別しないマッチングを可能にするケースフォールディング。例: lower, lowerUTF8, caseFoldUTF8。
- UTF-8 の正規化。例: normalizeUTF8NFC, normalizeUTF8NFD, normalizeUTF8NFKC, normalizeUTF8NFKD, normalizeUTF8NFKCCasefold, toValidUTF8。
- アクセント記号など、不要な文字や部分文字列の削除または変換。例: extractTextFromHTML, substring, idnaEncode, translate, removeDiacriticsUTF8。
Nullable(T) または LowCardinality(T) 型のカラムに対して構築されている場合、プリプロセッサ式は nullable または low-cardinality の値を受け入れられる必要があります (つまり、例外をスローしてはいけません) 。
例:
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)))INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = removeDiacriticsUTF8(caseFoldUTF8(col)))
INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = upper(lower(col)))INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = concat(lower(col), lower(col)))- 許可されません:
INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = concat(col, col))
プリプロセッサは原則として、索引対象のカラムまたは式をプリプロセッサ式でラップすることと同等です。
例えば、
INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(col)) における lower プリプロセッサは、INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha') によって再現できます。
後者の形式には、再現されたプリプロセッサが WHERE 句のフィルタ条件に一致する場合にのみ適用されるという欠点があります。
例えば、WHERE hasAllTokens(lower(col), [...]) は一致しますが、WHERE hasAllTokens(col, [...]) は一致しません。
そのため、最適なユーザー体験のために、プリプロセッサ式の使用を推奨します。SETTINGS use_skip_indexes = 0) とで異なる場合があることに注意してください。
例えば、
Query
Query
Query
Query
Query
Response
テキスト索引の使用
テキスト索引の検索には、関数
hasAnyTokens および hasAllTokens の使用を推奨します。詳しくは以下を参照してください。
これらの関数は、利用可能なすべてのトークナイザーと、あらゆるプリプロセッサ式に対応しています。
一方、その他のサポート対象の関数は歴史的にテキスト索引より前から存在していたため、多くの場合で従来の動作を維持する必要がありました (例: プリプロセッサをサポートしない) 。サポートされている関数
WHERE 句または PREWHERE 句で使用している場合は、テキスト索引を利用できます。
=
= (equals) は、指定された検索語全体と一致します。
例:
IN
IN (in) は equals と似ていますが、すべての検索語句に一致します。
例:
テキスト索引では、
NOT IN (notIn) はサポートされていません。LIKE と match
現在、これらの関数でフィルタリングにテキスト索引が使用されるのは、索引のトークナイザーが
splitByNonAlpha、ngrams、または sparseGrams のいずれかである場合に限られます。NOT LIKE (notLike) はテキスト索引ではサポートされていません。LIKE (like) および match 関数を使用するには、ClickHouse が検索語から完全なトークンを抽出できる必要があります。
ngrams トークナイザーを使用する索引では、ワイルドカードに挟まれた検索文字列の長さが N-gram の長さ以上であれば、これに該当します。
splitByNonAlpha トークナイザーを使用するテキスト索引の例:
support はこの例では、support、supports、supporting などに一致する可能性があります。
この種のクエリは部分文字列クエリであり、テキスト索引で高速化することはできません。
LIKE クエリでテキスト索引を活用するには、LIKE パターンを次のように書き換える必要があります。
support の左右に空白があることで、その語を token として抽出できます。
幸い、ClickHouse が転置索引を活用して LIKE クエリを大幅に高速化できる特別なケースがあります。
詳しくは、LIKE/ILIKE パフォーマンスチューニングのセクションを参照してください。
multiSearchAny と multiMatchAny
LIKE および match と同じ条件でテキスト索引を使用します (上記参照) 。つまり、ClickHouse が各 needle から完全なトークンを抽出でき、かつ needles のリストが定数である必要があります。
いずれかの needle が含まれている可能性があるグラニュールは読み取られます。
multiMatchAny では、1 つの pattern をトークン要件に還元できない場合 (たとえば任意の document に一致する .* など) 、テキスト索引は使用できず、クエリは完全走査にフォールバックします。
LIKE や match と同様に、部分文字列検索と正規表現検索は ngrams および sparseGrams トークナイザーで最も効果的に機能します。
これらのトークナイザーは、互いに重なり合う文字 N-gram を索引化します。そのため needle は N-gram に分解され、単語の途中で始まるか終わるかにかかわらず、needle が部分文字列として現れる箇所であれば索引内に存在します。
したがって、needle は N-gram サイズ以上の長さがあれば、そのまま使用できます。
ngrams トークナイザーを使ったテキスト索引の例:
splitByNonAlpha トークナイザーは完全なトークン (単語全体) だけを索引付けします。
needle は単語の途中で始まったり終わったりすることがあるため、ClickHouse は各 needle の先頭と末尾のトークンを除外します。そのため、索引がグラニュールを絞り込めるのは、完全なトークンを使う場合に限られます。
splitByNonAlpha で substring 検索や正規表現検索に索引を利用させるには、各 needle を区切り文字 (スペースなど) で囲み、1 つ以上の完全なトークンになるようにします。
splitByNonAlpha トークナイザーを使用したテキスト索引の例:
startsWith and endsWith
LIKE と同様に、関数 startsWith と endsWith も、検索語から完全な token を抽出できる場合にのみ、テキスト索引を利用できます。
ngrams トークナイザーを使用する索引では、ワイルドカードに挟まれた検索文字列の長さが ngram 長以上の場合に該当します。
splitByNonAlpha トークナイザーを使用するテキスト索引の例:
clickhouse のみです。
support は support、supports、supporting などに一致する可能性があるため、トークンではありません。
clickhouse supports で始まるすべての行を検索するには、検索パターンの末尾にスペースを入れてください:
endsWith も先頭にスペースを付けて使用する必要があります。
hasToken と hasTokenOrNull
関数
hasToken は一見簡単に使えそうですが、デフォルト以外のトークナイザーやプリプロセッサ式を使う場合には、いくつか注意点があります。
代わりに、関数 hasAnyTokens と hasAllTokens を使用することを推奨します。hasAnyTokens と hasAllTokens
hasPhrase
hasAllTokens とは異なり、hasPhrase ではそれらが連続した並びとして出現する必要があります。
検索フレーズは、索引対象のカラムに設定されているものと同じトークナイザーでトークン化されます。
この関数を使用するには、splitByNonAlpha、splitByString、ngrams、asciiCJK のいずれかのトークナイザーが必要です。
例:
has
hasAny and hasAll
mapContains
mapContainsKey のエイリアス) は、マップのキーに対して、検索文字列から抽出されたトークンとの照合を行います。
この動作は、String カラムに対する equals 関数と似ています。
テキスト索引が使用されるのは、mapKeys(map) 式に対して作成されている場合のみです。
例:
mapContainsValue
String カラムに対する equals 関数に似ています。
テキスト索引が使用されるのは、mapValues(map) 式に対して作成されている場合のみです。
例:
mapContainsKeyLike and mapContainsValueLike
operator[]
mapKeys(map) または mapValues(map) 式、あるいはその両方に対して作成されている場合のみです。
例:
Array(T) 型および Map(K, V) 型のカラムを使用する方法については、以下の例を参照してください。
Array(String) カラムの索引作成
clickhouse) を含む投稿を見つけるには、すべてのエントリをスキャンする必要があります。
keywords 配列をすべて調べる必要があるため、これは次第に遅くなります。
このパフォーマンス上の問題を解決するため、カラム keywords にテキスト索引を定義します。
Mapカラムの索引付け
JSONカラムの索引付け
JSONカラムに対して次の 3 つの方法で使用できます。
- 特定のサブカラムに対する索引 — 通常のカラムと同じように、既知の JSON パスにテキスト索引を作成します。これにより、そのパスにある値が索引化されます。
- JSONAllPaths を使用したパスベースの索引 — 各グラニュールに存在するすべてのパスを索引化し、クエリ対象のパスを含み得ないグラニュールをスキップします。
Mapカラムの場合と同様です。 - JSONAllValues を使用した値ベースの索引 — すべての JSON パスにまたがるすべての値を索引化し、単一の索引で任意の JSON サブカラムに対する全文検索を高速化します。
特定のサブカラムに対する索引
- JSON 型ヒントで宣言された 型付きパス — 名前で直接アクセスします:
json.a - 明示的にキャストする 動的パス —
::キャスト構文を使用します:json.b::String
Query
Query
Response
Query
Response
JSONAllPaths を使用したパスベースの索引
Map カラムと同様に、JSON カラムでも JSONAllPaths を使ってテキスト索引を作成できます。
この索引は各グラニュールに存在する JSON パスの集合を格納し、クエリされたパスが存在しないグラニュールをスキップするために利用されます。
索引定義の例:
Query
EXPLAIN indexes = 1 を使用すると、スキップ索引が使われていることを確認できます。
あるパスが一方のパートにしか存在しない場合、索引によってもう一方のパートはスキップされます。
例:
Query
Response
Query
Response
IS NOT NULL でも索引が使用され、path が存在しないグラニュールはスキップされます (その場合、値は NULL になるためです) :
例:
Query
Response
JSONAllValues を使用した値ベースの索引
JSONAllValues を介して JSON カラムに対する検索を高速化できます。
JSONAllValues は、JSON カラム内のすべての値を Array(String) として返します。
文字列以外のデータ型の値 (たとえば整数や配列) は、テキスト表現に変換されます。
JSONAllValues を使って構築したテキスト索引は、各行のすべての JSON パスにまたがるこれらのテキスト表現に索引を作成します。
この索引により、個々の JSON サブカラムで絞り込むクエリを高速化できます。
クエリが特定のサブカラムでフィルタする場合 (例: data.user_name = 'alice') 、テキスト索引は、どの JSON 値にも検索トークンが含まれていない行 (およびグラニュール) をすばやくスキップできます。
異なる JSON パスに同じトークンが含まれている場合、この索引で偽陽性が発生することがあります。
たとえば、行 1 が
{"a": "hello", "b": "world"} で、クエリが data.a = 'world' を検索する場合、テキスト索引では world がパス a ではなく b に属していることを区別できません。
このような場合、索引はその行をスキップせず、実際のカラムデータに対するフィルタで最終的な評価が行われます。
これは、索引が高速な事前フィルタとして機能する、他のテキスト索引のユースケースと同じ動作です。索引の作成
サポートされるクエリパターン
String カラムで使うものと同じ関数、およびすべてのカラムで使える関数 equals です。
サブカラムへのアクセス:
CAST を使用したサブカラムへのアクセス:
IN 演算子:
フレーズ検索
hasPhrase 関数によるフレーズ検索をサポートしています。
フレーズ内のすべてのトークンは、ドキュメント内で連続し、同じ順序で出現する必要があります。
テキスト索引は、フレーズ内のすべてのトークンのポスティングリストの積集合を取り、候補となる グラニュール を特定することで、フレーズ検索を高速化します。
その後、ClickHouse はそれらの グラニュール 内で、トークンが正確に隣接していることを検証します。
hasPhrase は、splitByNonAlpha、splitByString、ngrams、asciiCJK の各トークナイザーでサポートされています。
フレーズ文字列は、索引に設定されたトークナイザーでトークン化されます。
フレーズ内のトークナイザーの区切り文字は無視されます。splitByNonAlpha トークナイザーでは、hasPhrase(text, 'quick+brown') は hasPhrase(text, 'quick brown') と同等です。
例
Query
Query
Response
'New weather in York') は、トークンの順序が正しくないため一致しません。
3 行目 ('weather in New Orleans') は、トークン 'York' を含まないため一致しません。
パフォーマンスチューニング
Direct read
- 設定 query_plan_direct_read_from_text_index (デフォルトは true) は、direct read を全体として有効にするかどうかを指定します。
- 設定 use_skip_indexes_on_data_read は、ClickHouse バージョン < 26.4 では direct read の前提条件でした。
hasToken、hasAllTokens、hasAnyTokens 関数をサポートします。
テキスト索引が array トークナイザーで定義されている場合、direct read は equals、has、hasAny、hasAll、mapContainsKey、mapContainsValue 関数でもサポートされます。
これらの関数は、AND、OR、NOT 演算子で組み合わせることもできます。
WHERE 句または PREWHERE 句には、追加の非テキスト検索関数のフィルタ (テキストカラムまたは他のカラムに対するフィルタ) を含めることもできます。この場合でも direct read 最適化は使用されますが、効果はやや低下します (適用されるのはサポート対象のテキスト検索関数のみです) 。
クエリが direct read を利用しているか確認するには、EXPLAIN PLAN actions = 1 を付けてクエリを実行します。
例として、direct read を無効にしたクエリは
query_plan_direct_read_from_text_index = 1 を指定して実行すると
__text_index_<index_name>_<function_name>_<id> が含まれます。
このカラムが存在する場合、direct read が使用されています。
WHERE フィルタ句にテキスト検索関数しか含まれていない場合、クエリはカラムデータをまったく読み取らずに済むため、direct read によるパフォーマンス上のメリットを最大限に得られます。
ただし、クエリ内のほかの箇所でテキストカラムにアクセスしている場合でも、direct read によってパフォーマンス改善は得られます。
ヒントとしての direct read
ヒントとしての direct read は、通常の direct read と同じ原理に基づきますが、基になるテキストカラムを除外する代わりに、テキスト索引データから構築した追加のフィルタを加えます。
これは、テキスト索引だけを読み取ると偽陽性が発生する関数で使用されます。
サポートされている関数は次のとおりです: like, startsWith, endsWith, equals, has, hasPhrase, mapContainsKey, mapContainsValue。
この追加フィルタは、ほかのフィルタと組み合わせることで結果セットをさらに絞り込むための選択性を高め、他のカラムから読み取るデータ量の削減に役立ちます。
ヒントとしての direct read は、設定 query_plan_text_index_add_hint で制御されます (デフォルトで有効) 。
ヒントなしのクエリの例:
query_plan_text_index_add_hint = 1 を指定して同じクエリを実行した場合は
__text_index_...) がフィルタ条件に加えられていることがわかります。
PREWHERE の最適化により、フィルタ条件は3つの個別の論理積条件に分解され、計算コストの低い順に適用されます。
このクエリでは、適用順は __text_index_...、次に greaterOrEquals(...)、最後に like(...) です。
この順序により、WHERE 句の後でクエリ内で使用される重いカラムを読み取る前に、テキスト索引と元のフィルタでスキップされるグラニュールよりもさらに多くのデータグラニュールをスキップでき、読み取るデータ量をいっそう削減できます。
LIKE/ILIKE クエリ
%<スペースを含まない英数字文字>% で、テキスト索引のトークナイザーが splitByNonAlpha または array の場合、ClickHouse は転置索引を利用して LIKE/ILIKE クエリを大幅に高速化します。これを実現するために、ClickHouse は一致するパターンを見つける際、テーブル全体をスキャンする代わりに転置索引の Dictionary をスキャンします。
この最適化が有効な場合、LIKE/ILIKE クエリはテーブル全体のスキャンより大幅に高速になるはずです。ただし、パターンが Dictionary 内のトークンの大半に一致する場合は、テーブル全体のスキャンと比べて性能が悪化することがあります。幸い、それを防ぐためのフォールバックの仕組みがあります。
この最適化は、次の設定で制御されます。
フォールバックの仕組みは、次の 2 つの設定で制御されます。
この最適化でサポートされるのは、関数 like と ilike のみです。
キャッシュ
テキスト索引トークンキャッシュの設定
ヘッダーキャッシュの設定
ポスティングリスト cache の設定
制限事項
- トークン数が非常に多いテキスト索引 (例: 100 億トークン) のマテリアライズでは、大量のメモリを消費する可能性があります。テキスト
索引のマテリアライズは、直接 (
ALTER TABLE <table> MATERIALIZE INDEX <index>) 行われる場合と、パーツのマージで間接的に行われる場合があります。 - 4,294,967,296 (= 2^32 = 約 42 億) 行を超えるパーツでは、テキスト索引をマテリアライズできません。テキスト索引がマテリアライズされていない場合、クエリはそのパーツ内での低速な総当たり検索にフォールバックします。最悪ケースの見積もりとして、パーツには String 型のカラムが 1 つだけ含まれ、MergeTree setting
max_bytes_to_merge_at_max_space_in_pool(デフォルト: 150 GB) が変更されていないと仮定してください。この場合、そのカラムの 1 行あたりの平均文字数が 29.5 文字未満であれば、この状況が発生します。実際には、テーブルにはほかのカラムも含まれるため、しきい値はこれより何倍も小さくなります (ほかのカラムの数、型、サイズに依存します) 。
テキスト索引とブルームフィルタベースの索引の違い
bloom_filter、ngrambf_v1、tokenbf_v1、sparse_grams) によって高速化できますが、両者は設計と想定ユースケースの点で本質的に異なります。
ブルームフィルタ索引
- 偽陽性が発生しうる確率的データ構造に基づいています。
- 集合への所属判定、つまりそのカラムにトークン X が含まれている可能性があるか、あるいは確実に含まれていないか、ということしか判定できません。
- クエリ実行時に大まかな範囲をスキップできるよう、granule レベルの情報を格納します。
- 適切にチューニングするのが難しいです (例は こちら を参照) 。
- 比較的コンパクトです (1 パーツあたり数 KB ~数 MB) 。
- トークンに対して決定論的な転置索引を構築します。索引自体による偽陽性は発生しません。
- テキスト検索ワークロード向けに特化して最適化されています。
- 効率的な用語ルックアップを可能にするため、行レベルの情報を格納します。
- 比較的大きくなります (1 パーツあたり数十~数百 MB) 。
- 高度なトークン化や前処理には対応していません。
- 複数トークンの検索には対応していません。
- 転置索引に期待されるような性能特性は得られません。
- トークン化と前処理を提供します
hasAllTokens、LIKE、matchなどのテキスト検索関数を効率的にサポートします。- 大規模なテキストコーパスに対して、はるかに優れたスケーラビリティを発揮します。
実装の詳細
- 各トークンをポスティングリストに対応付けるDictionary
- それぞれが行番号の集合を表す、ポスティングリストの集合
dictionary_block_size で設定できます) 。
Dictionaryブロックファイル (.dct) には、パーツ内のすべてのインデックスグラニュールに含まれるすべてのDictionaryブロックが格納されます。
索引ヘッダーファイル (.idx)
索引ヘッダーファイルには、各Dictionaryブロックについて、そのブロックの先頭トークンと、Dictionaryブロックファイル内での相対オフセットが格納されます。
このスパースインデックス構造は、ClickHouse のスパース主キー索引) に似ています。
ポスティングリストファイル (.pst)
すべてのトークンのポスティングリストは、ポスティングリストファイル内に順番に配置されます。
容量を節約しつつ高速な積集合およびユニオン操作を可能にするため、ポスティングリストは roaring bitmaps として格納されます。
ポスティングリストが posting_list_block_size より大きい場合は、複数のブロックに分割され、ポスティングリストファイルに順番に格納されます。
テキスト索引のマージ
データパーツがマージされる際、テキスト索引を最初から再構築する必要はありません。代わりに、マージ処理内の別ステップで効率的にマージできます。
このステップでは、各入力パーツのテキスト索引にあるソート済みDictionaryを読み込み、新しい統合Dictionaryへ結合します。
また、ポスティングリスト内の行番号も、初期マージフェーズで作成された旧行番号から新行番号への対応関係を用いて、マージ後のデータパーツ内での新しい位置を反映するよう再計算されます。
このテキスト索引のマージ方法は、_part_offset カラムを持つ projections のマージ方法に似ています。
ソースパーツ内で索引がマテリアライズされていない場合は、索引を構築して一時ファイルに書き込み、その後、ほかのパーツの索引およびほかの一時索引ファイルの索引とともにマージされます。
デバッグ
テーブル関数 mergeTreeTextIndex を使用すると、テキスト索引の内部を調査できます。
例: Hacker News データセット
hackernewsテーブルに挿入してみましょう:
ALTER TABLE を使用して comment カラムにテキスト索引を追加し、その後マテリアライズします:
hasToken、hasAnyTokens、hasAllTokens 関数を使ってクエリを実行してみましょう。
以下の例では、通常の索引スキャンと direct read 最適化の間にある大きな性能差を示します。
1. hasToken を使用する
hasToken は、テキストに特定の単一トークンが含まれているかどうかを確認します。
大文字と小文字を区別するトークン ‘ClickHouse’ を検索します。
direct read 無効 (標準スキャン)
デフォルトでは、ClickHouse はスキップ索引を使ってグラニュールをフィルタリングし、その後、それらのグラニュールのカラムデータを読み取ります。
この動作は、direct read を無効にすることで再現できます。
2. hasAnyTokens の使用
hasAnyTokens は、テキストに指定したトークンのうち少なくとも 1 つが含まれているかどうかを判定します。
‘love’ または ‘ClickHouse’ を含むコメントを検索します。
Direct read 無効 (標準スキャン)
3. hasAllTokens の使用
hasAllTokens は、テキストに指定したすべてのトークンが含まれているかどうかを判定します。
‘love’ と ‘ClickHouse’ の両方を含むコメントを検索します。
Direct read 無効時 (標準スキャン)
Direct read が無効でも、標準のスキップ索引は引き続き有効です。
28.7M 行を 147.46K 行まで絞り込めますが、それでもカラムから 57.03 MB を読み取る必要があります。
4. 複合検索: OR, AND, NOT, …
hasAnyTokens(comment, ['ClickHouse', 'clickhouse']) を使うほうが、より効率的で推奨される構文です。
- ブログ: ClickHouse 全文検索の一般提供を発表
- ブログ: オブジェクトストレージ向けの高性能な全文検索の構築
- ビデオ: ClickHouse における全文検索の紹介
- ビデオ: 舞台裏: ClickHouse のスケールと速度を支える全文検索
- プレゼンテーション: ClickHouse 全文検索の内部: 高速・ネイティブ・列指向
- プレゼンテーション: 転置データベース索引: 必要性、概要、実装方法、FOSDEM 2026
- ブログ: ClickHouse における転置索引の紹介
- ブログ: ClickHouse 全文検索の内部: 高速・ネイティブ・列指向
- ビデオ: 全文索引: 設計と実験