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

> 텍스트에서 검색어를 빠르게 찾습니다.

# 텍스트 인덱스를 사용한 전문 검색

export const PrivatePreviewBadge = () => {
  return <div className="privatePreviewBadge">
            <div className="privatePreviewIcon">
            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M5.33301 6.66667V4.66667V4.66667C5.33301 3.194 6.52701 2 7.99967 2V2C9.47234 2 10.6663 3.194 10.6663 4.66667V4.66667V6.66667" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
                <path d="M8.00033 9.33337V11.3334" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
                <path fillRule="evenodd" clipRule="evenodd" d="M11.333 14H4.66634C3.92967 14 3.33301 13.4033 3.33301 12.6666V7.99996C3.33301 7.26329 3.92967 6.66663 4.66634 6.66663H11.333C12.0697 6.66663 12.6663 7.26329 12.6663 7.99996V12.6666C12.6663 13.4033 12.0697 14 11.333 14Z" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
        </div>
            {'ClickHouse Cloud 비공개 프리뷰'}
        </div>;
};

<PrivatePreviewBadge />

ClickHouse의 텍스트 인덱스(["역색인"](https://en.wikipedia.org/wiki/Inverted_index)라고도 함)는 문자열 데이터에 대해 빠른 전문 검색 기능을 제공합니다.
이 인덱스는 컬럼의 각 토큰을 해당 토큰이 포함된 행에 매핑합니다.
토큰은 토큰화라고 하는 과정을 통해 생성됩니다.
예를 들어, ClickHouse는 기본적으로 영어 문장 "All cat like mice."를 \["All", "cat", "like", "mice"]로 토큰화합니다(문장 끝의 마침표는 무시됨).
로그 데이터와 같은 경우에는 더 고급 토크나이저도 사용할 수 있습니다.

<div id="creating-a-text-index">
  ## 텍스트 인덱스 생성
</div>

텍스트 인덱스를 생성하려면 먼저 해당 실험적 설정을 활성화하십시오:

```sql theme={null}
SET allow_experimental_full_text_index = true;
```

텍스트 인덱스는 다음 구문을 사용해 [String](/ko/reference/data-types/string), [FixedString](/ko/reference/data-types/fixedstring), [Array(String)](/ko/reference/data-types/array), [Array(FixedString)](/ko/reference/data-types/array), 그리고 [Map](/ko/reference/data-types/map) ([mapKeys](/ko/reference/functions/regular-functions/tuple-map-functions#mapkeys) 및 [mapValues](/ko/reference/functions/regular-functions/tuple-map-functions#mapvalues) 맵 함수를 통해) 컬럼에 정의할 수 있습니다:

```sql theme={null}
CREATE TABLE tab
(
    `key` UInt64,
    `str` String,
    INDEX text_idx(str) TYPE text(
                                -- 필수 매개변수:
                                tokenizer = splitByNonAlpha|splitByString(S)|ngrams(N)|array
                                -- 선택적 매개변수:
                                [, preprocessor = expression(str)]
                                -- 선택적 고급 매개변수:
                                [, dictionary_block_size = D]
                                [, dictionary_block_frontcoding_compression = B]
                                [, max_cardinality_for_embedded_postings = M]
                                [, bloom_filter_false_positive_rate = R]
                            ) [GRANULARITY 64]
)
ENGINE = MergeTree
ORDER BY key
```

**토크나이저 인수**. `tokenizer` 인수는 토크나이저를 지정합니다.

* `splitByNonAlpha`는 ASCII 영숫자가 아닌 문자를 기준으로 문자열을 분할합니다(함수 [splitByNonAlpha](/ko/reference/functions/regular-functions/splitting-merging-functions#splitByNonAlpha)도 참조).
* `splitByString(S)`는 사용자 정의 구분자 문자열 `S`를 기준으로 문자열을 분할합니다(함수 [splitByString](/ko/reference/functions/regular-functions/splitting-merging-functions#splitByString)도 참조).
  구분자는 선택적 매개변수로 지정할 수 있습니다. 예를 들어 `tokenizer = splitByString([', ', '; ', '\n', '\\'])`와 같습니다.
  각 문자열은 여러 문자로 이루어질 수 있습니다(예시의 `', '`).
  명시적으로 지정하지 않으면(예: `tokenizer = splitByString`) 기본 구분자 목록은 단일 공백 문자 `[' ']`입니다.
* `ngrams(N)`는 문자열을 같은 크기의 `N`-그램으로 분할합니다(함수 [ngrams](/ko/reference/functions/regular-functions/splitting-merging-functions#ngrams)도 참조).
  n-그램 길이는 2에서 8 사이의 선택적 정수 매개변수로 지정할 수 있습니다. 예를 들어 `tokenizer = ngrams(3)`와 같습니다.
  명시적으로 지정하지 않으면(예: `tokenizer = ngrams`) 기본 n-그램 크기는 3입니다.
* `array`는 토큰화를 수행하지 않습니다. 즉, 각 행의 값 자체가 하나의 토큰이 됩니다(함수 [array](/ko/reference/functions/regular-functions/array-functions#array)도 참조).
* `sparseGrams(min_length, max_length, min_cutoff_length)` — [sparseGrams](/ko/reference/functions/regular-functions/string-functions#sparseGrams) 함수와 동일한 알고리즘을 사용해 문자열을 `min_length` 길이의 모든 n-그램과 `max_length`까지의 더 긴 일부 n-그램으로 분할합니다. 여기서 `max_length`는 포함됩니다. `min_cutoff_length`를 지정하면 길이가 `min_cutoff_length` 이상인 N-그램만 인덱스에 저장됩니다. 고정 길이 N-그램만 생성하는 `ngrams(N)`와 달리, `sparseGrams`는 지정된 범위 내에서 가변 길이 N-그램 집합을 생성하므로 텍스트 문맥을 더 유연하게 표현할 수 있습니다. 예를 들어 `tokenizer = sparseGrams(3, 5, 4)`는 입력 문자열에서 3-, 4-, 5-그램을 생성하고, 이 중 4-그램과 5-그램만 인덱스에 저장합니다.

<Note>
  `splitByString` 토크나이저는 분할 구분자를 왼쪽에서 오른쪽 순서로 적용합니다.
  이로 인해 모호성이 생길 수 있습니다.
  예를 들어 구분자 문자열이 `['%21', '%']`이면 `%21abc`는 `['abc']`로 토큰화되지만, 구분자 문자열의 순서를 `['%', '%21']`로 바꾸면 `['21abc']`가 출력됩니다.
  대부분의 경우 더 긴 구분자가 먼저 일치하도록 하는 것이 좋습니다.
  일반적으로는 구분자 문자열을 길이가 긴 순서대로 전달하면 됩니다.
  구분자 문자열이 [prefix code](https://en.wikipedia.org/wiki/Prefix_code)를 이루는 경우에는 임의의 순서로 전달해도 됩니다.
</Note>

<Warning>
  현재로서는 중국어와 같은 비서구권 언어 텍스트에 텍스트 인덱스를 구축하는 것을 권장하지 않습니다.
  현재 지원되는 토크나이저는 인덱스 크기를 매우 크게 만들고 쿼리 시간을 크게 늘릴 수 있습니다.
  앞으로는 이러한 경우를 더 잘 처리할 수 있도록 언어별 특화 토크나이저를 추가할 계획입니다.
</Warning>

토크나이저가 입력 문자열을 어떻게 분할하는지 테스트하려면 ClickHouse의 [tokens](/ko/reference/functions/regular-functions/splitting-merging-functions#tokens) 함수를 사용할 수 있습니다:

예시:

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

반환값

```result theme={null}
+-tokens--------------------------+
| ['abc','bc ','c d',' de','def'] |
+---------------------------------+
```

**전처리기 인수**. 선택적 인수 `preprocessor`는 토큰화 전에 입력 문자열을 변환하는 표현식입니다.

전처리기 인수의 일반적인 사용 사례는 다음과 같습니다.

1. 대소문자를 구분하지 않는 매칭을 위해 입력 문자열을 소문자(또는 대문자)로 변환합니다. 예: [lower](/ko/reference/functions/regular-functions/string-functions#lower), [lowerUTF8](/ko/reference/functions/regular-functions/string-functions#lowerUTF8). 아래 첫 번째 예시를 참조하십시오.
2. UTF-8 정규화를 수행합니다. 예: [normalizeUTF8NFC](/ko/reference/functions/regular-functions/string-functions#normalizeUTF8NFC), [normalizeUTF8NFD](/ko/reference/functions/regular-functions/string-functions#normalizeUTF8NFD), [normalizeUTF8NFKC](/ko/reference/functions/regular-functions/string-functions#normalizeUTF8NFKC), [normalizeUTF8NFKD](/ko/reference/functions/regular-functions/string-functions#normalizeUTF8NFKD), [toValidUTF8](/ko/reference/functions/regular-functions/string-functions#toValidUTF8).
3. 불필요한 문자 또는 부분 문자열을 제거하거나 변환합니다. 예: [extractTextFromHTML](/ko/reference/functions/regular-functions/string-functions#extractTextFromHTML), [substring](/ko/reference/functions/regular-functions/string-functions#substring), [idnaEncode](/ko/reference/functions/regular-functions/string-functions#idnaEncode).

전처리기 표현식은 [String](/ko/reference/data-types/string) 또는 [FixedString](/ko/reference/data-types/fixedstring) 타입의 입력값을 동일한 타입의 값으로 변환해야 합니다.

예시:

* `INDEX idx(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(col))`
* `INDEX idx(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = substringIndex(col, '\n', 1))`
* `INDEX idx(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(extractTextFromHTML(col))`

또한 전처리기 표현식은 텍스트 인덱스가 정의된 컬럼만 참조해야 합니다.
비결정적 함수를 사용하는 것은 허용되지 않습니다.

함수 [hasToken](/ko/reference/functions/regular-functions/string-search-functions#hasToken), [hasAllTokens](/ko/reference/functions/regular-functions/string-search-functions#hasAllTokens), [hasAnyTokens](/ko/reference/functions/regular-functions/string-search-functions#hasAnyTokens)는 검색어를 토큰화하기 전에 먼저 전처리기를 사용해 검색어를 변환합니다.

예를 들면 다음과 같습니다.

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

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

다음과 같습니다:

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

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

**기타 인수**. ClickHouse의 텍스트 인덱스는 [보조 인덱스](/ko/reference/engines/table-engines/mergetree-family/mergetree#skip-index-types)로 구현됩니다.
하지만 다른 스키핑 인덱스와 달리 텍스트 인덱스의 기본 인덱스 GRANULARITY는 64입니다.
이 값은 경험적으로 선택되었으며, 대부분의 사용 사례에서 속도와 인덱스 크기 사이에 적절한 균형을 제공합니다.
고급 사용자는 다른 인덱스 세분화 수준을 지정할 수 있지만, 이는 권장하지 않습니다.

<AccordionGroup>
  <Accordion title="선택적 고급 매개변수">
    다음 고급 매개변수의 기본값은 거의 모든 상황에서 잘 작동합니다.
    이를 변경하는 것은 권장하지 않습니다.

    선택적 매개변수 `dictionary_block_size` (기본값: 128)는 딕셔너리 블록의 크기를 행 수 기준으로 지정합니다.

    선택적 매개변수 `dictionary_block_frontcoding_compression` (기본값: 1)은 딕셔너리 블록이 압축 방식으로 프론트 코딩을 사용하는지 여부를 지정합니다.

    선택적 매개변수 `max_cardinality_for_embedded_postings` (기본값: 16)는 포스팅 리스트를 딕셔너리 블록에 내장할지 결정하는 카디널리티 임계값을 지정합니다.

    선택적 매개변수 `bloom_filter_false_positive_rate` (기본값: 0.1)는 딕셔너리 블룸 필터의 위양성 비율을 지정합니다.
  </Accordion>
</AccordionGroup>

테이블이 생성된 후에도 컬럼에 텍스트 인덱스를 추가하거나 제거할 수 있습니다:

```sql theme={null}
ALTER TABLE tab DROP INDEX text_idx;
ALTER TABLE tab ADD INDEX text_idx(s) TYPE text(tokenizer = splitByNonAlpha);
```

<div id="using-a-text-index">
  ## 텍스트 인덱스 사용
</div>

SELECT 쿼리에서 텍스트 인덱스를 사용하는 것은 간단합니다. 일반적인 문자열 검색 함수가 자동으로 인덱스를 활용하기 때문입니다.
인덱스가 없으면 아래 문자열 검색 함수는 느린 브루트 포스(전체 스캔) 방식으로 처리됩니다.

<div id="supported-functions">
  ### 지원되는 함수
</div>

텍스트 함수가 SELECT 쿼리의 `WHERE` 절에 사용되면 텍스트 인덱스를 사용할 수 있습니다:

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

<div id="and">
  #### `=` 및 `!=`
</div>

`=` ([equals](/ko/reference/functions/regular-functions/comparison-functions#equals)) 및 `!=` ([notEquals](/ko/reference/functions/regular-functions/comparison-functions#notEquals) )는 지정한 검색어와 전체가 일치하는 경우에 매칭됩니다.

예시:

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

텍스트 인덱스는 `=` 및 `!=`를 지원하지만, 동등/부등 검색은 `array` 토크나이저를 사용할 때만 유의미합니다(이 경우 인덱스는 각 행의 전체 값을 저장합니다).

<div id="in-and-not-in">
  #### `IN` and `NOT IN`
</div>

`IN` ([in](/ko/reference/functions/regular-functions/in-functions)) 및 `NOT IN` ([notIn](/ko/reference/functions/regular-functions/in-functions))은 함수 `equals` 및 `notEquals`와 비슷하지만, 검색어 전체와 일치하는 경우(`IN`) 또는 검색어와 전혀 일치하지 않는 경우(`NOT IN`)에 사용됩니다.

예시:

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

`=` 및 `!=`와 동일한 제약이 적용됩니다. 즉, `IN` 및 `NOT IN`은 `array` 토크나이저와 함께 사용할 때만 의미가 있습니다.

<div id="like-not-like-and-match">
  #### `LIKE`, `NOT LIKE` 및 `match`
</div>

<Note>
  현재 이러한 함수는 인덱스 토크나이저가 `splitByNonAlpha` 또는 `ngrams`인 경우에만 필터링에 텍스트 인덱스를 사용합니다.
</Note>

텍스트 인덱스와 함께 `LIKE` [like](/ko/reference/functions/regular-functions/string-search-functions#like), `NOT LIKE` ([notLike](/ko/reference/functions/regular-functions/string-search-functions#notLike)), 그리고 [match](/ko/reference/functions/regular-functions/string-search-functions#match) 함수를 사용하려면 ClickHouse가 검색어에서 완전한 토큰을 추출할 수 있어야 합니다.

예시:

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

예시의 `support`는 `support`, `supports`, `supporting` 등에 일치할 수 있습니다.
이러한 쿼리는 부분 문자열 쿌리이며, 텍스트 인덱스로는 속도를 높일 수 없습니다.

LIKE 쿼리에서 텍스트 인덱스를 활용하려면 LIKE 패턴을 다음과 같이 다시 작성해야 합니다:

```sql theme={null}
SELECT count() FROM tab WHERE comment LIKE ' support %'; -- 또는 `% support %`
```

`support`의 왼쪽과 오른쪽에 있는 공백은 해당 용어를 토큰으로 추출할 수 있게 합니다.

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

`LIKE`와 마찬가지로 [startsWith](/ko/reference/functions/regular-functions/string-functions#startsWith) 및 [endsWith](/ko/reference/functions/regular-functions/string-functions#endsWith) 함수도 검색어에서 완전한 토큰을 추출할 수 있는 경우에만 텍스트 인덱스를 사용할 수 있습니다.

예시:

```sql theme={null}
SELECT count() FROM tab 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 tab WHERE endsWith(comment, ' olap engine');
```

<div id="hastoken-and-hastokenornull">
  #### `hasToken` and `hasTokenOrNull`
</div>

[hasToken](/ko/reference/functions/regular-functions/string-search-functions#hasToken) 및 [hasTokenOrNull](/ko/reference/functions/regular-functions/string-search-functions#hasTokenOrNull) 함수는 지정된 단일 토큰과 일치하는지 확인합니다.

앞서 설명한 함수들과 달리, 이 함수들은 검색어를 토큰화하지 않습니다(입력이 단일 토큰이라고 가정합니다).

예시:

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

함수 `hasToken` 및 `hasTokenOrNull`은 `text` 인덱스와 함께 사용할 때 가장 높은 성능을 제공하는 함수입니다.

<div id="hasanytokens-and-hasalltokens">
  #### `hasAnyTokens` 및 `hasAllTokens`
</div>

함수 [hasAnyTokens](/ko/reference/functions/regular-functions/string-search-functions#hasAnyTokens)와 [hasAllTokens](/ko/reference/functions/regular-functions/string-search-functions#hasAllTokens)는 지정된 토큰 중 하나 이상 또는 전체와 일치하는지 확인합니다.

이 두 함수는 검색 토큰을 두 가지 형태로 받습니다. 하나는 인덱스 컬럼에 사용된 것과 동일한 토크나이저로 토큰화되는 문자열이고, 다른 하나는 검색 전에 추가 토큰화가 적용되지 않는, 이미 처리된 토큰의 배열입니다.
자세한 내용은 함수 문서를 참조하십시오.

예시:

```sql theme={null}
-- 문자열 인수로 전달된 검색 토큰
SELECT count() FROM tab WHERE hasAnyTokens(comment, 'clickhouse olap');
SELECT count() FROM tab WHERE hasAllTokens(comment, 'clickhouse olap');

-- Array(String)으로 전달된 검색 토큰
SELECT count() FROM tab WHERE hasAnyTokens(comment, ['clickhouse', 'olap']);
SELECT count() FROM tab WHERE hasAllTokens(comment, ['clickhouse', 'olap']);
```

<div id="has">
  #### `has`
</div>

배열 함수 [has](/ko/reference/functions/regular-functions/array-functions#has)는 문자열 배열에서 단일 토큰과 일치하는지 확인합니다.

예시:

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

<div id="mapcontains">
  #### `mapContains`
</div>

함수 [mapContains](/ko/reference/functions/regular-functions/tuple-map-functions#mapcontainskey)(별칭: `mapContainsKey`)은 맵의 키에 있는 단일 토큰과 일치하는지 확인합니다.

예시:

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

<div id="operator">
  #### `operator[]`
</div>

접근 [operator\[\]](/ko/reference/operators/index#access-operators) 연산자를 텍스트 인덱스와 함께 사용하면 키와 값을 필터링할 수 있습니다.

예시:

```sql theme={null}
SELECT count() FROM tab WHERE map['engine'] = 'clickhouse'; -- will use the text index if defined
```

다음 예시에서 텍스트 인덱스와 함께 `Array(T)` 및 `Map(K, V)`를 사용하는 예를 확인할 수 있습니다.

<div id="examples-for-the-text-index-array-and-map-support">
  ### 텍스트 인덱스의 `Array` 및 `Map` 지원 예시
</div>

<div id="indexing-arraystring">
  #### Array(String) 인덱싱
</div>

간단한 블로깅 플랫폼에서는 작성자가 콘텐츠를 분류하기 위해 게시물에 키워드를 지정합니다.
일반적으로 사용자는 키워드를 클릭하거나 주제를 검색해 관련 콘텐츠를 찾을 수 있습니다.

다음 테이블 정의를 살펴보겠습니다.

```sql theme={null}
CREATE TABLE posts (
    post_id UInt64,
    title String,
    content String,
    keywords Array(String) COMMENT 'Author-defined keywords'
)
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);
```

<Note>
  중요: 텍스트 인덱스를 추가한 후에는 기존 데이터에도 인덱스를 다시 빌드해야 합니다:

  ```sql theme={null}
  ALTER TABLE posts MATERIALIZE INDEX keywords_idx;
  ```
</Note>

<div id="indexing-map">
  #### 맵 인덱싱
</div>

로깅 시스템에서는 서버 요청 메타데이터를 key-value 쌍으로 저장하는 경우가 많습니다. 운영 팀은 디버깅, 보안 사고 대응, 모니터링을 위해 로그를 효율적으로 검색해야 합니다.

다음 로그 테이블을 살펴보겠습니다:

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

텍스트 인덱스가 없으면 [맵](/ko/reference/data-types/map) 데이터 검색 시 전체 테이블 스캔이 필요합니다:

1. rate limiting이 적용된 모든 로그를 찾습니다:

```sql theme={null}
SELECT count() FROM logs WHERE has(mapKeys(attributes), 'rate_limit'); -- 느린 전체 테이블 스캔
```

2. 특정 IP의 모든 logs를 찾습니다:

```sql theme={null}
SELECT count() FROM logs WHERE has(mapValues(attributes), '192.168.1.1'); -- 느린 전체 테이블 스캔
```

로그 양이 늘어날수록 이러한 쿼리는 느려집니다.

해결 방법은 [Map](/ko/reference/data-types/map) 키와 값에 텍스트 인덱스를 생성하는 것입니다.

필드 이름이나 속성 타입으로 로그를 찾아야 할 때는 [mapKeys](/ko/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);
```

속성의 실제 내용에서 검색해야 하는 경우 [mapValues](/ko/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);
```

<Note>
  중요: 텍스트 인덱스를 추가한 후에는 기존 데이터에 대해 인덱스를 다시 빌드해야 합니다:

  ```sql theme={null}
  ALTER TABLE posts MATERIALIZE INDEX attributes_keys_idx;
  ALTER TABLE posts MATERIALIZE INDEX attributes_vals_idx;
  ```
</Note>

1. 속도 제한이 적용된 모든 요청을 찾습니다:

```sql theme={null}
SELECT * FROM logs WHERE mapContainsKey(attributes, 'rate_limit'); -- 빠름
```

2. 특정 IP에서 생성된 모든 로그를 찾습니다:

```sql theme={null}
SELECT * FROM logs WHERE has(mapValues(attributes), '192.168.1.1'); -- 빠름
```

<div id="implementation">
  ## 구현
</div>

<div id="index-layout">
  ### 인덱스 레이아웃
</div>

각 텍스트 인덱스는 두 가지 (추상적인) 데이터 구조로 이루어집니다.

* 각 토큰을 포스팅 리스트에 매핑하는 딕셔너리
* 그리고 각각이 행 번호 집합을 나타내는 포스팅 리스트 집합입니다.

텍스트 인덱스는 스킵 인덱스이므로 이러한 데이터 구조는 논리적으로 각 인덱스 그래뉼마다 존재합니다.

인덱스를 생성하는 동안 파일 3개가 생성됩니다(파트별).

**딕셔너리 블록 파일 (.dct)**

인덱스 그래뉼 내 토큰은 정렬된 후, 각각 128개의 토큰을 담는 딕셔너리 블록에 저장됩니다(블록 크기는 매개변수 `dictionary_block_size`로 설정할 수 있습니다).
딕셔너리 블록 파일(.dct)은 하나의 파트에 있는 모든 인덱스 그래뉼의 모든 딕셔너리 블록으로 구성됩니다.

**인덱스 그래뉼 파일 (.idx)**

인덱스 그래뉼 파일에는 각 딕셔너리 블록마다 블록의 첫 번째 토큰, 딕셔너리 블록 파일 내 상대 오프셋, 그리고 블록의 모든 토큰에 대한 블룸 필터가 포함됩니다.
이 희소 인덱스 구조는 ClickHouse의 [희소 프라이머리 키 인덱스](/ko/guides/clickhouse/data-modelling/sparse-primary-indexes))와 유사합니다.
블룸 필터를 사용하면 검색 중인 토큰이 딕셔너리 블록에 없을 경우 해당 딕셔너리 블록을 미리 건너뛸 수 있습니다.

**포스팅 리스트 파일 (.pst)**

모든 토큰의 포스팅 리스트는 포스팅 리스트 파일에 순차적으로 배치됩니다.
공간을 절약하면서도 빠른 intersect 및 union 연산을 지원하기 위해 포스팅 리스트는 [roaring bitmaps](https://roaringbitmap.org/)로 저장됩니다.
포스팅 리스트의 카디널리티가 16보다 작으면(매개변수 `max_cardinality_for_embedded_postings`로 설정 가능) 딕셔너리에 내장됩니다.

<div id="direct-read">
  ### 직접 읽기
</div>

특정 타입의 텍스트 쿼리는 "직접 읽기"라는 최적화를 통해 속도를 크게 높일 수 있습니다.
구체적으로는 SELECT 쿼리가 텍스트 컬럼을 결과로 *선택하지 않는* 경우 이 최적화를 적용할 수 있습니다.

예시:

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

ClickHouse의 직접 읽기 최적화는 원본 텍스트 컬럼에 접근하지 않고 텍스트 인덱스(즉, 텍스트 인덱스 조회)만 사용해 쿼리를 처리합니다.
텍스트 인덱스 조회는 읽는 데이터 양이 비교적 적기 때문에 ClickHouse의 일반적인 스킵 인덱스보다 훨씬 빠릅니다(일반적인 스킵 인덱스는 스킵 인덱스 조회를 수행한 다음, 남은 그래뉼을 로드하고 필터링합니다).

직접 읽기는 두 가지 설정으로 제어됩니다:

* [query\_plan\_direct\_read\_from\_text\_index](/ko/reference/settings/session-settings#query_plan_direct_read_from_text_index) 설정(기본값: 1): 직접 읽기를 기본적으로 활성화할지 지정합니다.
* [use\_skip\_indexes\_on\_data\_read](/ko/reference/settings/session-settings#use_skip_indexes_on_data_read) 설정(기본값: 1): 직접 읽기를 사용하기 위한 또 다른 필수 조건입니다. ClickHouse 데이터베이스에서 [compatibility](/ko/reference/settings/session-settings#compatibility) \< 25.10인 경우 `use_skip_indexes_on_data_read`가 비활성화되므로, compatibility 설정 값을 높이거나 `SET use_skip_indexes_on_data_read = 1`을 명시적으로 설정해야 합니다.

또한 직접 읽기를 사용하려면 텍스트 인덱스가 완전히 구체화되어 있어야 합니다(이를 위해 `ALTER TABLE ... MATERIALIZE INDEX`를 사용하십시오).

**지원되는 함수**
직접 읽기 최적화는 `hasToken`, `hasAllTokens`, `hasAnyTokens` 함수를 지원합니다.
이 함수들은 AND, OR, NOT 연산자로 조합할 수도 있습니다.
WHERE 절에는 추가로 텍스트 검색 함수가 아닌 필터(텍스트 컬럼 또는 다른 컬럼에 대한 필터)도 포함될 수 있습니다. 이 경우에도 직접 읽기 최적화는 사용되지만 효과는 다소 떨어집니다(지원되는 텍스트 검색 함수에만 적용되기 때문입니다).

쿼리가 직접 읽기를 사용하는지 확인하려면 `EXPLAIN PLAN actions = 1`로 쿼리를 실행하십시오.
예를 들어, 직접 읽기가 비활성화된 쿼리는 다음과 같습니다.

```sql theme={null}
EXPLAIN PLAN actions = 1
SELECT count()
FROM tab
WHERE hasToken(col, 'some_token')
SETTINGS query_plan_direct_read_from_text_index = 0;
```

반환값

```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 tab
WHERE hasToken(col, 'some_token')
SETTINGS query_plan_direct_read_from_text_index = 1;
```

반환값

```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
[...]
```

두 번째 EXPLAIN PLAN 출력에는 가상 컬럼 `__text_index_<index_name>_<function_name>_<id>`이 포함되어 있습니다.
이 컬럼이 있으면 직접 읽기가 사용된 것입니다.

<div id="example-hackernews-dataset">
  ## 예시: Hacker News 데이터셋
</div>

텍스트가 많은 대규모 데이터셋에서 텍스트 인덱스가 성능을 얼마나 개선하는지 살펴보겠습니다.
인기 웹사이트인 Hacker News의 댓글 28.7M행을 사용합니다.
다음은 텍스트 인덱스가 없는 테이블입니다:

```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);
```

S3의 Parquet 파일에 28.7M개의 행이 있습니다 - 이제 이를 `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}
-- 인덱스 추가
ALTER TABLE hackernews ADD INDEX comment_idx(comment) TYPE text(tokenizer = splitByNonAlpha);

-- 기존 데이터에 대해 인덱스 구체화
ALTER TABLE hackernews MATERIALIZE INDEX comment_idx SETTINGS mutations_sync = 2;
```

이제 `hasToken`, `hasAnyTokens`, `hasAllTokens` 함수를 사용해 쿼리를 실행해 보겠습니다.
다음 예시에서는 표준 인덱스 스캔과 직접 읽기 최적화 간의 큰 성능 차이를 확인할 수 있습니다.

<div id="1-using-hastoken">
  ### 1. `hasToken` 사용하기
</div>

`hasToken`은 텍스트에 특정한 단일 토큰이 포함되어 있는지 확인합니다.
대소문자를 구분하는 토큰 'ClickHouse'를 검색합니다.

**직접 읽기 비활성화 (표준 스캔)**
기본적으로 ClickHouse는 스킵 인덱스를 사용해 그래뉼을 필터링한 다음, 해당 그래뉼의 컬럼 데이터를 읽습니다.
직접 읽기를 비활성화하면 이 동작을 시뮬레이션할 수 있습니다.

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

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

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

**직접 읽기 활성화(빠른 인덱스 읽기)**
이제 직접 읽기를 활성화한 상태(기본값)에서 동일한 쿼리를 실행합니다.

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

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

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

직접 읽기 쿼리는 인덱스만 읽기 때문에 45배 이상 빠르고(0.362초 대비 0.008초), 처리하는 데이터 양도 훨씬 적습니다(9.51 GB 대비 3.15 MB).

<div id="2-using-hasanytokens">
  ### 2. `hasAnyTokens` 사용하기
</div>

`hasAnyTokens`는 텍스트에 지정된 토큰(token) 중 하나 이상이 포함되어 있는지 확인합니다.
'love' 또는 'ClickHouse'가 포함된 댓글을 검색하겠습니다.

**직접 읽기 비활성화(표준 스캔)**

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

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

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

**직접 읽기 활성화(빠른 인덱스 읽기)**

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

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

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

이 일반적인 "OR" 검색에서는 속도 향상이 훨씬 더 극적입니다.
전체 컬럼 스캔을 피하면 쿼리 속도가 거의 89배 빨라집니다(1.329s 대 0.015s).

<div id="3-using-hasalltokens">
  ### 3. `hasAllTokens` 사용하기
</div>

`hasAllTokens`는 텍스트에 지정된 모든 token이 포함되어 있는지 확인합니다.
'love'와 'ClickHouse'가 모두 포함된 댓글을 검색하겠습니다.

**직접 읽기 비활성화됨 (표준 스캔)**
직접 읽기가 비활성화된 경우에도 표준 스킵 인덱스는 여전히 효과적입니다.
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, use_skip_indexes_on_data_read = 0;

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

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

**직접 읽기 활성화(빠른 인덱스 읽기)**
직접 읽기는 인덱스 데이터만 사용해 쿼리에 응답하므로, 147.46 KB만 읽습니다.

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

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

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

이 "AND" 검색에서는 직접 읽기 최적화가 일반적인 스킵 인덱스 스캔보다 26배 이상 빠릅니다(0.184초 대 0.007초).

<div id="4-compound-search-or-and-not">
  ### 4. 복합 검색: OR, AND, NOT, ...
</div>

직접 읽기 최적화는 복합 불리언 표현식에도 적용됩니다.
여기서는 'ClickHouse' OR 'clickhouse'에 대해 대소문자를 구분하지 않는 검색을 수행해 보겠습니다.

**직접 읽기 비활성화 (표준 스캔)**

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

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

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

**직접 읽기 활성화(빠른 인덱스 읽기)**

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

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

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

인덱스 결과를 조합하면 직접 읽기 쿼리는 34배 더 빨라지며(0.450초 대비 0.013초), 9.58 GB의 컬럼 데이터를 읽지 않아도 됩니다.
이 경우에는 `hasAnyTokens(comment, ['ClickHouse', 'clickhouse'])` 구문을 사용하는 것이 더 효율적이며 권장됩니다.

<div id="tuning-the-text-index">
  ## 텍스트 인덱스 튜닝
</div>

현재 I/O를 줄이기 위해 텍스트 인덱스의 역직렬화된 딕셔너리 블록, 헤더, 포스팅 리스트에 대한 캐시가 제공됩니다.

각각 [use\_text\_index\_dictionary\_cache](/ko/reference/settings/session-settings#use_text_index_dictionary_cache), [use\_text\_index\_header\_cache](/ko/reference/settings/session-settings#use_text_index_header_cache), [use\_text\_index\_postings\_cache](/ko/reference/settings/session-settings#use_text_index_postings_cache) 설정을 통해 활성화할 수 있습니다. 기본적으로는 비활성화되어 있습니다.

캐시를 구성하려면 다음 서버 설정을 참고하십시오.

<div id="server-settings">
  ### 서버 설정
</div>

<div id="dictionary-blocks-cache-settings">
  #### 딕셔너리 블록 캐시 설정
</div>

| 설정                                                                                                                                                   | 설명                                               | 기본값          |
| ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ------------ |
| [text\_index\_dictionary\_block\_cache\_policy](/ko/reference/settings/server-settings/settings#text_index_dictionary_block_cache_policy)            | 텍스트 인덱스 딕셔너리 블록 캐시 정책 이름입니다.                     | `SLRU`       |
| [text\_index\_dictionary\_block\_cache\_size](/ko/reference/settings/server-settings/settings#text_index_dictionary_block_cache_size)                | 바이트 단위의 최대 캐시 크기입니다.                             | `1073741824` |
| [text\_index\_dictionary\_block\_cache\_max\_entries](/ko/reference/settings/server-settings/settings#text_index_dictionary_block_cache_max_entries) | 캐시에 저장할 수 있는 역직렬화된 딕셔너리 블록의 최대 개수입니다.            | `1'000'000`  |
| [text\_index\_dictionary\_block\_cache\_size\_ratio](/ko/reference/settings/server-settings/settings#text_index_dictionary_block_cache_size_ratio)   | 텍스트 인덱스 딕셔너리 블록 캐시에서 전체 캐시 크기 대비 보호 큐의 크기 비율입니다. | `0.5`        |

<div id="header-cache-settings">
  #### 헤더 캐시 설정
</div>

| Setting                                                                                                                         | Description                                 | Default      |
| ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ------------ |
| [text\_index\_header\_cache\_policy](/ko/reference/settings/server-settings/settings#text_index_header_cache_policy)            | 텍스트 인덱스 헤더 캐시 정책의 이름입니다.                    | `SLRU`       |
| [text\_index\_header\_cache\_size](/ko/reference/settings/server-settings/settings#text_index_header_cache_size)                | 바이트 단위의 최대 캐시 크기입니다.                        | `1073741824` |
| [text\_index\_header\_cache\_max\_entries](/ko/reference/settings/server-settings/settings#text_index_header_cache_max_entries) | 캐시에 저장할 수 있는 역직렬화된 헤더의 최대 개수입니다.            | `100'000`    |
| [text\_index\_header\_cache\_size\_ratio](/ko/reference/settings/server-settings/settings#text_index_header_cache_size_ratio)   | 텍스트 인덱스 헤더 캐시에서 전체 캐시 크기 대비 보호 큐 크기의 비율입니다. | `0.5`        |

<div id="posting-lists-cache-settings">
  #### 포스팅 리스트 캐시 설정
</div>

| Setting                                                                                                                             | Description                                      | Default      |
| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ------------ |
| [text\_index\_postings\_cache\_policy](/ko/reference/settings/server-settings/settings#text_index_postings_cache_policy)            | 텍스트 인덱스 포스팅 리스트 캐시 정책의 이름입니다.                    | `SLRU`       |
| [text\_index\_postings\_cache\_size](/ko/reference/settings/server-settings/settings#text_index_postings_cache_size)                | 캐시의 최대 크기(바이트)입니다.                               | `2147483648` |
| [text\_index\_postings\_cache\_max\_entries](/ko/reference/settings/server-settings/settings#text_index_postings_cache_max_entries) | 캐시에 저장할 역직렬화된 포스팅의 최대 개수입니다.                     | `1'000'000`  |
| [text\_index\_postings\_cache\_size\_ratio](/ko/reference/settings/server-settings/settings#text_index_postings_cache_size_ratio)   | 텍스트 인덱스 포스팅 리스트 캐시에서 전체 캐시 크기 대비 보호 큐 크기의 비율입니다. | `0.5`        |

<div id="related-content">
  ## 관련 콘텐츠
</div>

* 블로그: [ClickHouse의 역색인 소개](https://clickhouse.com/blog/clickhouse-search-with-inverted-indices)
* 블로그: [ClickHouse 전문 검색의 내부: 빠르고 네이티브하며 열 지향적입니다](https://clickhouse.com/blog/clickhouse-full-text-search)
* Video: [전문 검색 인덱스: 설계 및 실험](https://www.youtube.com/watch?v=O_MnyUkrIq8)
