> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-trino-dialect.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> テキスト内の検索語をすばやく見つけます。

# テキスト索引による全文検索

export const CloudNotSupportedBadge = () => {
  return <a href="https://clickhouse.com/docs/products/cloud/guides/cloud-compatibility#list-of-unsupported-features" className="cloudNotSupportedBadge">
            <div className="cloudNotSupportedIcon">
            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path strokeWidth="1.5" d="M6.33366 12.6666L12.3739 12.6667C13.6593 12.6667 14.7073 11.6187 14.7073 10.3334C14.7073 9.04804 13.6593 8.00003 12.3739 8.00003C12.3739 8.00003 12.3337 7.66659 12.0003 7.33325M10.667 5.33322C8.00033 2.33325 4.45395 4.78537 4.14195 6.68203C2.55728 6.7627 1.29395 8.06203 1.29395 9.6667C1.29395 11.3234 2.66699 12.6666 4.00033 12.6666" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
                <path strokeWidth="1.5" d="M2.66699 14L12.0003 4.66663" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
            </svg>

        </div>
            ClickHouse Cloud ではサポートされていません
        </a>;
};

テキスト索引 ([転置索引](https://en.wikipedia.org/wiki/Inverted_index) とも呼ばれます) を使用すると、テキストデータに対して高速な全文検索が可能になります。
テキスト索引には、各トークンを含む行番号への対応付けが保存されます。
トークンは、トークン化と呼ばれる処理によって生成されます。
たとえば、ClickHouse のデフォルトのトークナイザーは、英語の文 "The cat likes mice." を \["The", "cat", "likes", "mice"] というトークン列に変換します。

例として、1 つのカラムと 3 行を持つテーブルを考えます

```result theme={null}
1: The cat likes mice.
2: Mice are afraid of dogs.
3: I have two dogs and a cat.
```

対応するトークンは次のとおりです。

```result theme={null}
1: The, cat, likes, mice
2: Mice, are, afraid, of, dogs
3: I, have, two, dogs, and, a, cat
```

通常は大文字と小文字を区別せずに検索したいため、トークンを小文字に変換します：

```result theme={null}
1: the, cat, likes, mice
2: mice, are, afraid, of, dogs
3: i, have, two, dogs, and, a, cat
```

また、ほぼすべての行に現れる "I"、"the"、"and" などのストップワードも削除します:

```result theme={null}
1: cat, likes, mice
2: mice, afraid, dogs
3: have, two, dogs, cat
```

テキスト索引には、概念的には次の情報が含まれます：

```result theme={null}
afraid : [2]
cat    : [1, 3]
dogs   : [2, 3]
have   : [3]
likes  : [1]
mice   : [1]
two    : [3]
```

検索トークンを指定すると、この索引構造により一致するすべての行をすばやく見つけられます。

<div id="creating-a-text-index">
  ## テキスト索引の作成
</div>

テキスト索引は、ClickHouse バージョン 26.2 以降で一般提供 (GA) されています。
これらのバージョンでは、テキスト索引を使用するために特別な設定を行う必要はありません。
本番環境で使用する場合は、ClickHouse バージョン >= 26.2 の利用を強く推奨します。

<Note>
  テキスト索引は、[compatibility](/ja/reference/settings/session-settings/compatibility#compatibility) 設定に関係なく、ClickHouse バージョン >= 26.2 であれば使用できます。
</Note>

テキスト索引を作成するには、次の構文を使用します。

```sql title="Query" theme={null}
CREATE TABLE table
(
    key UInt64,
    str String,
    INDEX text_idx str TYPE text(
                                -- Mandatory parameters:
                                tokenizer = splitByNonAlpha
                                            | splitByString[(S)]
                                            | asciiCJK
                                            | chinese[(granularity)]
                                            | icu(locale)
                                            | japanese
                                            | ngrams[(N)]
                                            | sparseGrams[(min_length[, max_length[, min_cutoff_length]])]
                                            | array
                                -- Optional parameters:
                                [, preprocessor = expression(str)]
                                [, postprocessor = expression(str)]
                                [, support_phrase_search = 0 | 1 ] -- experimental
                                -- Optional advanced parameters:
                                [, dictionary_block_size = D]
                                [, dictionary_block_frontcoding_compression = B]
                                [, posting_list_block_size = C]
                                [, posting_list_codec = 'none' | 'bitpacking' ]
                            )
)
ENGINE = MergeTree
ORDER BY key
```

テキスト索引は、次の型のカラムに定義できます。

* [String](/ja/reference/data-types/string) と [FixedString](/ja/reference/data-types/fixedstring)
* [Array(String)](/ja/reference/data-types/array) と [Array(FixedString)](/ja/reference/data-types/array)
* [Map](/ja/reference/data-types/map) ([mapKeys](/ja/reference/functions/regular-functions/tuple-map-functions#mapKeys) および [mapValues](/ja/reference/functions/regular-functions/tuple-map-functions#mapValues) 関数経由)
* [JSON](/ja/reference/data-types/newjson) ([JSONAllPaths](/ja/reference/functions/regular-functions/json-functions#JSONAllPaths) および [`JSONAllValues`](/ja/reference/functions/regular-functions/json-functions#JSONAllValues) 関数経由)

[Nullable(T)](/ja/reference/data-types/nullable) 型および [LowCardinality()](/ja/reference/data-types/lowcardinality) 型のカラムにも対応しており、`Array(Nullable(String or FixedString))` も含まれます。

また、既存のテーブルにテキスト索引を追加するには:

```sql title="Query" theme={null}
ALTER TABLE table
    ADD INDEX text_idx str TYPE text(
                                -- Mandatory parameters:
                                tokenizer = splitByNonAlpha
                                            | splitByString[(S)]
                                            | asciiCJK
                                            | chinese[(granularity)]
                                            | icu(locale)
                                            | japanese
                                            | ngrams[(N)]
                                            | sparseGrams[(min_length[, max_length[, min_cutoff_length]])]
                                            | array
                                -- Optional parameters:
                                [, preprocessor = expression(str)]
                                [, postprocessor = expression(str)]
                                [, support_phrase_search = 0 | 1 ] -- experimental
                                -- Optional advanced parameters:
                                [, dictionary_block_size = D]
                                [, dictionary_block_frontcoding_compression = B]
                                [, posting_list_block_size = C]
                                [, posting_list_codec = 'none' | 'bitpacking' ]
                            )

```

既存のテーブルに索引を追加する場合は、既存のテーブルパーツに対して索引をマテリアライズすることを推奨します (そうしないと、索引のないパーツの検索では低速な総当たりスキャンにフォールバックします) 。

```sql title="Query" theme={null}
ALTER TABLE table MATERIALIZE INDEX text_idx SETTINGS mutations_sync = 2;
```

テキスト索引を削除するには、次を実行します

```sql title="Query" theme={null}
ALTER TABLE table DROP INDEX text_idx;
```

**トークナイザー引数 (必須) **。`tokenizer` 引数では、トークナイザーを指定します。

* `splitByNonAlpha` は、ASCII の英数字以外の文字で String を分割します (関数 [splitByNonAlpha](/ja/reference/functions/regular-functions/splitting-merging-functions#splitByNonAlpha) を参照) 。
* `splitByString(S)` は、ユーザー定義の区切り String `S` で String を分割します (関数 [splitByString](/ja/reference/functions/regular-functions/splitting-merging-functions#splitByString) を参照) 。
  区切り文字は省略可能なパラメータで指定できます。たとえば、`tokenizer = splitByString([', ', '; ', '\n', '\\'])` のように指定します。
  各 String は複数文字で構成することもできます (例では `', '`) 。
  明示的に指定しない場合 (たとえば `tokenizer = splitByString`) 、デフォルトの区切り文字リストは単一の空白文字 `[' ']` です。
* `asciiCJK` は、Unicode の単語境界規則を使用して String をトークンに分割します ([Unicode Text Segmentation (UAX #29)](https://unicode.org/reports/tr29/) に類似) 。
  ASCII の英数字とアンダースコアは、コネクタ (文字に対する ASCII `:`、同種の文字に対する `.` および `'`) を含むトークンを構成します。非 ASCII の Unicode 文字は、[CJK](https://en.wikipedia.org/wiki/CJK_characters) 文字を含め、1 文字のトークンになります。
* `chinese[(granularity)]` は、Dictionary と隠れマルコフモデルを使用して中国語テキストを単語に分割します (アルゴリズムは [jieba](https://github.com/fxsjy/jieba) に従い、埋め込み Dictionary とモデルデータは [cppjieba](https://github.com/yanyiwu/cppjieba) に由来します) 。非 ASCII 文字をすべて 1 文字のトークンとして扱う `asciiCJK` とは異なり、`chinese` は連続する中国語文字を単語にまとめます (たとえば、`北京大学` は 4 つの 1 文字トークンではなく、1 つのトークン `北京大学` になります) 。これにより、中国語テキストに対してより意味のあるトークンが得られ、検索品質が向上します。一般的なテキストや混在テキストには `asciiCJK` を、中国語のみのテキストには `chinese` を使用してください。省略可能な `granularity` 引数には、`'coarse_grained'` (指定しない場合のデフォルト) または `'fine_grained'` を指定します。後者では、たとえば `北京邮电大学` に対して `北京`、`邮电`、`大学` も生成されるよう、重複するサブワードも列挙します。きめ細かいトークン化は、より大きな索引を必要とする代わりに再現率を向上させます。
  `chinese` テキスト索引の検索には、`hasToken` (ASCII の区切り文字でのみ分割) ではなく、[hasAnyTokens](/ja/reference/functions/regular-functions/string-search-functions#hasAnyTokens) / [hasAllTokens](/ja/reference/functions/regular-functions/string-search-functions#hasAllTokens) (needle を `chinese` トークナイザーでトークン化) を使用します。
* `icu(locale)` は、[ICU](https://icu.unicode.org/) ライブラリの Unicode 単語分割 (UAX #29) を使用して String を単語トークンに分割します。単語間に空白を入れない文字体系 (中国語、日本語、タイ語など) では、ICU は Dictionaryベースの分割を適用するため、`asciiCJK` とは異なり、このようなテキストは単一文字ではなく意味のある複数文字の単語に分割されます。ここでの「Dictionary」とは、これらの文字体系用に ICU にバンドルされている単語リストを意味します ([`brkitr/dictionaries`](https://github.com/unicode-org/icu/tree/main/icu4c/source/data/brkitr/dictionaries) を参照) 。ICU はこれらの単語に基づいて最も可能性の高い分割を選択します。
  `locale` はセグメンターに渡される ICU ロケールです。分割は主に文字体系と Dictionary に基づいて行われ、ロケールは ICU のロケール固有の調整を選択します。これは必須パラメータであり、たとえば `tokenizer = icu('ja')` または `tokenizer = icu('zh')` のように指定します。
  使用可能なロケールは `SELECT * FROM system.collations` で一覧表示できます。
* `japanese` は、[MeCab](https://github.com/taku910/mecab) 形態素アナライザを使用して日本語テキストを単語に分割します。CJK 入力に対して単一文字のトークンを出力する `asciiCJK` とは異なり、このトークナイザーは適切な単語分割を行います。実行時にサーバー設定から読み込まれる Dictionary が必要です ([Japanese tokenizer dictionary](#japanese-tokenizer-dictionary) を参照) 。
* `ngrams(N)` は、String を同じ長さの `N`-gram に分割します (関数 [ngrams](/ja/reference/functions/regular-functions/splitting-merging-functions#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](/ja/reference/functions/regular-functions/string-functions#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)` では、内部的には入力文字列から 3-gram、4-gram、5-gram を生成しますが、返されるのは 4-gram と 5-gram のみです。
* `array` はトークン化を行いません。つまり、各行の値がトークンになります (関数 [array](/ja/reference/functions/regular-functions/array-functions#array) を参照) 。

使用可能なすべてのトークナイザーは [system.tokenizers](/ja/reference/system-tables/tokenizers) に一覧表示されています。

<div id="japanese-tokenizer-dictionary">
  ### 日本語トークナイザー用のMeCab Dictionary
</div>

<CloudNotSupportedBadge />

`japanese` トークナイザーには、ClickHouse に同梱されていない MeCab Dictionary が必要です。サーバー設定で Dictionary を指定してください。

```xml theme={null}
<tokenizer>
    <japanese>
        <dictionary_location>https://example.com/dictionary.tar.zst</dictionary_location>
        <dictionary_sha>0123...cdef</dictionary_sha>
    </japanese>
</tokenizer>
```

* `dictionary_location` は、コンパイル済み MeCab Dictionary のアーカイブの場所です。[IPADIC](https://github.com/taku910/mecab) や [UniDic](https://clrd.ninjal.ac.jp/unidic/) など、任意の公式 Dictionary を使用できます。場所はサポートされるアーカイブ拡張子 (たとえば `.tar.gz`、`.tar.zst`、`.zip`) で終わる必要があります。アーカイブタイプはこの拡張子から検出されるため、このような拡張子のない URL (例: `https://example.com/download`) は拒否されます。サポートされる場所:
  * ローカルの `file://` パス。
  * 通常のダウンロードとして取得される `http(s)://` URL (パブリックまたは事前署名済みオブジェクトにはこれを使用します)。
  * AWS S3、GCS、MinIO、オンプレミスなど (AWS のみではありません) の S3 互換オブジェクトストア。`s3://`/`gs://`/`oss://`、または完全な `http(s)://endpoint/bucket/key` URL として指定します。プライベート bucket の場合は、S3 認証情報を `<japanese>` の子要素として指定します (以下の例を参照)。
* `dictionary_sha` は、そのアーカイブの SHA-256 です。Dictionary をロードする前に検証されます。不一致の場合、Dictionary はロードされず、エラーが発生します。

アーカイブは一度ダウンロードおよび展開され、その後ローカルにキャッシュされます。Dictionary はドキュメントのトークン化方法を決定するため、同じ `dictionary_sha` をすべてのレプリカで設定する必要があります。

プライベートな S3 互換 bucket から読み取るには、`dictionary_location` および `dictionary_sha` と並べて、S3 設定を `<japanese>` の子要素として追加します:

```xml theme={null}
<tokenizer>
    <japanese>
        <dictionary_location>s3://my-bucket/dictionary.tar.zst</dictionary_location>
        <dictionary_sha>0123...cdef</dictionary_sha>
        <access_key_id>...</access_key_id>
        <secret_access_key>...</secret_access_key>
        <region>us-east-1</region>
    </japanese>
</tokenizer>
```

認識される設定 (`access_key_id`、`secret_access_key`、`region`、`no_sign_request`、`use_environment_credentials`、...) は、ClickHouse の他の箇所で使用される S3 認証設定と同じです。これらの設定が存在することで、`http(s)://` URL は通常のダウンロードではなく、(リクエスト署名を行う) S3 クライアントを介して取得されます。

Dictionary を設定すると、`japanese` トークナイザーを次のように使用できます:

```sql title="Query" theme={null}
CREATE TABLE docs
(
    id UInt64,
    body String,
    INDEX body_idx body TYPE text(tokenizer = 'japanese')
)
ENGINE = MergeTree
ORDER BY id;

INSERT INTO docs VALUES (1, '日本語の形態素解析エンジン'), (2, 'これはテストの文章です');

SELECT id FROM docs WHERE hasAllTokens(body, '形態 解析', 'japanese') ORDER BY id;
```

```text title="Response" theme={null}
┌─id─┐
│  1 │
└────┘
```

<Note>
  `splitByString` トークナイザーは、分割区切り文字を左から右の順に適用します。
  そのため、曖昧さが生じることがあります。
  たとえば、区切り String `['%21', '%']` を指定すると、`%21abc` は `['abc']` としてトークン化されます。一方、区切り String の順序を `['%', '%21']` に入れ替えると、出力は `['21abc']` になります。
  多くの場合、より長い区切り文字が優先的に一致するようにするのが望ましいでしょう。
  通常は、区切り String を長さの降順で渡すことでこれを実現できます。
  区切り String がたまたま [prefix code](https://en.wikipedia.org/wiki/Prefix_code) を構成している場合は、任意の順序で渡せます。
</Note>

トークナイザーが入力文字列をどのように分割するかを確認するには、[tokens](/ja/reference/functions/regular-functions/splitting-merging-functions#tokens) 関数および [tokensForLikePattern](/ja/reference/functions/regular-functions/splitting-merging-functions#tokensForLikePattern) 関数を使用できます。

例:

```sql title="Query" theme={null}
SELECT tokens('abc def', 'ngrams', 3);
```

```result title="Response" theme={null}
['abc','bc ','c d',' de','def']
```

*非 ASCII 入力の処理。*
テキスト索引は、あらゆる言語および文字セットのテキストデータに対して構築できます。
非 ASCII テキストには、CJK 文字を含む Unicode の単語境界を正しく処理する `asciiCJK` トークナイザーを推奨します。
単語を空白で区切らない言語 (中国語、日本語、タイ語など) では、`icu(locale)` トークナイザーが ICU の辞書ベースの単語分割により、意味のある複数文字の単語トークンを生成します。
日本語の場合、`japanese` トークナイザー (MeCab) はテキストを単一文字ではなく単語に分割するため、一般により良い検索結果が得られます。
中国語の場合、`chinese` トークナイザー (jieba) はテキストを単一文字ではなく単語に分割するため、一般により良い検索結果が得られます。

<a id="preprocessor-argument-optional" />**プリプロセッサ引数 (任意) **。プリプロセッサは、トークン化の前に入力文字列に適用される式を指します。

プリプロセッサ引数の一般的なユースケースには、次のものがあります。

1. 大文字/小文字の変換、または大文字小文字を区別しないマッチングを可能にするケースフォールディング。例: [lower](/ja/reference/functions/regular-functions/string-functions#lower), [lowerUTF8](/ja/reference/functions/regular-functions/string-functions#lowerUTF8), [caseFoldUTF8](/ja/reference/functions/regular-functions/string-functions#caseFoldUTF8)。
2. UTF-8 の正規化。例: [normalizeUTF8NFC](/ja/reference/functions/regular-functions/string-functions#normalizeUTF8NFC), [normalizeUTF8NFD](/ja/reference/functions/regular-functions/string-functions#normalizeUTF8NFD), [normalizeUTF8NFKC](/ja/reference/functions/regular-functions/string-functions#normalizeUTF8NFKC), [normalizeUTF8NFKD](/ja/reference/functions/regular-functions/string-functions#normalizeUTF8NFKD), [normalizeUTF8NFKCCasefold](/ja/reference/functions/regular-functions/string-functions#normalizeUTF8NFKCCasefold), [toValidUTF8](/ja/reference/functions/regular-functions/string-functions#toValidUTF8)。
3. アクセント記号など、不要な文字や部分文字列の削除または変換。例: [extractTextFromHTML](/ja/reference/functions/regular-functions/string-functions#extractTextFromHTML), [substring](/ja/reference/functions/regular-functions/string-functions#substring), [idnaEncode](/ja/reference/functions/regular-functions/string-functions#idnaEncode), [translate](/ja/reference/functions/regular-functions/string-replace-functions#translate), [removeDiacriticsUTF8](/ja/reference/functions/regular-functions/string-functions#removeDiacriticsUTF8)。

プリプロセッサ式は、[String](/ja/reference/data-types/string) または [FixedString](/ja/reference/data-types/fixedstring) 型の入力値を、同じ型の値に変換する必要があります。
テキスト索引が `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))`

非決定論的関数は使用できません。

<Note>
  プリプロセッサは原則として、索引対象のカラムまたは式をプリプロセッサ式でラップすることと同等です。
  例えば、`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, [...])` は一致しません。
  そのため、最適なユーザー体験のために、プリプロセッサ式の使用を推奨します。
</Note>

関数 [hasToken](/ja/reference/functions/regular-functions/string-search-functions#hasToken)、[hasAllTokens](/ja/reference/functions/regular-functions/string-search-functions#hasAllTokens)、[hasAnyTokens](/ja/reference/functions/regular-functions/string-search-functions#hasAnyTokens)、および [hasPhrase](/ja/reference/functions/regular-functions/string-search-functions#hasPhrase) は、検索語をトークン化する前に、まずプリプロセッサを使用して変換します。
プリプロセッサはテキスト索引の経路でのみ適用されるため、これらの関数の結果は、テキスト索引を使用するクエリと使用しないクエリ (例: `SETTINGS use_skip_indexes = 0`) で異なる場合があることに注意してください。

たとえば、

```sql title="Query" theme={null}
CREATE TABLE table
(
    str String,
    INDEX idx str TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(str))
)
ENGINE = MergeTree
ORDER BY tuple();

SELECT count() FROM table WHERE hasToken(str, 'Foo');
```

と同等です:

```sql title="Query" theme={null}
CREATE TABLE table
(
    str String,
    INDEX idx lower(str) TYPE text(tokenizer = 'splitByNonAlpha')
)
ENGINE = MergeTree
ORDER BY tuple();

SELECT count() FROM table WHERE hasToken(str, lower('Foo'));
```

この場合、プリプロセッサ式は配列内の各要素をそれぞれ変換します。

例:

```sql title="Query" theme={null}
CREATE TABLE table
(
    arr Array(String),
    INDEX idx arr TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(arr))

    -- This is not legal:
    INDEX idx_illegal arr TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = arraySort(arr))
)
ENGINE = MergeTree
ORDER BY tuple();

SELECT count() FROM tab WHERE hasAllTokens(arr, 'foo');
```

[Map](/ja/reference/data-types/map) 型のカラムに対するテキスト索引でプリプロセッサを定義するには、その索引を
マップのキーと値のどちらを対象に構築するかを決める必要があります。

例:

```sql title="Query" theme={null}
CREATE TABLE table
(
    map Map(String, String),
    INDEX idx mapKeys(map)  TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(mapKeys(map)))
)
ENGINE = MergeTree
ORDER BY tuple();

SELECT count() FROM tab WHERE hasAllTokens(mapKeys(map), 'foo');
```

<a id="postprocessor-argument-optional" />**ポストプロセッサ引数 (任意)**。ポストプロセッサとは、トークン化の後に各出力トークンへ適用される式のことです。

入力文字列全体をトークナイザーがトークンへ分割する前に変換するプリプロセッサとは異なり、ポストプロセッサはトークンそのものに対して 1 つずつ処理を行います。
本質的にトークン単位の変換を行うのに適した場所です。

ポストプロセッサ引数の典型的なユースケースは次のとおりです:

1. **ストップワード (極めて高頻度なトークン) のフィルタリング**。"the"、"a"、"is" のような非常によく出現するトークンは、検索との関連性が低いうえ、索引を肥大化させます。
   ポストプロセッサを使えば、それらを空トークンに変換して除外できます。空トークンは無視され、つまり索引には追加されません。
   例: `if(str IN ('the', 'a', 'an', 'of', 'in', 'is', 'it'), '', str)`
2. **タイムスタンプの除去**。ログ行は、`2024-01-15T10:23:45` のような構造化されたタイムスタンプで始まっていることや、それを含んでいることがよくあります。
   タイムスタンプのトークンを索引化すると、検索上の関連性を持たない文字列によって索引が肥大化します。
   タイムスタンプを無視するための相補的な方法は 2 つあります:
   * **ポストプロセッサ方式**: `splitByString` トークナイザー (空白で分割) を使用してタイムスタンプ全体を 1 つのトークンにし、その後 `parseDateTimeOrNull` を使って検出して除外します。
     例: `if(isNull(parseDateTimeOrNull(str, '%Y-%m-%dT%H:%i:%S')), str, '')`
     タイムゾーンオフセットや小数秒を含むタイムスタンプには、明示的なフォーマット文字列を使わずに `parseDateTimeBestEffortOrNull(str)` を使用します。
   * **プリプロセッサ方式**: トークン化の *前* に、正規表現でログ行全体からタイムスタンプを取り除きます。
     例: `replaceRegexpAll(str, '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2} ', '')`
     この方法はどのトークナイザーでも機能し、タイムスタンプ文字列がまったくトークン化されないため、より効率的です。
     両方の方法を組み合わせることもできます。プリプロセッサでタイムスタンプを除去しつつ、ポストプロセッサで残りのトークンを正規化またはフィルタリングします (たとえば、小文字化 + `ERROR` や `INFO` のような重大度を表す語の除去)。
3. **ステミング**。各トークンをその語幹に対応付けることで、同じ語根を共有する語形の違いにも一致するようになり、検索の再現率が向上します。
   たとえば、英語のステミングでは "running"、"runs"、"run" はすべて "run" に語幹化されるため、これらの異形のいずれかに対するクエリですべてに一致します。
   ClickHouse には、複数の言語向けの組み込み [stem](/ja/reference/functions/regular-functions/nlp-functions#stem) 関数があります。
   例: `stem(str, 'en')`
4. **大文字小文字の正規化**。たとえば [lower](/ja/reference/functions/regular-functions/string-functions#lower) や [lowerUTF8](/ja/reference/functions/regular-functions/string-functions#lowerUTF8) を使って、トークンを小文字化または大文字化し、大文字小文字を区別しないマッチングを可能にします。
   小文字化および大文字化には、ポストプロセッサではなくプリプロセッサを推奨します。

ポストプロセッサ式は、[String](/ja/reference/data-types/string) 型のトークンを同じ型のトークンに変換します。
また、ポストプロセッサ式は、テキスト索引が定義されているカラムまたは式のみを参照する必要があります。
カラムが `Array(String)` 型の場合でも、ポストプロセッサは通常の `String` 値として個々のトークンを処理します。

非決定論的関数の使用は許可されていません。

ポストプロセッサは、索引構築時に生成される各トークンに適用されます (`array` トークナイザーでは、各配列要素がトークンになります)。クエリ時の動作は関数によって異なります:

* `hasToken`、`hasAllTokens`、`hasAnyTokens`、および `hasPhrase` (サポートされている任意のトークナイザーの場合): ポストプロセッサは検索対象のトークンと検索ニードルの両方に適用され、完全に正規化されたマッチング (たとえば、大文字小文字を区別しない検索) を可能にします。`hasPhrase` では、ポストプロセッサ処理後のトークンが密に配置されるため、ポストプロセッサが除外したトークンによって位置の空白は生じず、そのトークンをまたいでフレーズは引き続き一致します。たとえば、`the` を除外するストップワードポストプロセッサでは、`hasPhrase(col, 'see cat')` はドキュメント `see the cat` に一致します。
* その他すべての関数 (`=`, `IN`, `has`, `hasAny`, `hasAll`, `mapContains*`): 索引ヒントのルックアップでは検索ニードルのみがポストプロセッサ処理されます。行レベルの述語では、引き続き元のカラム値と比較されます。

例:

* ポストプロセッサ式を使用してストップワードを除去する:

```sql theme={null}
CREATE TABLE table
(
    str String,
    INDEX idx(str) TYPE text(
        tokenizer = 'splitByNonAlpha',
        postprocessor = if(str IN ('the', 'a', 'an', 'of', 'in', 'is', 'it'), '', str)
    )
)
ENGINE = MergeTree
ORDER BY tuple();
```

* ポストプロセッサ式を使用してタイムスタンプを除去します:

```sql theme={null}
-- Log lines: '2024-01-15T10:23:45 ERROR connection failed'
-- The splitByString tokenizer (default: whitespace) keeps the full timestamp as one token.
-- parseDateTimeOrNull detects and drops it; non-timestamp words are kept.
CREATE TABLE logs
(
    id   UInt64,
    line String,
    INDEX idx(line) TYPE text(
        tokenizer    = 'splitByString',
        postprocessor = if(isNull(parseDateTimeOrNull(line, '%Y-%m-%dT%H:%i:%S')), line, '')
    )
)
ENGINE = MergeTree ORDER BY id;

-- Only message-level words are indexed; timestamp tokens are not stored.
SELECT count() FROM logs WHERE hasAllTokens(line, ['ERROR']);       -- fast index lookup
SELECT count() FROM logs WHERE hasAllTokens(line, ['2024-01-15T10:23:45']);  -- returns 0: token was never indexed
```

* プリプロセッサ式でタイムスタンプを削除します:

```sql theme={null}
-- The preprocessor strips the ISO timestamp prefix before tokenization.
-- Any tokenizer can be used; timestamp characters are never seen by the tokenizer.
CREATE TABLE logs
(
    id   UInt64,
    line String,
    INDEX idx(line) TYPE text(
        tokenizer   = 'splitByNonAlpha',
        preprocessor = replaceRegexpAll(line, '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2} ', '')
    )
)
ENGINE = MergeTree ORDER BY id;
```

* プリプロセッサとポストプロセッサを組み合わせた式でタイムスタンプを削除します:

```sql theme={null}
-- Preprocessor strips the timestamp, then lowercases the remainder.
-- Postprocessor drops the severity word (error, info, warn, debug) after tokenization.
-- Result: only substantive message words are stored in the index.
CREATE TABLE logs
(
    id   UInt64,
    line String,
    INDEX idx(line) TYPE text(
        tokenizer    = 'splitByNonAlpha',
        preprocessor = lower(replaceRegexpAll(line, '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2} ', '')),
        postprocessor = if(line IN ('error', 'info', 'warn', 'warning', 'debug', 'critical'), '', line)
    )
)
ENGINE = MergeTree ORDER BY id;

-- Example log line: '2024-01-15T10:23:45 ERROR connection failed'
-- After preprocessor:  'error connection failed'
-- After tokenization:  ['error', 'connection', 'failed']
-- After postprocessor: ['connection', 'failed']   ← 'error' dropped as severity word
SELECT count() FROM logs WHERE hasAllTokens(line, ['connection']);
```

* 後処理式でトークンをステミングします:

```sql theme={null}
CREATE TABLE table
(
    str String,
    INDEX idx(str) TYPE text(
        tokenizer = 'splitByNonAlpha',
        postprocessor = stem(str, 'en')
    )
)
ENGINE = MergeTree
ORDER BY tuple();

-- The query token 'running' is stemmed to 'run' before the lookup,
-- matching rows that contain 'run', 'runs', 'ran', 'running', etc.
SELECT count() FROM table WHERE hasAllTokens(str, ['running']);
```

**関数のサポート**。

テキスト索引を参照する述語では、索引構築時に格納されたものと同じトークンを索引のルックアップで使えるように、グラニュールレベルのチェックの前にプリプロセッサとポストプロセッサが検索値に適用されます。
ほとんどの関数 (`=`, `IN`, `startsWith`, `endsWith`, `LIKE`, `mapContains*`) では、テキスト索引は無関係なデータブロックをスキップするためにのみ使用されます。ClickHouse はその後も、元のカラムデータに対して元の述語で、残った各行を検証します。
トークン検索関数 (`hasToken`, `hasAllTokens`, `hasAnyTokens`) では、テキスト索引が主な評価経路になります。ClickHouse は、索引構築時に適用されたものと同じプリプロセッサ、トークナイザー、ポストプロセッサを通して needle を正規化し、この正規化形を索引付き・索引なしの両方のテーブルパーツに対して使用します。ポストプロセッサがある場合は、haystack のトークンもクエリ時に正規化されるため (`array` に限らず、どのトークナイザーでも同様) 、比較の両側が一貫して変換され、結果は索引を直接読み取るかどうか (設定 `query_plan_direct_read_from_text_index`) 、または特定のパーツにマテリアライズされた索引があるかどうかに依存しません。たとえば、`lower` ポストプロセッサを使うと、`hasAllTokens(col, ['FOO'])` で大文字と小文字を区別しないマッチングを有効にできます。
`support_phrase_search` がない場合、`hasPhrase` は索引をヒントとしてのみ使用し、残った各行を元の述語で検証します。さらに、ポストプロセッサはフレーズと haystack のトークンの両方を同じ方法で正規化するため、結果は読み取り経路に依存せず、ポストプロセッサが削除するトークンによってフレーズの隣接関係が崩れることもありません。`support_phrase_search = 1` の場合、`hasPhrase` は正確な direct read を使用します (存在する場合は、ポストプロセッサも引き続き適用されます) 。
ポストプロセッサによって空文字列にマップされる検索トークンは無視され、つまり検索フレーズに存在しないものとして扱われます。

| 関数                                                                                                         | プリプロセッサ対応                   | 対応トークナイザー                                                       | ポストプロセッサ対応 |
| ---------------------------------------------------------------------------------------------------------- | --------------------------- | --------------------------------------------------------------- | ---------- |
| `=`                                                                                                        | はい                          | すべて                                                             | はい         |
| `IN`                                                                                                       | はい                          | すべて                                                             | はい         |
| [hasToken](/ja/reference/functions/regular-functions/string-search-functions#hasToken)                     | はい                          | すべて (`splitByNonAlpha` 向けに設計)                                   | はい         |
| [hasAnyTokens(col, str)](/ja/reference/functions/regular-functions/string-search-functions#hasAnyTokens)   | はい                          | すべて                                                             | はい         |
| [hasAllTokens(col, str)](/ja/reference/functions/regular-functions/string-search-functions#hasAllTokens)   | はい                          | すべて                                                             | はい         |
| [hasAnyTokens(col, arr)](/ja/reference/functions/regular-functions/string-search-functions#hasAnyTokens)   | いいえ (配列要素はそのままトークンとして扱われます) | すべて                                                             | はい         |
| [hasAllTokens(col, arr)](/ja/reference/functions/regular-functions/string-search-functions#hasAllTokens)   | いいえ (配列要素はそのままトークンとして扱われます) | すべて                                                             | はい         |
| [hasPhrase](/ja/reference/functions/regular-functions/string-search-functions#hasPhrase)                   | はい                          | `splitByNonAlpha`, `splitByString`, `ngrams`, `asciiCJK`, `icu` | はい         |
| [startsWith](/ja/reference/functions/regular-functions/string-functions#startsWith)                        | はい                          | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`          | はい         |
| [endsWith](/ja/reference/functions/regular-functions/string-functions#endsWith)                            | はい                          | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`          | はい         |
| [like](/ja/reference/functions/regular-functions/string-search-functions#like)                             | はい¹                         | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`¹         | はい¹        |
| [match](/ja/reference/functions/regular-functions/string-search-functions#match)                           | はい¹                         | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`¹         | はい¹        |
| [ilike](/ja/reference/functions/regular-functions/string-search-functions#like)                            | はい² (`lower`/`upper` のみ)    | `splitByNonAlpha`, `array`²                                     | いいえ²       |
| [mapContainsKey](/ja/reference/functions/regular-functions/tuple-map-functions#mapContainsKey)             | はい                          | すべて                                                             | はい         |
| [mapContainsValue](/ja/reference/functions/regular-functions/tuple-map-functions#mapContainsValue)         | はい                          | すべて                                                             | はい         |
| [mapContainsKeyLike](/ja/reference/functions/regular-functions/tuple-map-functions#mapContainsKeyLike)     | はい                          | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`          | はい         |
| [mapContainsValueLike](/ja/reference/functions/regular-functions/tuple-map-functions#mapContainsValueLike) | はい                          | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`          | はい         |
| [has](/ja/reference/functions/regular-functions/array-functions#has)                                       | はい                          | `array`                                                         | はい         |
| [hasAny](/ja/reference/functions/regular-functions/array-functions#hasAny)                                 | はい                          | `array`                                                         | はい         |
| [hasAll](/ja/reference/functions/regular-functions/array-functions#hasAll)                                 | はい                          | `array`                                                         | はい         |

¹ `LIKE` と `match` は、記載されたトークナイザーではヒントとして direct read を使用し、それ以外では 総当たりスキャン にフォールバックします。
`LIKE` はさらに、プリプロセッサやポストプロセッサを使わない `splitByNonAlpha` および `array` トークナイザーに対して、*direct read (ヒントなし) * もサポートします (`use_text_index_like_evaluation_by_dictionary_scan` で有効化) 。

² `ILIKE` は、direct read (ヒントなし) でのみサポートされます (`use_text_index_like_evaluation_by_dictionary_scan = 1`、`splitByNonAlpha` または `array` トークナイザー) 。
索引をヒントとして使うフォールバックはありません。設定が無効になっている場合、またはトークナイザーがサポート対象外の場合、`ILIKE` では索引は使用されません。
プリプロセッサがある場合は `lower` または `upper` である必要があり、ポストプロセッサはサポートされません。

**Experimental: フレーズ検索引数のサポート (任意) **。

実験的なパラメータ `support_phrase_search` (デフォルト: `0`) は、索引にトークン位置を保存するかどうかを制御します。
`1` に設定すると、索引は位置データ (`.pos` ファイル内) も追加で保存し、これにより [`hasPhrase`](#functions-example-hasphrase) 関数で direct read を介した完全なフレーズ一致が可能になります。
位置情報を保存すると、索引のディスク上のサイズと書き込みコストが増加するため、これはオプトインです。
ディスク上フォーマットはまだ stable ではないため、このパラメータは Experimental であり、将来の release で変更される可能性があります。
そのため、`support_phrase_search = 1` を指定して索引を作成するには、MergeTree setting [`allow_experimental_text_index_phrase_search`](/ja/reference/settings/merge-tree-settings/allow-experimental#allow_experimental_text_index_phrase_search) を有効にする必要があります。
ポスティングリストのみの保存を維持するには `support_phrase_search = 0` (デフォルト) を設定してください。この引数を指定せずに作成されたテキスト索引には位置情報は含まれません。

<Warning>
  この引数は Experimental であり、テスト用途でのみ使用してください。
  位置情報の保存を有効にするには、MergeTree setting [`allow_experimental_text_index_phrase_search`](/ja/reference/settings/merge-tree-settings/allow-experimental#allow_experimental_text_index_phrase_search) を設定してください。
</Warning>

<details markdown="1">
  <summary>任意の詳細パラメータ</summary>

  以下の詳細パラメータのデフォルト値は、ほぼあらゆる状況で適切に機能します。
  これらの値を変更することは推奨していません。

  任意のパラメータ `dictionary_block_size` (デフォルト: 512) は、辞書ブロックのサイズを行数で指定します。

  任意のパラメータ `dictionary_block_frontcoding_compression` (デフォルト: 1) は、辞書ブロックで圧縮として front coding を使用するかどうかを指定します。

  任意のパラメータ `posting_list_block_size` (デフォルト: 1048576) は、ポスティングリストブロックのサイズを行数で指定します。

  任意のパラメータ `posting_list_codec` (デフォルト: `none`) は、ポスティングリストに使用するコーデックを指定します。

  * `none` - ポスティングリストは追加の圧縮を行わずに保存されます。
  * `bitpacking` - [差分 (delta) 符号化](https://en.wikipedia.org/wiki/Delta_encoding) を適用した後、[bit-packing](https://dev.to/madhav_baby_giraffe/bit-packing-the-secret-to-optimizing-data-storage-and-transmission-m70) を適用します (いずれも固定サイズのブロック内で実行されます) 。SELECT クエリが遅くなるため、現時点では推奨されません。

  上記の詳細パラメータは、対応する MergeTree settings を通じてテーブルレベルで設定することもできます: [`text_index_dictionary_block_size`](/ja/reference/settings/merge-tree-settings/text-index#text_index_dictionary_block_size), [`text_index_dictionary_block_frontcoding_compression`](/ja/reference/settings/merge-tree-settings/text-index#text_index_dictionary_block_frontcoding_compression), [`text_index_posting_list_block_size`](/ja/reference/settings/merge-tree-settings/text-index#text_index_posting_list_block_size), および [`text_index_posting_list_codec`](/ja/reference/settings/merge-tree-settings/text-index#text_index_posting_list_codec)。
  これらは、パラメータを明示的に指定していない、そのテーブル内のすべてのテキスト索引に適用されます。

  テーブルレベル設定の主なユースケースは、すべてのテーブルパーツ上のテキスト索引を削除して再作成することなく、既存テーブルの索引パラメータを変更することです。
  テーブルレベル設定を変更すると、新しいパーツ用に構築されるテキスト索引にのみ新しいパラメータが適用され、既存のパーツは現在のレイアウトを維持します。

  たとえば、索引定義で指定された引数はテーブル設定よりも優先されます:

  ```sql theme={null}
  CREATE TABLE table(
      s String,
      -- この索引は 'bitpacking' を使用し、以下のテーブルレベルのデフォルトを上書きします:
      INDEX idx_a s TYPE text(tokenizer = 'splitByNonAlpha', posting_list_codec = 'bitpacking'),
      -- この索引はテーブル設定から 'none' を継承します:
      INDEX idx_b lower(s) TYPE text(tokenizer = 'splitByNonAlpha'))
  ENGINE = MergeTree()
  ORDER BY tuple()
  SETTINGS text_index_posting_list_codec = 'none';
  ```
</details>

*索引の粒度。*
テキスト索引は、ClickHouse では [スキップ索引](/ja/reference/engines/table-engines/mergetree-family/mergetree#skip-index-types) の一種として実装されています。
ただし、他のスキップ索引とは異なり、テキスト索引では無限粒度 (1 億) が使用されます。
これは、テキスト索引のテーブル定義を見ると確認できます。

例:

```sql title="Query" theme={null}
CREATE TABLE table(
    k UInt64,
    s String,
    INDEX idx s TYPE text(tokenizer = ngrams(2)))
ENGINE = MergeTree()
ORDER BY k;

SHOW CREATE TABLE table;
```

```result title="Response" theme={null}
┌─statement──────────────────────────────────────────────────────────────┐
│ CREATE TABLE default.table                                            ↴│
│↳(                                                                     ↴│
│↳    `k` UInt64,                                                       ↴│
│↳    `s` String,                                                       ↴│
│↳    INDEX idx s TYPE text(tokenizer = ngrams(2)) GRANULARITY 100000000↴│ <-- here
│↳)                                                                     ↴│
│↳ENGINE = MergeTree                                                    ↴│
│↳ORDER BY k                                                            ↴│
│↳SETTINGS index_granularity = 8192                                      │
└────────────────────────────────────────────────────────────────────────┘
```

非常に大きな索引粒度により、テキスト索引はパート全体に対して作成されます。
明示的に指定した索引粒度は無視されます。

<div id="using-a-text-index">
  ## テキスト索引の使用
</div>

SELECT クエリでテキスト索引を使用するのは簡単で、一般的な文字列検索関数は自動的に索引を利用します。
カラムまたはテーブルパートに索引がない場合、文字列検索関数は低速な総当たりスキャンにフォールバックします。

<Note>
  テキスト索引の検索には、関数 `hasAnyTokens` および `hasAllTokens` の使用を推奨します。詳しくは[以下](#functions-example-hasanytokens-hasalltokens)を参照してください。
  これらの関数は、利用可能なすべてのトークナイザーと、あらゆるプリプロセッサ式およびポストプロセッサ式に対応しています。
  一方、その他のサポート対象の関数は歴史的にテキスト索引より前から存在していたため、多くの場合で従来の動作を維持する必要がありました (例: プリプロセッサまたはポストプロセッサをサポートしない) 。
</Note>

<div id="functions-support">
  ### サポートされている関数
</div>

テキスト関数を `WHERE` 句または `PREWHERE` 句で使用している場合は、テキスト索引を利用できます。

```sql theme={null}
SELECT [...]
FROM [...]
WHERE string_search_function(column_with_text_index)
```

<div id="functions-example-equals">
  #### `=`
</div>

`=` ([equals](/ja/reference/functions/regular-functions/comparison-functions#equals)) は、指定された検索語全体と一致します。

例:

```sql theme={null}
SELECT * from table WHERE str = 'Hello';
```

<div id="functions-example-in">
  #### `IN`
</div>

`IN` ([in](/ja/reference/functions/regular-functions/in-functions)) は `equals` と似ていますが、すべての検索語句に一致します。

例:

```sql theme={null}
SELECT * from table WHERE str IN ('Hello', 'World');
```

<Note>
  テキスト索引では、`NOT IN` (`notIn`) はサポートされていません。
</Note>

<div id="functions-example-like-match">
  #### `LIKE` と `match`
</div>

<Note>
  現在、これらの関数でフィルタリングにテキスト索引が使用されるのは、索引のトークナイザーが `splitByNonAlpha`、`ngrams`、または `sparseGrams` のいずれかである場合に限られます。
</Note>

<Note>
  `NOT LIKE` (`notLike`) はテキスト索引ではサポートされていません。
</Note>

テキスト索引で `LIKE` ([like](/ja/reference/functions/regular-functions/string-search-functions#like)) および [match](/ja/reference/functions/regular-functions/string-search-functions#match) 関数を使用するには、ClickHouse が検索語から完全なトークンを抽出できる必要があります。
`ngrams` トークナイザーを使用する索引では、ワイルドカードに挟まれた検索文字列の長さが N-gram の長さ以上であれば、これに該当します。

`splitByNonAlpha` トークナイザーを使用するテキスト索引の例:

```sql theme={null}
SELECT count() FROM table WHERE comment LIKE 'support%';
```

`support` はこの例では、`support`、`supports`、`supporting` などに一致する可能性があります。
この種のクエリは部分文字列クエリであり、テキスト索引で高速化することはできません。

LIKE クエリでテキスト索引を活用するには、LIKE パターンを次のように書き換える必要があります。

```sql theme={null}
SELECT count() FROM table WHERE comment LIKE ' support %'; -- または `% support %`
```

`support` の左右に空白があることで、その語を token として抽出できます。

幸い、ClickHouse が転置索引を活用して LIKE クエリを大幅に高速化できる特別なケースがあります。

詳しくは、[LIKE/ILIKE パフォーマンスチューニングのセクション](#like-ilike-queries-perf)を参照してください。

<div id="functions-example-multisearchany-multimatchany">
  #### `multiSearchAny` と `multiMatchAny`
</div>

[multiSearchAny](/ja/reference/functions/regular-functions/string-search-functions#multiSearchAny) とその UTF-8 版である [multiSearchAnyUTF8](/ja/reference/functions/regular-functions/string-search-functions#multiSearchAnyUTF8) は、複数のリテラルな部分文字列のうちいずれかが検索対象文字列に現れるかどうかを判定し、[multiMatchAny](/ja/reference/functions/regular-functions/string-search-functions#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` トークナイザーを使ったテキスト索引の例:

```sql theme={null}
SELECT count() FROM table WHERE multiSearchAny(comment, ['clickhouse', 'support']);
```

これに対して、`splitByNonAlpha` トークナイザーは完全なトークン (単語全体) だけを索引付けします。
needle は単語の途中で始まったり終わったりすることがあるため、ClickHouse は各 needle の先頭と末尾のトークンを除外します。そのため、索引がグラニュールを絞り込めるのは、完全なトークンを使う場合に限られます。
`splitByNonAlpha` で substring 検索や正規表現検索に索引を利用させるには、各 needle を区切り文字 (スペースなど) で囲み、1 つ以上の完全なトークンになるようにします。

`splitByNonAlpha` トークナイザーを使用したテキスト索引の例:

```sql theme={null}
SELECT count() FROM table WHERE multiSearchAny(comment, [' clickhouse ', ' support ']);
```

<div id="functions-example-startswith-endswith">
  #### `startsWith` and `endsWith`
</div>

`LIKE` と同様に、関数 [startsWith](/ja/reference/functions/regular-functions/string-functions#startsWith) と [endsWith](/ja/reference/functions/regular-functions/string-functions#endsWith) も、検索語から完全なトークンを抽出できる場合にのみ、テキスト索引を利用できます。
`ngrams` トークナイザーを使用する索引では、ワイルドカードに挟まれた検索文字列の長さが N-gram 長以上の場合に該当します。
テキスト索引で ポストプロセッサ を使用している場合でも、正規化後に抽出されたヒント トークン が空でなければ、これらの関数は Hint モードでその索引を利用できます。正規化によってすべてのヒント トークン が削除される場合、その predicate では索引は使用されません。

`splitByNonAlpha` トークナイザーを使用するテキスト索引の例:

```sql theme={null}
SELECT count() FROM table WHERE startsWith(comment, 'clickhouse support');
```

この例では、トークンとして扱われるのは `clickhouse` のみです。
`support` は `support`、`supports`、`supporting` などに一致する可能性があるため、トークンではありません。

`clickhouse supports` で始まるすべての行を検索するには、検索パターンの末尾にスペースを入れてください：

```sql theme={null}
startsWith(comment, 'clickhouse supports ')`
```

同様に、`endsWith` も先頭にスペースを付けて使用する必要があります。

```sql theme={null}
SELECT count() FROM table WHERE endsWith(comment, ' olap engine');
```

<div id="functions-example-hastoken">
  #### `hasToken`
</div>

<Note>
  `hasToken` は、非 `splitByNonAlpha` トークナイザーやプリプロセッサ/ポストプロセッサ式を使用したテキスト索引でのルックアップに用いる場合、いくつか注意点があります。
  代わりに、`hasAnyTokens` と `hasAllTokens` を使用することを推奨します。

  大文字小文字を区別しないバリアントである `hasTokenCaseInsensitive` と `hasTokenCaseInsensitiveOrNull` はテキスト索引を考慮しません。テキスト索引付きのカラムであっても、常に全行スキャンとして実行されます。大文字小文字を区別しない照合を行うには、`lower(...)` のプリプロセッサまたはポストプロセッサを使用し、それを `hasToken` / `hasAllTokens` / `hasAnyTokens` と組み合わせてください。
</Note>

関数 [hasToken](/ja/reference/functions/regular-functions/string-search-functions#hasToken) は、指定した単一のトークンに対して照合を行います。

前述の関数とは異なり、これらの関数は検索語をトークン化しません (入力が単一のトークンであることを前提としています) 。

例:

```sql theme={null}
SELECT count() FROM table WHERE hasToken(comment, 'clickhouse');
```

<div id="functions-example-hasanytokens-hasalltokens">
  #### `hasAnyTokens` と `hasAllTokens`
</div>

関数 [hasAnyTokens](/ja/reference/functions/regular-functions/string-search-functions#hasAnyTokens) と [hasAllTokens](/ja/reference/functions/regular-functions/string-search-functions#hasAllTokens) は、指定したトークンのいずれか、またはすべてに一致します。

これら 2 つの関数では、検索トークンとして、索引カラムで使用されているものと同じトークナイザーでトークン化される文字列、または検索前にトークン化されない、処理済みトークンの配列を指定できます。
詳しくは、各関数のドキュメントを参照してください。

例:

```sql theme={null}
-- 文字列引数として渡された検索トークン
SELECT count() FROM table WHERE hasAnyTokens(comment, 'clickhouse olap');
SELECT count() FROM table WHERE hasAllTokens(comment, 'clickhouse olap');

-- Array(String)として渡された検索トークン
SELECT count() FROM table WHERE hasAnyTokens(comment, ['clickhouse', 'olap']);
SELECT count() FROM table WHERE hasAllTokens(comment, ['clickhouse', 'olap']);
```

<div id="functions-example-hasphrase">
  #### `hasPhrase`
</div>

関数 [hasPhrase](/ja/reference/functions/regular-functions/string-search-functions#hasPhrase) はフレーズとの一致を判定します。すべてのトークンが連続して、かつ検索文字列と同じ順序で出現する必要があります。

すべてのトークンがどこかに含まれていればよい `hasAllTokens` とは異なり、`hasPhrase` ではそれらが連続した並びとして出現する必要があります。
検索フレーズは、索引カラムに設定されているものと同じトークナイザーでトークン化されます。
テキスト索引で ポストプロセッサ を使用している場合、検索フレーズも索引のルックアップ前に正規化されます。
この関数を使用するには、`splitByNonAlpha`、`splitByString`、`ngrams`、`asciiCJK`、`icu` のいずれかのトークナイザーが必要です。

例:

```sql theme={null}
-- Matches: 'clickhouse' and 'olap' must appear consecutively in that order
SELECT count() FROM table WHERE hasPhrase(comment, 'clickhouse olap');

-- Does NOT match a row containing 'olap clickhouse' (wrong order)
-- Does NOT match a row containing 'clickhouse fast olap' (non-consecutive)
```

<div id="functions-example-has">
  #### `has`
</div>

Array関数 [has](/ja/reference/functions/regular-functions/array-functions#has) は、String の配列内の単一のトークン にマッチします。

例:

```sql theme={null}
SELECT count() FROM table WHERE has(array, 'clickhouse');
```

<div id="functions-example-hasany-hasall">
  #### `hasAny` and `hasAll`
</div>

Array 関数の [hasAny](/ja/reference/functions/regular-functions/array-functions#hasAny) と [hasAll](/ja/reference/functions/regular-functions/array-functions#hasAll) は、索引が設定された配列カラムに、定数の検索文字列の集合のいずれかまたはすべてが含まれているかどうかを判定します。

例:

```sql theme={null}
SELECT count() FROM table WHERE hasAny(tags, ['clickhouse', 'olap']);
SELECT count() FROM table WHERE hasAll(tags, ['clickhouse', 'olap']);
```

<div id="functions-example-mapcontains">
  #### `mapContains`
</div>

関数 [mapContains](/ja/reference/functions/regular-functions/tuple-map-functions#mapContainsKey) (`mapContainsKey` のエイリアス) は、マップのキーに対して、検索文字列から抽出されたトークンとの照合を行います。
この動作は、`String` カラムに対する `equals` 関数と似ています。
テキスト索引が使用されるのは、`mapKeys(map)` 式に対して作成されている場合のみです。

例:

```sql theme={null}
SELECT count() FROM table WHERE mapContainsKey(map, 'clickhouse');
-- OR
SELECT count() FROM table WHERE mapContains(map, 'clickhouse');
```

<div id="functions-example-mapcontainsvalue">
  #### `mapContainsValue`
</div>

関数 [mapContainsValue](/ja/reference/functions/regular-functions/tuple-map-functions#mapContainsValue) は、map の値について、検索対象の文字列から抽出されたトークンとの一致を判定します。
この動作は、`String` カラムに対する `equals` 関数に似ています。
テキスト索引が使用されるのは、`mapValues(map)` 式に対して作成されている場合のみです。

例:

```sql theme={null}
SELECT count() FROM table WHERE mapContainsValue(map, 'clickhouse');
```

<div id="functions-example-mapcontainslike">
  #### `mapContainsKeyLike` and `mapContainsValueLike`
</div>

関数 [mapContainsKeyLike](/ja/reference/functions/regular-functions/tuple-map-functions#mapContainsKeyLike) と [mapContainsValueLike](/ja/reference/functions/regular-functions/tuple-map-functions#mapContainsValueLike) は、Map のすべてのキーまたは値に対して、それぞれパターン照合を行います。

例:

```sql theme={null}
SELECT count() FROM table WHERE mapContainsKeyLike(map, '% clickhouse %');
SELECT count() FROM table WHERE mapContainsValueLike(map, '% clickhouse %');
```

<div id="functions-example-access-operator">
  #### `operator[]`
</div>

アクセス[operator\[\]](/ja/reference/operators/index#access-operators)は、テキスト索引と組み合わせて使用することで、キーと値を絞り込めます。テキスト索引が使用されるのは、`mapKeys(map)` または `mapValues(map)` 式、あるいはその両方に対して作成されている場合のみです。

例:

```sql theme={null}
SELECT count() FROM table WHERE map['engine'] = 'clickhouse';
```

テキスト索引で `Array(T)` 型および `Map(K, V)` 型のカラムを使用する方法については、以下の例を参照してください。

<div id="text-index-example-array">
  ### Array(String) カラムの索引作成
</div>

著者がキーワードでブログ記事を分類するブログプラットフォームを想像してみてください。
ユーザーがトピックを検索したりクリックしたりして、関連するコンテンツを見つけられるようにしたいとします。

次のテーブル定義を考えてみましょう。

```sql theme={null}
CREATE TABLE posts
(
    post_id UInt64,
    title String,
    content String,
    keywords Array(String)
)
ENGINE = MergeTree
ORDER BY (post_id);
```

テキスト索引がない場合、特定のキーワード (例: `clickhouse`) を含む投稿を見つけるには、すべてのエントリをスキャンする必要があります。

```sql theme={null}
SELECT count() FROM posts WHERE has(keywords, 'clickhouse'); -- 低速なフルテーブルスキャン - すべての投稿のすべてのキーワードをチェックする
```

プラットフォームの拡大に伴い、クエリは各行の `keywords` 配列をすべて調べる必要があるため、これは次第に遅くなります。
このパフォーマンス上の問題を解決するため、カラム `keywords` にテキスト索引を定義します。

```sql theme={null}
ALTER TABLE posts ADD INDEX keywords_idx(keywords) TYPE text(tokenizer = splitByNonAlpha);
ALTER TABLE posts MATERIALIZE INDEX keywords_idx; -- 既存データの索引の再構築を忘れずに
```

<div id="text-index-example-map">
  ### Mapカラムの索引付け
</div>

オブザーバビリティの多くのユースケースでは、ログメッセージを「要素」に分割し、それぞれを適切なデータ型で保存します。たとえば、timestamp には日時、ログレベルには enum などを使用します。
メトリクスのフィールドは、キー・バリューのペアとして保存するのが最適です。
運用チームは、デバッグ、セキュリティインシデント、監視のために、ログを効率的に検索できる必要があります。

次のログテーブルを考えてみましょう:

```sql theme={null}
CREATE TABLE logs
(
    id UInt64,
    timestamp DateTime,
    message String,
    attributes Map(String, String)
)
ENGINE = MergeTree
ORDER BY (timestamp);
```

テキスト索引がない場合、[Map](/ja/reference/data-types/map) データを検索するには、テーブル全体をスキャンする必要があります。

```sql theme={null}
-- rate limitingデータを含むすべてのログを検索:
SELECT * FROM logs WHERE has(mapKeys(attributes), 'rate_limit'); -- 低速なフルテーブルスキャン

-- 特定のIPからのすべてのログを検索:
SELECT * FROM logs WHERE has(mapValues(attributes), '192.168.1.1'); -- 低速なフルテーブルスキャン
```

ログの量が増えると、これらのクエリは遅くなります。

解決策は、[Map](/ja/reference/data-types/map) のキーと値に対してテキスト索引を作成することです。
フィールド名や属性タイプでログを検索する必要がある場合は、[mapKeys](/ja/reference/functions/regular-functions/tuple-map-functions#mapKeys) を使ってテキスト索引を作成します。

```sql theme={null}
ALTER TABLE logs ADD INDEX attributes_keys_idx mapKeys(attributes) TYPE text(tokenizer = array);
ALTER TABLE posts MATERIALIZE INDEX attributes_keys_idx;
```

属性の実際の内容内を検索する必要がある場合は、[mapValues](/ja/reference/functions/regular-functions/tuple-map-functions#mapValues) を使用してテキスト索引を作成します。

```sql theme={null}
ALTER TABLE logs ADD INDEX attributes_vals_idx mapValues(attributes) TYPE text(tokenizer = array);
ALTER TABLE posts MATERIALIZE INDEX attributes_vals_idx;
```

クエリの例:

```sql theme={null}
-- レート制限されたリクエストをすべて検索:
SELECT * FROM logs WHERE mapContainsKey(attributes, 'rate_limit'); -- fast

-- 特定のIPからのログをすべて検索:
SELECT * FROM logs WHERE has(mapValues(attributes), '192.168.1.1'); -- fast

-- いずれかの属性にエラーが含まれるログをすべて検索:
SELECT * FROM logs WHERE mapContainsValueLike(attributes, '% error %'); -- fast
```

<div id="text-index-example-json">
  ### JSONカラムの索引付け
</div>

テキスト索引は、`JSON`カラムに対して次の 3 つの方法で使用できます。

1. **特定のサブカラムに対する索引** — 通常のカラムと同じように、既知の JSON パスにテキスト索引を作成します。これにより、そのパスにある*値*が索引化されます。
2. **[JSONAllPaths](/ja/reference/functions/regular-functions/json-functions#JSONAllPaths) を使用したパスベースの索引** — 各グラニュールに存在する*すべてのパス*を索引化し、クエリ対象のパスを含み得ないグラニュールをスキップします。`Map`カラムの場合と同様です。
3. **[JSONAllValues](/ja/reference/functions/regular-functions/json-functions#JSONAllValues) を使用した値ベースの索引** — すべての JSON パスにまたがる*すべての値*を索引化し、単一の索引で任意の JSON サブカラムに対する全文検索を高速化します。

<div id="json-indexes-on-subcolumns">
  #### 特定のサブカラムに対する索引
</div>

通常のカラムと同じ構文で、任意の JSON サブカラムにスキップ索引を作成できます。

索引式で JSON サブカラムを参照する方法は 2 つあります。

* JSON 型ヒントで宣言された **型付きパス** — 名前で直接アクセスします: `json.a`
* 明示的にキャストする **動的パス** — `::` キャスト構文を使用します: `json.b::String`

索引定義の例:

```sql title="Query" theme={null}
CREATE TABLE sensor_data
(
    data JSON(sensor_id String),
    INDEX idx_sensor data.sensor_id TYPE text(tokenizer = splitByNonAlpha),
    INDEX idx_location data.location::String TYPE text(tokenizer = splitByNonAlpha)
)
ENGINE = MergeTree
ORDER BY tuple()
SETTINGS index_granularity = 1;

INSERT INTO sensor_data SELECT toJSONString(map('sensor_id', 'id_' || number , 'location', 'room_' || toString(number))) FROM numbers(4);
INSERT INTO sensor_data SELECT toJSONString(map('sensor_id', 'id_' || number, 'location', 'room_' || toString(number))) FROM numbers(4, 4);
```

クエリ例:

```sql title="Query" theme={null}
EXPLAIN indexes = 1 SELECT * FROM sensor_data WHERE data.sensor_id = 'id_5';
```

```text title="Response" theme={null}
...
    Indexes:
      Skip
        Name: idx_sensor
        Description: text
        Condition: (mode: All; tokens: ["5", "id"])
        Parts: 1/2
        Granules: 1/8
```

クエリの例:

```sql title="Query" theme={null}
EXPLAIN indexes = 1 SELECT * FROM sensor_data WHERE data.location::String = 'room_5';
```

```text title="Response" theme={null}
...
    Indexes:
      Skip
        Name: idx_location
        Description: text
        Condition: (mode: All; tokens: ["5", "room"])
        Parts: 1/2
        Granules: 1/8
```

<div id="json-indexes-jsonallpaths">
  #### JSONAllPaths を使用したパスベースの索引
</div>

`Map` カラムと同様に、[JSON](/ja/reference/data-types/newjson) カラムでも [`JSONAllPaths`](/ja/reference/functions/regular-functions/json-functions#JSONAllPaths) を使ってテキスト索引を作成できます。
この索引は各グラニュールに存在する JSON パスの集合を格納し、クエリされたパスが存在しないグラニュールをスキップするために利用されます。

索引定義の例:

```sql title="Query" theme={null}
CREATE TABLE events
(
    data JSON,
    INDEX idx JSONAllPaths(data) TYPE text(tokenizer = array)
)
ENGINE = MergeTree
ORDER BY tuple();

INSERT INTO events VALUES ('{"user": {"name": "Alice"}, "action": "login"}');
INSERT INTO events VALUES ('{"metric": {"cpu": 0.95}, "host": "srv1"}');
```

`EXPLAIN indexes = 1` を使用すると、スキップ索引が使われていることを確認できます。
あるパスが一方のパートにしか存在しない場合、索引によってもう一方のパートはスキップされます。

例:

```sql title="Query" theme={null}
EXPLAIN indexes = 1 SELECT * FROM events WHERE data.user.name = 'Alice';
```

```text title="Response" theme={null}
...
    Indexes:
      Skip
        Name: idx
        Description: text
        Condition: (mode: All; tokens: ["user.name"])
        Parts: 1/2
        Granules: 1/2
```

そのパスがどのパーツにも存在しない場合、すべてのパーツとグラニュールはスキップされます。

例:

```sql title="Query" theme={null}
EXPLAIN indexes = 1 SELECT * FROM events WHERE data.nonexistent = 1;
```

```text title="Response" theme={null}
...
    Indexes:
      Skip
        Name: idx
        Description: text
        Condition: (mode: All; tokens: ["nonexistent"])
        Parts: 0/2
        Granules: 0/2
```

`IS NOT NULL` でも索引が使用され、path が存在しないグラニュールはスキップされます (その場合、値は `NULL` になるためです) ：

例:

```sql title="Query" theme={null}
EXPLAIN indexes = 1 SELECT * FROM events WHERE data.user.name IS NOT NULL;
```

```text title="Response" theme={null}
...
    Indexes:
      Skip
        Name: idx
        Description: text
        Condition: (mode: All; tokens: ["user.name"])
        Parts: 1/2
        Granules: 1/2
```

<div id="json-indexes-jsonallvalues">
  #### JSONAllValues を使用した値ベースの索引
</div>

テキスト索引を使用すると、関数 [`JSONAllValues`](/ja/reference/functions/regular-functions/json-functions#JSONAllValues) を介して [JSON](/ja/reference/data-types/newjson) カラムに対する検索を高速化できます。

`JSONAllValues` は、JSON カラム内のすべての値を `Array(String)` として返します。
文字列以外のデータ型の値 (たとえば整数や配列) は、テキスト表現に変換されます。
`JSONAllValues` を使って構築したテキスト索引は、各行のすべての JSON パスにまたがるこれらのテキスト表現に索引を作成します。
この索引により、個々の JSON サブカラムで絞り込むクエリを高速化できます。
クエリが特定のサブカラムでフィルタする場合 (例: `data.user_name = 'alice'`) 、テキスト索引は、どの JSON 値にも検索トークンが含まれていない行 (およびグラニュール) をすばやくスキップできます。

<Note>
  異なる JSON パスに同じトークンが含まれている場合、この索引で偽陽性が発生することがあります。
  たとえば、行 1 が `{"a": "hello", "b": "world"}` で、クエリが `data.a = 'world'` を検索する場合、テキスト索引では `world` がパス `a` ではなく `b` に属していることを区別できません。
  このような場合、索引はその行をスキップせず、実際のカラムデータに対するフィルタで最終的な評価が行われます。
  これは、索引が高速な事前フィルタとして機能する、他のテキスト索引のユースケースと同じ動作です。
</Note>

<div id="json-all-values-creating-the-index">
  ##### 索引の作成
</div>

索引定義の例:

```sql theme={null}
CREATE TABLE events
(
    id UInt64,
    data JSON,
    INDEX json_idx JSONAllValues(data) TYPE text(tokenizer = splitByNonAlpha)
)
ENGINE = MergeTree
ORDER BY id;
```

<div id="json-all-values-supported-query-patterns">
  ##### サポートされるクエリパターン
</div>

索引を作成すると、JSONサブカラムに対するクエリを高速化できます。使用できるのは、`String` カラムで使うものと同じ関数、およびすべてのカラムで使える関数 `equals` です。

サブカラムへのアクセス:

```sql theme={null}
SELECT * FROM events WHERE data.user_name = 'alice';
SELECT * FROM events WHERE data.message LIKE '% error %';
SELECT * FROM events WHERE startsWith(data.status, 'fail');
SELECT * FROM events WHERE hasToken(data.title, 'clickhouse');
```

明示的に `CAST` を使用したサブカラムへのアクセス:

```sql theme={null}
SELECT * FROM events WHERE hasAllTokens(data.message::String, 'connection timeout');
SELECT * FROM events WHERE data.status_code::UInt64 = 404;
SELECT * FROM events WHERE has(data.tags::Array(String), 'bug')
```

`IN` 演算子:

```sql theme={null}
SELECT * FROM events WHERE data.level IN ('error', 'critical');
```

<div id="text-index-phrase-search">
  ### フレーズ検索
</div>

通常のテキスト索引検索の例

```sql theme={null}
SELECT *
FROM tab
WHERE hasAllTokens(col, 'weather in Tokyo')
```

指定されたトークンを任意の順序で含むすべての行に一致します。
この例では、`While she stayed in Tokyo, the weather was great.` という行がフィルタに一致します。

これに対して、フレーズ検索では、指定された順序どおりに並んだトークンに一致します。
例えば、

```sql theme={null}
SELECT *
FROM tab
WHERE hasPhrase(col, 'weather in Tokyo')
```

`weather in Tokyo` というトークン列を含む任意の行 (たとえば `How is the weather in Tokyo?`) に一致しますか？

テキスト索引は、フレーズ内のすべてのトークンの posting list の積集合を求めて候補となる グラニュール を特定することで、フレーズ検索を高速化します。
その後、ClickHouse はそれらの グラニュール 内で、トークンが正確に隣接していることを検証します。
この処理は比較的コストが高く、通常のテキスト検索クエリより低速です。
フレーズ検索クエリを高速化するには、テキスト索引で位置情報の保存を有効にしてください (上記の `Optional parameters` を参照) 。

`hasPhrase` は、トークナイザー `splitByNonAlpha`、`splitByString`、`ngrams`、`asciiCJK`、`icu` とあわせて使用できます。
指定したフレーズ文字列は、索引のトークナイザーを使ってトークン化されます。
フレーズ内の区切り文字は無視されます。`splitByNonAlpha` をトークナイザーとして使用している場合、`hasPhrase(text, 'quick+brown')` は `hasPhrase(text, 'quick brown')` と同等です。

<div id="text-index-phrase-search-example">
  #### 例
</div>

```sql theme={null}
CREATE TABLE tab (
    id UInt32,
    text String,
    INDEX idx text TYPE text(tokenizer = splitByNonAlpha)
)
ENGINE = MergeTree
ORDER BY id;

INSERT INTO tab VALUES
    (1, 'weather in New York'),
    (2, 'New weather in York'),
    (3, 'weather in New Orleans');
```

```sql title="Query" theme={null}
SELECT id, text FROM tab WHERE hasPhrase(text, 'weather in New York');
```

```result title="Response" theme={null}
   ┌─id─┬─text────────────────┐
1. │  1 │ weather in New York │
   └────┴─────────────────────┘
```

2 行目 (`'New weather in York'`) は、トークンの順序が正しくないため一致しません。
3 行目 (`'weather in New Orleans'`) は、トークン `'York'` を含まないため一致しません。

<div id="performance-tuning">
  ## パフォーマンスチューニング
</div>

<div id="direct-read">
  ### Direct read
</div>

一部の種類のテキスト検索クエリは、「direct read」と呼ばれる最適化によって大幅に高速化できます。

例:

```sql theme={null}
SELECT column_a, column_b, ...
FROM [...]
WHERE string_search_function(column_with_text_index)
```

direct read 最適化では、基になるテキストカラムにアクセスせず、テキスト索引 (つまりテキスト索引ルックアップ) のみを使ってクエリを処理します。
テキスト索引ルックアップで読み取るデータ量は比較的少ないため、ClickHouse の通常のスキップ索引 (スキップ索引のルックアップを行った後、残りのグラニュールを読み込んでフィルタリングする方式) よりも大幅に高速です。

direct read は 2 つの設定で制御されます。

* 設定 [query\_plan\_direct\_read\_from\_text\_index](/ja/reference/settings/session-settings/query-plan#query_plan_direct_read_from_text_index) (デフォルトは true) は、direct read を全体として有効にするかどうかを指定します。
* 設定 [use\_skip\_indexes\_on\_data\_read](/ja/reference/settings/session-settings/use-skip-indexes#use_skip_indexes_on_data_read) は、ClickHouse バージョン \< 26.4 では direct read の前提条件でした。

**サポートされる関数**

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 を無効にしたクエリは

```sql theme={null}
EXPLAIN PLAN actions = 1
SELECT count()
FROM table
WHERE hasToken(col, 'some_token')
SETTINGS query_plan_direct_read_from_text_index = 0, -- disable direct read
```

戻り値

```text theme={null}
[...]
Filter ((WHERE + Change column names to column identifiers))
Filter column: hasToken(__table1.col, 'some_token'_String) (removed)
Actions: INPUT : 0 -> col String : 0
         COLUMN Const(String) -> 'some_token'_String String : 1
         FUNCTION hasToken(col :: 0, 'some_token'_String :: 1) -> hasToken(__table1.col, 'some_token'_String) UInt8 : 2
[...]
```

一方、同じクエリを `query_plan_direct_read_from_text_index = 1` を指定して実行すると

```sql theme={null}
EXPLAIN PLAN actions = 1
SELECT count()
FROM table
WHERE hasToken(col, 'some_token')
SETTINGS query_plan_direct_read_from_text_index = 1, -- enable direct read
```

戻り値

```text theme={null}
[...]
Expression (Before GROUP BY)
Positions:
  Filter
  Filter column: __text_index_idx_hasToken_94cc2a813036b453d84b6fb344a63ad3 (removed)
  Actions: INPUT :: 0 -> __text_index_idx_hasToken_94cc2a813036b453d84b6fb344a63ad3 UInt8 : 0
[...]
```

2 番目の EXPLAIN PLAN の出力には、仮想カラム `__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](/ja/reference/settings/session-settings/query-plan#query_plan_text_index_add_hint) で制御されます (デフォルトで有効) 。

ヒントなしのクエリの例:

```sql theme={null}
EXPLAIN actions = 1
SELECT count()
FROM table
WHERE (col LIKE '%some-token%') AND (d >= today())
SETTINGS query_plan_text_index_add_hint = 0
FORMAT TSV
```

戻り値

```text theme={null}
[...]
Prewhere filter column: and(like(__table1.col, \'%some-token%\'_String), greaterOrEquals(__table1.d, _CAST(20440_Date, \'Date\'_String))) (removed)
[...]
```

一方、`query_plan_text_index_add_hint = 1` を指定して同じクエリを実行した場合は

```sql theme={null}
EXPLAIN actions = 1
SELECT count()
FROM table
WHERE col LIKE '%some-token%'
SETTINGS query_plan_text_index_add_hint = 1
```

返す

```text theme={null}
[...]
Prewhere filter column: and(__text_index_idx_col_like_d306f7c9c95238594618ac23eb7a3f74, like(__table1.col, \'%some-token%\'_String), greaterOrEquals(__table1.d, _CAST(20440_Date, \'Date\'_String))) (removed)
[...]
```

2つ目の EXPLAIN PLAN の出力では、追加の論理積条件 (`__text_index_...`) がフィルタ条件に加えられていることがわかります。
[PREWHERE](/ja/reference/statements/select/prewhere) の最適化により、フィルタ条件は3つの個別の論理積条件に分解され、計算コストの低い順に適用されます。
このクエリでは、適用順は `__text_index_...`、次に `greaterOrEquals(...)`、最後に `like(...)` です。
この順序により、`WHERE` 句の後でクエリ内で使用される重いカラムを読み取る前に、テキスト索引と元のフィルタでスキップされるグラニュールよりもさらに多くのデータグラニュールをスキップでき、読み取るデータ量をいっそう削減できます。

<div id="like-ilike-queries-perf">
  ### LIKE/ILIKE クエリ
</div>

LIKE/ILIKE クエリのパターンが `%<スペースを含まない英数字文字>%` で、テキスト索引のトークナイザーが `splitByNonAlpha` または `array` の場合、ClickHouse は転置索引を利用して LIKE/ILIKE クエリを大幅に高速化します。これを実現するために、ClickHouse は一致するパターンを見つける際、フルテーブルスキャンの代わりに転置索引の Dictionary をスキャンします。

この最適化が有効な場合、LIKE/ILIKE クエリはフルテーブルスキャンより大幅に高速になるはずです。ただし、パターンが Dictionary 内のトークンの大半に一致する場合は、フルテーブルスキャンと比べて性能が悪化することがあります。幸い、それを防ぐためのフォールバックの仕組みがあります。

この最適化は、次の設定で制御されます。

* [use\_text\_index\_like\_evaluation\_by\_dictionary\_scan](/ja/reference/settings/session-settings/use-text#use_text_index_like_evaluation_by_dictionary_scan)

フォールバックの仕組みは、次の 2 つの設定で制御されます。

* [text\_index\_like\_min\_pattern\_length](/ja/reference/settings/session-settings/text-index#text_index_like_min_pattern_length)
* [text\_index\_like\_max\_postings\_to\_read](/ja/reference/settings/session-settings/text-index#text_index_like_max_postings_to_read)

この最適化でサポートされるのは、関数 `like` と `ilike` のみです。

<div id="count-queries-perf">
  ### 単純なカウントクエリ
</div>

テキスト検索の条件に一致する行だけをカウントするクエリ

```sql theme={null}
SELECT count()
FROM [...]
WHERE hasToken(column_with_text_index, 'token')
-- or: hasAllTokens(column_with_text_index, ['token', ...])
-- or: hasAnyTokens(column_with_text_index, ['token', ...])
```

テキスト索引から応答されるため、一致する行を読み取る必要はありません。
ClickHouse は索引内のポスティングリストのカーディナリティから count を取得するため、一致する行を示す仮想カラムやその集約をスキップします。単一トークンの場合は Dictionary のカーディナリティだけで済むため、ポスティングリストは読み取りません。複数トークンの述語では、ポスティングリストをユニオン (`hasAnyTokens`) または積集合 (`hasAllTokens`) する場合にのみ読み取ります。テキスト索引はパーツ全体をカバーするため、count は正確であり、非常に大きなパーツでも高速です。

この最適化は、単一の `hasToken`、`hasAnyTokens`、または `hasAllTokens` 述語 (または `Array`/`Map` での同等の表現) でフィルタリングされた単独の `count()` に適用されます。`AND`/`OR`/`NOT` で結合した述語、追加のフィルタ (例: `... AND id > 10`) 、`m['key'] = 'value'` のようなヒントモードの述語、count 以外の値の選択、フレーズ検索、または `LIKE`/パターン検索がある場合、クエリは代わりに行を読み取ります。マテリアライズされた索引を持たないパーツも、行を読み取ることで正しくカウントされます。

最適化が有効になっていることを確認するには、クエリプランに `ReadFromTextIndexCount` が含まれているか確認します。

```sql theme={null}
EXPLAIN
SELECT count() FROM table WHERE hasToken(col, 'sometoken')
```

```text theme={null}
[...]
Aggregating
└──ReadFromTextIndexCount (Trivial count from text index (idx, token = 'sometoken'))
```

この最適化は、[query\_plan\_optimize\_count\_from\_text\_index](/ja/reference/settings/session-settings/query-plan#query_plan_optimize_count_from_text_index) (デフォルトで有効) で制御されます。[direct read](#direct-read)を前提としているため、[query\_plan\_direct\_read\_from\_text\_index](/ja/reference/settings/session-settings/query-plan#query_plan_direct_read_from_text_index)、[use\_skip\_indexes](/ja/reference/settings/session-settings/use-skip-indexes#use_skip_indexes)、および[optimize\_trivial\_count\_query](/ja/reference/settings/session-settings/optimize-trivial#optimize_trivial_count_query)も有効にする必要があります (いずれもデフォルトで有効) 。

<div id="caching">
  ### キャッシュ
</div>

テキスト索引の一部をメモリ上に保持するための、サーバー全体で共有されるさまざまな cache があります ([実装の詳細](#implementation) セクションを参照してください) 。
現在、I/O を削減するために、テキスト索引のデシリアライズ済みヘッダー、トークン、ポスティングリスト用の cache が用意されています。
設定 [use\_text\_index\_header\_cache](/ja/reference/settings/session-settings/use-text#use_text_index_header_cache)、[use\_text\_index\_tokens\_cache](/ja/reference/settings/session-settings/use-text#use_text_index_tokens_cache)、および [use\_text\_index\_postings\_cache](/ja/reference/settings/session-settings/use-text#use_text_index_postings_cache) を使用すると、クエリによる個々の cache への読み取りと書き込みを無効にできます。
データパーツに存在しないトークンのキャッシュはデフォルトで有効になっており、[`use_text_index_negative_tokens_cache`](/ja/reference/settings/session-settings/use-text) を使用して個別に制御できます。

cache をクリアするには、ステートメント [SYSTEM CLEAR TEXT INDEX CACHES](/ja/reference/statements/system#drop-text-index-caches) を使用します。

cache を設定するには、以下のサーバー設定を参照してください。

<div id="caching-tokens">
  #### テキスト索引トークンcacheの設定
</div>

| Setting                                                                                                                                    | Description                                    |
| ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------- |
| [text\_index\_tokens\_cache\_policy](/ja/reference/settings/server-settings/settings/text-index#text_index_tokens_cache_policy)            | テキスト索引トークンcacheのcacheポリシー名。                    |
| [text\_index\_tokens\_cache\_size](/ja/reference/settings/server-settings/settings/text-index#text_index_tokens_cache_size)                | cacheの最大サイズ (バイト単位) 。                          |
| [text\_index\_tokens\_cache\_max\_entries](/ja/reference/settings/server-settings/settings/text-index#text_index_tokens_cache_max_entries) | cache内のデシリアライズ済みトークンの最大数。                      |
| [text\_index\_tokens\_cache\_size\_ratio](/ja/reference/settings/server-settings/settings/text-index#text_index_tokens_cache_size_ratio)   | cache全体のサイズに対する、テキスト索引トークンcache内の保護キューのサイズの比率。 |

<div id="caching-header">
  #### ヘッダーcacheの設定
</div>

| Setting                                                                                                                                    | Description                             |
| ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------- |
| [text\_index\_header\_cache\_policy](/ja/reference/settings/server-settings/settings/text-index#text_index_header_cache_policy)            | テキスト索引ヘッダーcacheのポリシー名。                  |
| [text\_index\_header\_cache\_size](/ja/reference/settings/server-settings/settings/text-index#text_index_header_cache_size)                | cacheの最大サイズ (バイト) 。                     |
| [text\_index\_header\_cache\_max\_entries](/ja/reference/settings/server-settings/settings/text-index#text_index_header_cache_max_entries) | cache内に保持できるデシリアライズ済みヘッダーの最大数。          |
| [text\_index\_header\_cache\_size\_ratio](/ja/reference/settings/server-settings/settings/text-index#text_index_header_cache_size_ratio)   | テキスト索引ヘッダーcache全体のサイズに対する、保護キューのサイズの比率。 |

<div id="caching-posting-lists">
  #### ポスティングリスト cache の設定
</div>

| Setting                                                                                                                                        | Description                                                  |
| ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| [text\_index\_postings\_cache\_policy](/ja/reference/settings/server-settings/settings/text-index#text_index_postings_cache_policy)            | テキスト索引のポスティング cache ポリシー名。                                   |
| [text\_index\_postings\_cache\_size](/ja/reference/settings/server-settings/settings/text-index#text_index_postings_cache_size)                | cache の最大サイズ (バイト単位) 。                                       |
| [text\_index\_postings\_cache\_max\_entries](/ja/reference/settings/server-settings/settings/text-index#text_index_postings_cache_max_entries) | cache 内のデシリアライズ済みポスティングの最大数。                                 |
| [text\_index\_postings\_cache\_size\_ratio](/ja/reference/settings/server-settings/settings/text-index#text_index_postings_cache_size_ratio)   | テキスト索引のポスティング cache における保護キューのサイズを、cache 全体のサイズに対する比率で指定します。 |

<div id="limitations">
  ## 制限事項
</div>

現在、テキスト索引には次の制限があります。

* トークン数が非常に多いテキスト索引 (例: 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 文字未満であれば、この状況が発生します。実際には、テーブルにはほかのカラムも含まれるため、しきい値はこれより何倍も小さくなります (ほかのカラムの数、型、サイズに依存します) 。

<div id="text-index-vs-bloom-filter-indexes">
  ## テキスト索引とブルームフィルタベースの索引の違い
</div>

文字列述語は、テキスト索引やブルームフィルタベースの索引 (索引タイプ `bloom_filter`、`ngrambf_v1`、`tokenbf_v1`、`sparse_grams`) によって高速化できますが、両者は設計と想定ユースケースの点で本質的に異なります。

**ブルームフィルタ索引**

* 偽陽性が発生しうる確率的データ構造に基づいています。
* 集合への所属判定、つまりそのカラムにトークン X が含まれている可能性があるか、あるいは確実に含まれていないか、ということしか判定できません。
* クエリ実行時に大まかな範囲をスキップできるよう、granule レベルの情報を格納します。
* 適切にチューニングするのが難しいです (例は [こちら](/ja/reference/engines/table-engines/mergetree-family/mergetree#n-gram-bloom-filter) を参照) 。
* 比較的コンパクトです (1 パーツあたり数 KB ～数 MB) 。

**テキスト索引**

* トークンに対して決定論的な転置索引を構築します。索引自体による偽陽性は発生しません。
* テキスト検索ワークロード向けに特化して最適化されています。
* 効率的な用語ルックアップを可能にするため、行レベルの情報を格納します。
* 比較的大きくなります (1 パーツあたり数十～数百 MB) 。

ブルームフィルタベースの索引が全文検索をサポートするのは、あくまで「副次的な効果」にすぎません。

* 高度なトークン化や前処理には対応していません。
* 複数トークンの検索には対応していません。
* 転置索引に期待されるような性能特性は得られません。

一方、テキスト索引は全文検索向けに専用設計されています。

* トークン化と前処理を提供します
* `hasAllTokens`、`LIKE`、`match` などのテキスト検索関数を効率的にサポートします。
* 大規模なテキストコーパスに対して、はるかに優れたスケーラビリティを発揮します。

<div id="implementation">
  ## 実装の詳細
</div>

各テキスト索引は、 (抽象的には) 2つのデータ構造で構成されます。

* 各トークンをポスティングリストに対応付けるDictionary
* それぞれが行番号の集合を表す、ポスティングリストの集合

テキスト索引は、パーツ全体に対して構築されます。
ほかのスキップ索引とは異なり、テキスト索引はデータパーツのマージ時に再構築するのではなく、そのままマージできます (詳細は以下を参照) 。

索引の作成時には、 (パーツごとに) 3つのファイルが作成されます。

**Dictionaryブロックファイル (.dct)**

テキスト索引内のトークンはソートされ、512トークンごとのDictionaryブロックに格納されます (ブロックサイズはパラメータ `dictionary_block_size` で設定できます) 。
Dictionaryブロックファイル (.dct) には、パーツ内のすべてのインデックスグラニュールに含まれるすべてのDictionaryブロックが格納されます。

**索引ヘッダーファイル (.idx)**

索引ヘッダーファイルには、各Dictionaryブロックについて、そのブロックの先頭トークンと、Dictionaryブロックファイル内での相対オフセットが格納されます。

このスパースインデックス構造は、ClickHouse の[スパース主キー索引](/ja/guides/clickhouse/data-modelling/sparse-primary-indexes)) に似ています。

**ポスティングリストファイル (.pst)**

すべてのトークンのポスティングリストは、ポスティングリストファイル内に順番に配置されます。
容量を節約しつつ高速な積集合およびユニオン操作を可能にするため、ポスティングリストは [roaring bitmaps](https://roaringbitmap.org/) として格納されます。
ポスティングリストが `posting_list_block_size` より大きい場合は、複数のブロックに分割され、ポスティングリストファイルに順番に格納されます。

**位置ファイル (.pos)**

任意。索引引数 `support_phrase_search = 1` の場合のみ作成されます。
一致した行内におけるトークンの位置を格納します。

**テキスト索引のマージ**

データパーツがマージされる際、テキスト索引を最初から再構築する必要はありません。代わりに、マージ処理内の別ステップで効率的にマージできます。
このステップでは、各入力パーツのテキスト索引にあるソート済みDictionaryを読み込み、新しい統合Dictionaryへ結合します。
また、ポスティングリスト内の行番号も、初期マージフェーズで作成された旧行番号から新行番号への対応関係を用いて、マージ後のデータパーツ内での新しい位置を反映するよう再計算されます。
このテキスト索引のマージ方法は、`_part_offset` カラムを持つ [projections](/ja/reference/statements/alter/projection#projection-indexes) のマージ方法に似ています。
ソースパーツ内で索引がマテリアライズされていない場合は、索引を構築して一時ファイルに書き込み、その後、ほかのパーツの索引およびほかの一時索引ファイルの索引とともにマージされます。

**デバッグ**

テーブル関数 [mergeTreeTextIndex](/ja/reference/functions/table-functions/mergeTreeTextIndex) を使用すると、テキスト索引の内部を調査できます。

<div id="hacker-news-dataset">
  ## 例: Hacker News データセット
</div>

テキストが多い大規模なデータセットに対して、テキスト索引によってどの程度パフォーマンスが向上するかを見てみましょう。
人気サイト Hacker News のコメント 2,870 万行を使用します。
以下は、テキスト索引がないテーブルです。

```sql theme={null}
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);
```

2,870万行のデータはS3上のParquetファイルにあります。これを`hackernews`テーブルに挿入してみましょう:

```sql theme={null}
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');
```

`ALTER TABLE` を使用して comment カラムにテキスト索引を追加し、その後マテリアライズします：

```sql theme={null}
-- Add the index
ALTER TABLE hackernews ADD INDEX comment_idx comment TYPE text(tokenizer = splitByNonAlpha);

-- Materialize the index for existing data
ALTER TABLE hackernews MATERIALIZE INDEX comment_idx SETTINGS mutations_sync = 2;
```

それでは、`hasToken`、`hasAnyTokens`、`hasAllTokens` 関数を使ってクエリを実行してみましょう。
以下の例では、通常の索引スキャンと direct read 最適化の間にある大きな性能差を示します。

<div id="using-hasToken">
  ### 1. `hasToken` を使用する
</div>

`hasToken` は、テキストに特定の単一トークンが含まれているかどうかを確認します。
大文字と小文字を区別するトークン 'ClickHouse' を検索します。

**direct read 無効 (標準スキャン) **
デフォルトでは、ClickHouse はスキップ索引を使ってグラニュールをフィルタリングし、その後、それらのグラニュールのカラムデータを読み取ります。
この動作は、direct read を無効にすることで再現できます。

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasToken(comment, 'ClickHouse')
SETTINGS query_plan_direct_read_from_text_index = 0;

┌─count()─┐
│     516 │
└─────────┘

1 row in set. Elapsed: 0.362 sec. Processed 24.90 million rows, 9.51 GB
```

**direct read 有効 (高速な索引読み取り) **
ここでは、direct read を有効にした状態 (デフォルト) で、同じクエリを実行します。

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasToken(comment, 'ClickHouse')
SETTINGS query_plan_direct_read_from_text_index = 1;

┌─count()─┐
│     516 │
└─────────┘

1 row in set. Elapsed: 0.008 sec. Processed 3.15 million rows, 3.15 MB
```

direct readクエリは、索引のみを参照することで、45倍以上高速で (0.362秒 vs 0.008秒) 、処理するデータ量も大幅に少なくなります (9.51 GB vs 3.15 MB) 。

<div id="using-hasAnyTokens">
  ### 2. `hasAnyTokens` の使用
</div>

`hasAnyTokens` は、テキストに指定したトークンのうち少なくとも 1 つが含まれているかどうかを判定します。
'love' または 'ClickHouse' を含むコメントを検索します。

**Direct read 無効 (標準スキャン) **

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasAnyTokens(comment, 'love ClickHouse')
SETTINGS query_plan_direct_read_from_text_index = 0;

┌─count()─┐
│  408426 │
└─────────┘

1 row in set. Elapsed: 1.329 sec. Processed 28.74 million rows, 9.72 GB
```

**Direct read が有効 (索引の高速読み取り) **

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasAnyTokens(comment, 'love ClickHouse')
SETTINGS query_plan_direct_read_from_text_index = 1;

┌─count()─┐
│  408426 │
└─────────┘

1 row in set. Elapsed: 0.015 sec. Processed 27.99 million rows, 27.99 MB
```

この一般的な "OR" 検索では、高速化の効果がさらに顕著です。
フルカラムスキャンを回避することで、クエリは約89倍高速になります (1.329秒 対 0.015秒) 。

<div id="using-hasAllTokens">
  ### 3. `hasAllTokens` の使用
</div>

`hasAllTokens` は、テキストに指定したすべてのトークンが含まれているかどうかを判定します。
'love' と 'ClickHouse' の両方を含むコメントを検索します。

**Direct read 無効時 (標準スキャン) **
Direct read が無効でも、標準のスキップ索引は引き続き有効です。
28.7M 行を 147.46K 行まで絞り込めますが、それでもカラムから 57.03 MB を読み取る必要があります。

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasAllTokens(comment, 'love ClickHouse')
SETTINGS query_plan_direct_read_from_text_index = 0;

┌─count()─┐
│      11 │
└─────────┘

1 row in set. Elapsed: 0.184 sec. Processed 147.46 thousand rows, 57.03 MB
```

**Direct read 有効 (高速な索引読み取り) **
Direct read では索引データを直接利用してクエリに応答するため、読み取り量は 147.46 KB のみです。

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasAllTokens(comment, 'love ClickHouse')
SETTINGS query_plan_direct_read_from_text_index = 1;

┌─count()─┐
│      11 │
└─────────┘

1 row in set. Elapsed: 0.007 sec. Processed 147.46 thousand rows, 147.46 KB
```

この"AND"検索では、direct read最適化は標準的なスキップ索引スキャンと比べて26倍以上高速です (0.184秒に対し0.007秒) 。

<div id="compound-search">
  ### 4. 複合検索: OR, AND, NOT, ...
</div>

direct read の最適化は、複合ブール式にも適用されます。
ここでは、'ClickHouse' OR 'clickhouse' の大文字と小文字を区別しない検索を行います。

**Direct read 無効 (標準スキャン) **

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasToken(comment, 'ClickHouse') OR hasToken(comment, 'clickhouse')
SETTINGS query_plan_direct_read_from_text_index = 0;

┌─count()─┐
│     769 │
└─────────┘

1 row in set. Elapsed: 0.450 sec. Processed 25.87 million rows, 9.58 GB
```

**Direct read が有効 (高速な索引読み取り) **

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasToken(comment, 'ClickHouse') OR hasToken(comment, 'clickhouse')
SETTINGS query_plan_direct_read_from_text_index = 1;

┌─count()─┐
│     769 │
└─────────┘

1 row in set. Elapsed: 0.013 sec. Processed 25.87 million rows, 51.73 MB
```

索引の結果を組み合わせることで、direct read クエリは 34 倍高速になり (0.450 秒に対して 0.013 秒) 、9.58 GB のカラムデータを読み取る必要がありません。
このケースでは、`hasAnyTokens(comment, ['ClickHouse', 'clickhouse'])` を使うほうが、より効率的で推奨される構文です。

<div id="related-content">
  ## 関連コンテンツ
</div>

* ブログ: [ClickHouse 全文検索の一般提供を発表](https://clickhouse.com/blog/full-text-search-ga-release)
* ブログ: [オブジェクトストレージ向けの高性能な全文検索の構築](https://clickhouse.com/blog/clickhouse-full-text-search-object-storage)
* ビデオ: [ClickHouse における全文検索の紹介](https://www.youtube.com/watch?v=9zPmf1a_heU)
* ビデオ: [舞台裏: ClickHouse のスケールと速度を支える全文検索](https://www.youtube.com/watch?v=8JbqE_ubfkU)
* プレゼンテーション: [ClickHouse 全文検索の内部: 高速・ネイティブ・列指向](https://github.com/ClickHouse/clickhouse-presentations/blob/master/2025-tumuchdata-munich/ClickHouse_%20full-text%20search%20-%2011.11.2025%20Munich%20Database%20Meetup.pdf)
* プレゼンテーション: [転置データベース索引: 必要性、概要、実装方法、FOSDEM 2026](https://presentations.clickhouse.com/2026-fosdem-inverted-index/Inverted_indexes_the_what_the_why_the_how.pdf)

**旧資料**

* ブログ: [ClickHouse における転置索引の紹介](https://clickhouse.com/blog/clickhouse-search-with-inverted-indices)
* ブログ: [ClickHouse 全文検索の内部: 高速・ネイティブ・列指向](https://clickhouse.com/blog/clickhouse-full-text-search)
* ビデオ: [全文索引: 設計と実験](https://www.youtube.com/watch?v=O_MnyUkrIq8)
