Skip to main content
텍스트 인덱스(역색인이라고도 함)를 사용하면 텍스트 데이터에서 전문 검색을 빠르게 수행할 수 있습니다. 텍스트 인덱스는 각 토큰을 포함하는 행 번호에 대한 매핑을 저장합니다. 토큰은 토큰화라고 하는 과정을 통해 생성됩니다. 예를 들어, ClickHouse의 기본 토크나이저는 영어 문장 “The cat likes mice.”를 [“The”, “cat”, “likes”, “mice”] 토큰으로 변환합니다. 예시로, 단일 컬럼과 3개의 행으로 이루어진 테이블이 있다고 가정합니다
해당 토큰은 다음과 같습니다:
보통은 대소문자를 구분하지 않고 검색하므로, 토큰을 소문자로 변환합니다:
또한 거의 모든 행에 등장하므로 “I”, “the”, “and”와 같은 불용어도 제거합니다:
그러면 텍스트 인덱스에는 (개념적으로) 다음과 같은 정보가 포함됩니다:
검색 토큰이 주어지면, 이 인덱스 구조를 사용해 일치하는 모든 행을 빠르게 찾을 수 있습니다.

텍스트 인덱스 생성

텍스트 인덱스는 ClickHouse 버전 26.2 이상에서 일반 제공(GA)됩니다. 이 버전에서는 텍스트 인덱스를 사용하기 위해 별도의 설정을 구성할 필요가 없습니다. 프로덕션 환경에서 사용할 경우 ClickHouse 버전 >= 26.2를 사용할 것을 강력히 권장합니다.
텍스트 인덱스는 compatibility 설정과 관계없이 ClickHouse 버전 >= 26.2 이상에서 사용할 수 있습니다.
텍스트 인덱스를 생성하려면 다음 구문을 사용하십시오:
Query
텍스트 인덱스는 다음 타입의 컬럼에 정의할 수 있습니다: Nullable(T)LowCardinality() 타입의 컬럼도 지원되며, Array(Nullable(String or FixedString))도 포함됩니다. 또는 기존 테이블에 텍스트 인덱스를 추가하려면:
Query
기존 테이블에 인덱스를 추가하는 경우, 기존 테이블 파트에 대해서도 인덱스를 물리화하는 것을 권장합니다(그렇지 않으면 인덱스가 없는 파트를 검색할 때 느린 브루트 포스(전체 스캔) 방식으로 폴백됩니다).
Query
텍스트 인덱스를 삭제하려면 다음을 실행하세요
Query
토크나이저 인수(필수). tokenizer 인수는 사용할 토크나이저를 지정합니다:
  • splitByNonAlpha는 영숫자가 아닌 ASCII 문자를 기준으로 문자열을 분할합니다(함수 splitByNonAlpha 참조).
  • splitByString(S)는 사용자가 정의한 특정 구분자 문자열 S를 기준으로 문자열을 분할합니다(함수 splitByString 참조). 구분자는 선택적 매개변수로 지정할 수 있습니다. 예: tokenizer = splitByString([', ', '; ', '\n', '\\']). 각 문자열은 여러 문자로 구성될 수 있습니다(예시의 ', '). 명시적으로 지정하지 않으면(예: tokenizer = splitByString) 기본 구분자 목록은 공백 1개인 [' ']입니다.
  • asciiCJK는 유니코드 단어 경계 규칙을 사용해 문자열을 토큰으로 분할합니다(Unicode Text Segmentation (UAX #29)와 유사). ASCII 영숫자와 밑줄은 연결 문자와 함께 토큰을 이룹니다(문자의 경우 ASCII :, 같은 유형의 문자에는 .'). CJK 문자를 포함한 비ASCII 유니코드 문자는 한 글자 토큰이 됩니다.
  • ngrams(N)는 문자열을 같은 길이의 N-그램으로 분할합니다(함수 ngrams 참조). ngram 길이는 1부터 8 사이의 선택적 정수 매개변수로 지정할 수 있습니다. 예: tokenizer = ngrams(3). 명시적으로 지정하지 않으면(예: tokenizer = ngrams) 기본 ngram 크기는 3입니다.
  • sparseGrams(min_length, max_length, min_cutoff_length)는 문자열을 최소 min_length, 최대 max_length(포함) 길이의 가변 길이 n-그램으로 분할합니다(함수 sparseGrams 참조). 명시적으로 지정하지 않으면 min_lengthmax_length의 기본값은 각각 3과 100입니다. 매개변수 min_cutoff_length가 제공되면 길이가 min_cutoff_length 이상인 n-그램만 반환됩니다. ngrams(N)와 비교하면 sparseGrams 토크나이저는 가변 길이 N-그램을 생성하므로 원본 텍스트를 더 유연하게 표현할 수 있습니다. 예를 들어 tokenizer = sparseGrams(3, 5, 4)는 내부적으로 입력 문자열에서 3-, 4-, 5-그램을 생성하지만, 실제로는 4-그램과 5-그램만 반환합니다.
  • array는 토큰화를 수행하지 않습니다. 즉, 각 행 값 자체가 하나의 토큰입니다(함수 array 참조).
사용 가능한 모든 토크나이저는 system.tokenizers에 나열되어 있습니다.
splitByString 토크나이저는 분할 구분자를 왼쪽에서 오른쪽 순서로 적용합니다. 이로 인해 모호성이 발생할 수 있습니다. 예를 들어 구분자 문자열이 ['%21', '%']이면 %21abc['abc']로 토큰화되지만, 두 구분자 문자열의 순서를 ['%', '%21']로 바꾸면 ['21abc']가 출력됩니다. 대부분의 경우 더 긴 구분자가 먼저 일치하도록 하는 것이 좋습니다. 일반적으로는 구분자 문자열을 길이 내림차순으로 전달하면 됩니다. 구분자 문자열이 prefix code를 이룬다면 임의의 순서로 전달할 수 있습니다.
토크나이저가 입력 문자열을 어떻게 분할하는지 확인하려면 tokenstokensForLikePattern 함수를 사용할 수 있습니다: 예시:
Query
Response
비 ASCII 입력 다루기. 텍스트 인덱스는 모든 언어와 문자 집합의 텍스트 데이터에 대해 생성할 수 있습니다. 비 ASCII 텍스트에는 CJK 문자를 포함한 유니코드 단어 경계를 올바르게 처리하는 asciiCJK 토크나이저를 권장합니다. 전처리기 인수(선택 사항). 전처리기는 토큰화 전에 입력 문자열에 적용되는 표현식을 의미합니다. 전처리기 인수의 일반적인 사용 사례는 다음과 같습니다.
  1. 대소문자를 구분하지 않고 일치하도록 소문자/대문자 변환 또는 case folding을 적용합니다. 예: lower, lowerUTF8, caseFoldUTF8.
  2. UTF-8 정규화를 적용합니다. 예: normalizeUTF8NFC, normalizeUTF8NFD, normalizeUTF8NFKC, normalizeUTF8NFKD, normalizeUTF8NFKCCasefold, toValidUTF8.
  3. 악센트처럼 불필요한 문자나 부분 문자열을 제거하거나 변환합니다. 예: extractTextFromHTML, substring, idnaEncode, translate, removeDiacriticsUTF8.
전처리기 표현식은 String 또는 FixedString 타입의 입력값을 같은 타입의 값으로 변환해야 합니다. 텍스트 인덱스가 Nullable(T) 또는 LowCardinality(T) 타입의 컬럼에 build된 경우, 전처리기 표현식은 nullable 또는 low-cardinality 값을 받아들일 수 있어야 합니다(즉, 예외를 발생시키지 않아야 합니다). 예시:
  • INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(col))
  • INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = substringIndex(col, '\n', 1))
  • INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(extractTextFromHTML(col)))
  • INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = removeDiacriticsUTF8(caseFoldUTF8(col)))
또한 전처리기 표현식은 텍스트 인덱스가 정의된 컬럼 또는 표현식만 참조해야 합니다. 예시:
  • INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = upper(lower(col)))
  • INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = concat(lower(col), lower(col)))
  • 허용되지 않음: INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = concat(col, col))
비결정적 함수는 사용할 수 없습니다.
전처리기는 원칙적으로 인덱스 컬럼 또는 표현식을 전처리기 표현식으로 감싸는 것과 같습니다. 예를 들어 INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(col))lower 전처리기는 INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha')로 구현할 수 있습니다. 후자의 형식은 에뮬레이션된 전처리기가 WHERE 절의 filter condition과 일치할 때만 적용된다는 단점이 있습니다. 예를 들어 WHERE hasAllTokens(lower(col), [...])는 일치하지만 WHERE hasAllTokens(col, [...])는 일치하지 않습니다. 따라서 최적의 사용자 경험을 위해 전처리기 표현식을 사용하는 것을 권장합니다.
함수 hasToken, hasAllTokens, hasAnyTokens, hasPhrase는 검색어를 토큰화하기 전에 먼저 전처리기를 사용해 검색어를 변환합니다. 전처리기는 텍스트 인덱스 경로에만 적용되므로, 이러한 함수의 결과는 텍스트 인덱스를 사용하는 쿼리와 사용하지 않는 쿼리 간에 다를 수 있습니다(예: SETTINGS use_skip_indexes = 0). 예를 들어,
Query
와 같습니다:
Query
이 경우 전처리기 표현식은 배열의 각 요소를 개별적으로 변환합니다. 예시:
Query
타입 컬럼에 텍스트 인덱스용 전처리기를 정의하려면, 인덱스를 맵 키를 기준으로 빌드할지 아니면 값을 기준으로 빌드할지 결정해야 합니다. 예시:
Query
후처리기 인수(선택 사항). 후처리기는 토큰화 후 각 출력 토큰에 적용되는 표현식을 의미합니다. 전처리기가 토크나이저가 전체 입력 문자열을 토큰으로 분할하기 전에 이를 변환하는 것과 달리, 후처리기는 토큰 자체에 하나씩 적용됩니다. 따라서 본질적으로 토큰 수준에서 수행되는 변환에 적합합니다. 후처리기 인수의 일반적인 사용 사례는 다음과 같습니다:
  1. 불용어(매우 자주 나타나는 토큰) 필터링. “the”, “a”, “is”와 같은 매우 흔한 토큰은 검색 관련성이 거의 없고 인덱스를 불필요하게 키웁니다. 후처리기를 사용해 이를 빈 토큰으로 변환하여 버릴 수 있습니다. 빈 토큰은 무시되며, 즉 인덱스에 추가되지 않습니다. 예시: if(str IN ('the', 'a', 'an', 'of', 'in', 'is', 'it'), '', str)
  2. 타임스탬프 제거. 로그 줄은 종종 2024-01-15T10:23:45와 같은 구조화된 타임스탬프로 시작하거나 이를 포함합니다. 타임스탬프 토큰을 인덱싱하면 검색 관련성이 없는 문자열로 인해 인덱스가 불필요하게 커집니다. 타임스탬프를 무시하는 상호 보완적인 접근 방식은 두 가지입니다:
    • 후처리기 접근 방식: splitByString 토크나이저(공백 기준 분할)를 사용해 전체 타임스탬프가 하나의 토큰이 되도록 한 다음, 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 함수를 제공합니다. 예시: stem(str, 'en')
  4. 대소문자 정규화. 대소문자를 구분하지 않는 일치를 가능하게 하도록 토큰을 소문자 또는 대문자로 변환합니다. 예: lower, lowerUTF8. 소문자화 및 대문자화에는 후처리기보다 전처리기를 권장합니다.
후처리기 표현식은 String 타입의 토큰을 동일한 타입의 토큰으로 변환합니다. 또한, 후처리기 표현식은 텍스트 인덱스가 정의된 컬럼 또는 표현식만 참조해야 합니다. 컬럼의 타입이 Array(String)인 경우에도 후처리기는 개별 토큰을 일반 String 값으로 처리합니다. 비결정적 함수는 사용할 수 없습니다. 후처리기는 인덱스 빌드 중 생성된 각 토큰에 적용됩니다(array 토크나이저의 경우 각 배열 요소가 하나의 토큰입니다). 쿼리 시점에는 함수에 따라 동작이 달라집니다.
  • hasToken, hasAllTokens, hasAnyTokens, hasPhrase(지원되는 모든 토크나이저 사용 시)의 경우: 후처리기는 검색 대상(haystack)의 토큰과 검색 needle 모두에 적용되므로 완전히 정규화된 매칭(예: 대소문자를 구분하지 않는 검색)이 가능합니다. hasPhrase의 경우 후처리된 토큰은 빈틈없이 배치되므로, 후처리기가 제거한 토큰이 있더라도 위치상 공백이 생기지 않아 그 토큰을 건너뛰어도 구문이 계속 일치합니다. 예를 들어 the를 제거하는 불용어 후처리기를 사용할 때 hasPhrase(col, 'see cat')는 문서 see the cat과 일치합니다.
  • 그 밖의 모든 함수(=, IN, has, hasAny, hasAll, mapContains*)의 경우: 인덱스 힌트 조회에는 검색 needle에만 후처리기가 적용되며, 행 수준 프레디케이트는 계속 원래 컬럼 값과 비교합니다.
예시:
  • 후처리기 표현식을 사용해 불용어를 제거합니다:
  • 후처리기 표현식을 사용하여 타임스탬프를 제거합니다:
  • 전처리기 표현식을 사용해 타임스탬프를 제거합니다:
  • 전처리기와 후처리기를 결합한 표현식을 사용해 타임스탬프를 제거합니다:
  • 후처리기 표현식을 사용해 토큰을 어간화합니다:
함수 지원. 텍스트 인덱스를 참조하는 프레디케이트에서는 인덱스 조회가 인덱스 빌드 시 저장된 것과 동일한 토큰을 사용하도록, granule 수준 검사 전에 검색 값에 전처리기와 후처리기를 적용합니다. 대부분의 함수(=, IN, startsWith, endsWith, LIKE, mapContains*)에서는 텍스트 인덱스가 관련 없는 데이터 블록을 건너뛰는 데만 사용되며, ClickHouse는 남은 각 행에 대해 원본 컬럼 데이터를 기준으로 원본 프레디케이트를 사용해 계속 검증합니다. 토큰 검색 함수(hasToken, hasAllTokens, hasAnyTokens)에서는 텍스트 인덱스가 기본 평가 경로입니다. ClickHouse는 인덱스 빌드 시 적용된 것과 동일한 전처리기, 토크나이저, 후처리기를 통해 needle을 정규화하고, 이 정규화된 형태를 인덱스가 있는 table part와 없는 table part 모두에 사용합니다. 후처리기가 있으면 haystack 토큰도 쿼리 시점에 정규화되므로(array뿐 아니라 모든 토크나이저에 적용됨), 비교 양쪽이 일관되게 변환되며 결과도 인덱스를 직접 읽는지(설정 query_plan_direct_read_from_text_index) 또는 특정 파트에 구체화된 인덱스가 있는지와 관계없이 동일합니다. 예를 들어 lower 후처리기를 사용하면 hasAllTokens(col, ['FOO'])에서 대소문자를 구분하지 않는 매칭을 사용할 수 있습니다. support_phrase_search가 없으면 hasPhrase는 인덱스를 힌트로만 사용하고, 남은 각 행은 원본 프레디케이트로 검증합니다. 또한 후처리기가 있으면 구문과 haystack 토큰 모두를 같은 방식으로 정규화하므로 결과는 읽기 경로와 무관하며, 후처리기가 제거한 토큰 때문에 구문 인접성이 깨지지 않습니다. support_phrase_search = 1이면 hasPhrase는 정확한 직접 읽기를 사용합니다(후처리기가 있으면 계속 적용됨). 후처리기가 빈 문자열로 매핑하는 검색 토큰은 무시되며, 즉 검색 구문에 없는 것으로 처리됩니다。 ¹ LIKEmatch는 나열된 토크나이저에서 힌트로 직접 읽기를 사용하며, 그렇지 않으면 브루트 포스(전체 스캔) 스캔으로 폴백됩니다. 또한 LIKE는 전처리기나 후처리기 없이 splitByNonAlphaarray 토크나이저에 대해 *직접 읽기(힌트 없이)*도 지원합니다(use_text_index_like_evaluation_by_dictionary_scan으로 활성화). ² ILIKE는 *직접 읽기(힌트 없이)*를 통해서만 지원됩니다(use_text_index_like_evaluation_by_dictionary_scan = 1, splitByNonAlpha 또는 array 토크나이저). 인덱스를 힌트로 사용하는 폴백은 지원되지 않습니다. 즉, 설정이 비활성화되어 있거나 토크나이저가 지원되는 집합에 없으면 ILIKE에는 인덱스가 사용되지 않습니다. 전처리기가 있는 경우 lower 또는 upper여야 하며, 후처리기는 지원되지 않습니다. 실험적: 구문 검색 인수 지원(선택 사항). 실험적 매개변수 support_phrase_search(기본값: 0)는 인덱스가 토큰 위치를 저장할지 제어합니다. 1로 설정하면 인덱스는 위치 데이터(.pos 파일에 저장됨)를 추가로 저장하며, 이를 통해 hasPhrase 함수에서 직접 읽기를 사용한 정확한 구문 일치가 가능해집니다. 위치를 저장하면 인덱스의 디스크상 크기와 쓰기 비용이 증가하므로, 이 기능은 명시적으로 선택해야 합니다. 디스크상 포맷은 아직 안정되지 않았으므로, 이 매개변수는 실험적이며 향후 릴리스에서 변경될 수 있습니다. 따라서 support_phrase_search = 1로 인덱스를 생성하려면 MergeTree 설정 allow_experimental_text_index_phrase_search를 활성화해야 합니다. 포스팅 리스트만 저장하는 방식을 유지하려면 support_phrase_search = 0(기본값)으로 설정하십시오. 이 인수를 지정하지 않고 생성된 텍스트 인덱스에는 위치 정보가 저장되지 않습니다.
이 인수는 실험적이며 테스트 용도로만 사용해야 합니다. 위치 저장을 활성화하려면 MergeTree 설정 allow_experimental_text_index_phrase_search를 설정하십시오.
인덱스 세분화 수준. 텍스트 인덱스는 ClickHouse에서 스킵 인덱스의 한 종류로 구현됩니다. 하지만 다른 스킵 인덱스와 달리 텍스트 인덱스는 무한대로 설정된 세분화 수준(1억)을 사용합니다. 이는 텍스트 인덱스의 테이블 정의에서 확인할 수 있습니다. 예시:
Query
Response
매우 큰 인덱스 세분화 수준을 사용하면 텍스트 인덱스가 파트 전체에 생성됩니다. 명시적으로 지정한 인덱스 세분화 수준은 무시됩니다.

텍스트 인덱스 사용

SELECT 쿼리에서 텍스트 인덱스를 사용하는 방법은 간단합니다. 일반적인 문자열 검색 함수가 자동으로 인덱스를 활용하기 때문입니다. 컬럼 또는 테이블 파트에 인덱스가 없으면 문자열 검색 함수는 느린 브루트 포스(전체 스캔)로 대체됩니다.
텍스트 인덱스를 검색할 때는 hasAnyTokenshasAllTokens 함수를 사용하는 것을 권장합니다. 자세한 내용은 아래를 참조하십시오. 이 함수는 사용 가능한 모든 토크나이저와 가능한 모든 전처리기 및 후처리기 표현식에서 작동합니다. 그 외의 지원 함수는 역사적으로 텍스트 인덱스보다 먼저 도입되었기 때문에, 많은 경우 기존 동작을 유지해야 했습니다(예: 전처리기 또는 후처리기 미지원).

지원되는 함수

WHERE 절 또는 PREWHERE 절에서 텍스트 함수를 사용하는 경우 텍스트 인덱스를 사용할 수 있습니다:
= (equals)은 지정된 검색어 전체와 일치합니다. 예시:

IN

IN (in)은 equals와 비슷하지만, 모든 검색어를 일치 대상으로 합니다. 예시:
텍스트 인덱스에서는 NOT IN (notIn)을 지원하지 않습니다.

LIKEmatch

현재 이러한 함수는 인덱스 토크나이저가 splitByNonAlpha, ngrams 또는 sparseGrams인 경우에만 필터링에 텍스트 인덱스(text index)를 사용합니다.
NOT LIKE (notLike)는 텍스트 인덱스에서 지원되지 않습니다.
텍스트 인덱스와 함께 LIKE(like) 및 match 함수를 사용하려면 ClickHouse가 검색어에서 완전한 토큰을 추출할 수 있어야 합니다. ngrams 토크나이저를 사용하는 인덱스에서는 와일드카드 사이의 검색 문자열 길이가 ngram 길이와 같거나 더 길면 이 조건이 충족됩니다. splitByNonAlpha 토크나이저를 사용하는 텍스트 인덱스 예시:
예시의 supportsupport, supports, supporting 등에 일치할 수 있습니다. 이 유형의 쿼리는 부분 문자열(substring) 쿼리이므로 텍스트 인덱스로는 속도를 높일 수 없습니다. LIKE 쿼리에서 텍스트 인덱스를 활용하려면 LIKE 패턴을 다음과 같이 다시 작성해야 합니다:
support의 좌우 공백은 해당 용어가 token으로 추출되도록 합니다. 다행히 ClickHouse가 inverted index를 활용해 LIKE 쿼리 성능을 크게 높일 수 있는 특별한 경우가 있습니다. 자세한 내용은 LIKE/ILIKE 성능 튜닝 섹션을 참조하십시오.

multiSearchAny and multiMatchAny

multiSearchAny 및 해당 UTF-8 변형인 multiSearchAnyUTF8는 여러 리터럴 부분 문자열 중 하나라도 검색 대상 문자열에 포함되는지 확인하고, multiMatchAny는 여러 정규식 중 하나라도 일치하는지 확인합니다. 이 함수들은 LIKEmatch와 동일한 조건에서 텍스트 인덱스를 사용합니다(위 참조). 즉, ClickHouse가 각 검색 패턴에서 완전한 토큰을 추출할 수 있어야 하며, 검색 패턴 목록은 상수여야 합니다. 검색 패턴 중 하나라도 포함되어 있을 가능성이 있으면 해당 그래뉼(granule)을 읽습니다. multiMatchAny의 경우, 단일 pattern을 토큰 요구 사항으로 축약할 수 없으면(예: 모든 문서와 일치하는 .*) 텍스트 인덱스를 사용할 수 없으며, 쿼리는 전체 스캔으로 돌아갑니다. LIKEmatch와 마찬가지로, 부분 문자열 검색과 정규식 검색은 ngramssparseGrams 토크나이저에서 가장 잘 동작합니다. 이 토크나이저는 서로 겹치는 문자 n-그램을 인덱싱하므로, 검색 패턴은 부분 문자열로 나타나는 모든 위치에서 인덱스에 존재하는 n-그램으로 분해됩니다. 단어의 중간에서 시작하거나 끝나는 경우도 마찬가지입니다. 따라서 검색 패턴의 길이가 n-그램 크기 이상이면 그대로 사용할 수 있습니다. ngrams 토크나이저를 사용하는 텍스트 인덱스 예시:
반면 splitByNonAlpha 토크나이저는 완전한 토큰(전체 단어)만 인덱싱합니다. needle은 단어의 중간에서 시작하거나 끝날 수 있으므로, ClickHouse는 각 needle의 앞뒤 토큰을 삭제합니다. 따라서 인덱스는 완전한 토큰만 사용해 그래뉼을 가지치기할 수 있습니다. 부분 문자열 및 정규 표현식 검색에서 splitByNonAlpha와 함께 인덱스를 사용하려면, 각 needle을 구분 문자(예: 공백)로 감싸 하나 이상의 완전한 토큰이 되도록 하십시오. splitByNonAlpha 토크나이저를 사용하는 텍스트 인덱스의 예시:

startsWith and endsWith

LIKE와 마찬가지로, startsWithendsWith 함수는 검색어에서 완전한 토큰을 추출할 수 있을 때만 텍스트 인덱스를 사용할 수 있습니다. ngrams 토크나이저를 사용하는 인덱스에서는 와일드카드 사이의 검색 문자열 길이가 ngram 길이와 같거나 더 길면 이에 해당합니다. 텍스트 인덱스가 후처리기(postprocessor)를 사용하는 경우, 추출된 힌트 토큰이 정규화 후에도 비어 있지 않으면 이러한 함수는 Hint 모드에서도 계속 인덱스를 사용할 수 있습니다. 정규화 과정에서 모든 힌트 토큰이 제거되면 해당 프레디케이트에는 인덱스가 사용되지 않습니다. splitByNonAlpha 토크나이저를 사용하는 텍스트 인덱스의 예시:
예시에서는 clickhouse만 토큰으로 간주됩니다. supportsupport, supports, supporting 등과 일치할 수 있으므로 토큰이 아닙니다. clickhouse supports로 시작하는 모든 행을 찾으려면 검색 패턴 끝에 공백을 추가하십시오:
마찬가지로 endsWith도 앞에 공백을 넣어 사용해야 합니다:

hasToken

hasTokensplitByNonAlpha가 아닌 토크나이저 및/또는 전처리기/후처리기 표현식이 있는 텍스트 인덱스에서 조회에 사용할 때 몇 가지 주의할 점이 있습니다. 대신 hasAnyTokenshasAllTokens를 사용하는 것이 좋습니다.대소문자를 구분하지 않는 변형인 hasTokenCaseInsensitivehasTokenCaseInsensitiveOrNull은 텍스트 인덱스를 인식하지 못합니다. 즉, 텍스트 인덱스가 있는 컬럼에서도 항상 전체 행 스캔으로 실행됩니다. 대소문자를 구분하지 않는 일치가 필요하다면 lower(...) 전처리기 또는 후처리기를 사용하고, 이를 hasToken / hasAllTokens / hasAnyTokens와 함께 조합하십시오.
함수 hasToken은 주어진 단일 토큰과 일치하는지 확인합니다. 앞서 설명한 함수들과 달리, 이 함수들은 검색어를 토큰화하지 않습니다(입력이 단일 토큰이라고 가정합니다). 예시:

hasAnyTokenshasAllTokens

함수 hasAnyTokenshasAllTokens는 지정된 토큰 중 하나 이상 또는 전체와 일치하는지 확인합니다. 이 두 함수는 검색 토큰을 인덱스 컬럼에 사용된 것과 동일한 토크나이저로 토큰화할 문자열로 받거나, 검색 전에 추가 토큰화가 적용되지 않는 이미 처리된 토큰의 배열로 받을 수 있습니다. 자세한 내용은 함수 문서를 참조하십시오. 예시:

hasPhrase

함수 hasPhrase는 구(phrase)와 일치하는지 확인합니다. 즉, 모든 토큰이 검색 문자열에 있는 순서 그대로 연속해서 나타나야 합니다. 모든 토큰이 어딘가에 존재하기만 하면 되는 hasAllTokens와 달리, hasPhrase는 토큰이 연속된 시퀀스로 나타나야 합니다. 검색 구는 인덱스 컬럼에 구성된 것과 동일한 토크나이저를 사용해 토큰화됩니다. 텍스트 인덱스가 후처리기를 사용하는 경우, 검색 구도 인덱스 lookup 전에 정규화됩니다. 이 함수는 splitByNonAlpha, splitByString, ngrams, asciiCJK 토크나이저 중 하나를 요구합니다. 예시:

has

배열 함수 has는 문자열 배열에서 단일 토큰과 일치하는지 확인합니다. 예시:

hasAnyhasAll

배열 함수 hasAnyhasAll는 인덱스가 적용된 배열 컬럼에 상수 검색 문자열 집합의 일부 또는 전체가 포함되어 있는지 확인합니다. 예시:

mapContains

함수 mapContains는 맵의 키에서 검색 문자열로부터 추출한 토큰과 일치하는지 확인합니다. 동작은 String 컬럼에 대한 equals 함수와 유사합니다. 텍스트 인덱스는 mapKeys(map) 표현식에 생성된 경우에만 사용됩니다. 예시:

mapContainsValue

함수 mapContainsValue는 맵의 값에서 검색 문자열로부터 추출된 토큰과 일치하는지 확인합니다. 동작은 String 컬럼에 사용하는 equals 함수와 유사합니다. 텍스트 인덱스는 mapValues(map) 표현식에 생성된 경우에만 사용됩니다. 예시:

mapContainsKeyLike and mapContainsValueLike

함수 mapContainsKeyLikemapContainsValueLike는 맵의 모든 키 또는 값에 대해 각각 패턴 일치 여부를 확인합니다. 예시:

operator[]

접근 operator[]를 텍스트 인덱스와 함께 사용하면 키와 값을 필터링할 수 있습니다. 텍스트 인덱스는 mapKeys(map) 또는 mapValues(map) 표현식에 생성되었거나, 두 표현식 모두에 생성된 경우에만 사용됩니다. 예시:
텍스트 인덱스에서 Array(T)맵(K, V) 유형의 컬럼을 사용하는 예시는 다음과 같습니다.

Array(String) 컬럼 인덱싱

작성자가 키워드로 블로그 게시물을 분류하는 블로깅 플랫폼을 생각해 보겠습니다. 사용자가 주제를 검색하거나 클릭해 관련 콘텐츠를 찾을 수 있도록 하려 합니다. 다음 테이블 정의를 살펴보겠습니다:
텍스트 인덱스가 없으면 특정 키워드(예: clickhouse)가 포함된 게시물을 찾으려면 모든 항목을 스캔해야 합니다:
플랫폼 규모가 커질수록 쿼리가 각 행의 keywords 배열을 모두 확인해야 하므로 점점 더 느려집니다. 이 성능 문제를 해결하기 위해 keywords 컬럼에 텍스트 인덱스를 정의합니다:

맵 컬럼 인덱싱

관측성 관련 많은 사용 사례에서 로그 메시지는 “구성 요소”로 분리되어 적절한 데이터 타입에 저장됩니다. 예를 들어 타임스탬프에는 날짜 및 시간, 로그 레벨에는 enum과 같은 타입이 사용됩니다. 메트릭 필드는 key-value 쌍으로 저장하는 것이 가장 적합합니다. 운영 팀은 문제 해결, 보안 사고 대응, 모니터링을 위해 로그를 효율적으로 검색할 수 있어야 합니다. 다음 로그 테이블을 살펴보겠습니다:
텍스트 인덱스가 없으면 데이터를 검색할 때 테이블 전체를 스캔해야 합니다:
로그 양이 증가하면 이러한 쿼리의 속도가 느려집니다. 해결 방법은 키와 값에 텍스트 인덱스를 생성하는 것입니다. 필드 이름이나 속성 유형으로 로그를 찾아야 하는 경우 mapKeys를 사용해 텍스트 인덱스를 생성하십시오:
속성의 실제 내용을 검색해야 할 때는 mapValues를 사용해 텍스트 인덱스를 생성합니다:
쿼리 예시:

JSON 컬럼 인덱싱

텍스트 인덱스는 JSON 컬럼에 다음 3가지 방식으로 사용할 수 있습니다:
  1. 특정 서브컬럼에 대한 인덱스 — 일반 컬럼과 마찬가지로 알려진 JSON 경로에 텍스트 인덱스를 생성합니다. 그러면 해당 경로의 이 인덱싱됩니다.
  2. JSONAllPaths를 사용하는 경로 기반 인덱스 — 각 그래뉼에 있는 모든 경로를 인덱싱하여, 쿼리한 경로를 포함할 수 없는 그래뉼을 건너뜁니다. 컬럼과 유사합니다.
  3. JSONAllValues를 사용하는 값 기반 인덱스 — 모든 JSON 경로에 걸친 모든 값을 인덱싱하여, 단일 인덱스로 모든 JSON 서브컬럼의 전문 검색을 가속화합니다.

특정 서브컬럼의 인덱스

일반 컬럼과 동일한 구문으로 모든 JSON 서브컬럼에 스킵 인덱스를 생성할 수 있습니다. 인덱스 표현식에서 JSON 서브컬럼을 참조하는 방법은 두 가지입니다:
  • JSON 타입 힌트에 선언된 타입 지정 경로 — 이름으로 직접 접근합니다: json.a.
  • 명시적 cast를 사용하는 동적 경로:: cast 구문을 사용합니다: json.b::String.
예시 인덱스 정의:
Query
예시 쿼리:
Query
Response
쿼리 예시:
Query
Response

JSONAllPaths를 사용한 경로 기반 인덱스

Map 컬럼과 마찬가지로 JSON 컬럼에서도 JSONAllPaths를 사용해 텍스트 인덱스를 생성할 수 있습니다. 이 인덱스는 각 그래뉼에 존재하는 JSON 경로 집합을 저장하며, 질의한 경로가 없는 그래뉼을 건너뛰는 데 이를 사용합니다. 예시 인덱스 정의:
Query
EXPLAIN indexes = 1을 사용하면 스킵 인덱스가 실제로 사용되는지 확인할 수 있습니다. 경로가 하나의 파트에만 존재하면 인덱스는 다른 파트를 건너뜁니다. 예시:
Query
Response
경로가 어떤 파트에도 존재하지 않으면 모든 파트와 그래뉼을 건너뜁니다. 예시:
Query
Response
IS NOT NULL도 인덱스를 사용합니다 — 경로가 없으면 값이 NULL이 되므로 해당 그래뉼은 건너뜁니다: 예시:
Query
Response

JSONAllValues를 사용한 값 기반 인덱스

텍스트 인덱스는 JSONAllValues 함수를 사용해 JSON 컬럼 검색을 가속할 수 있습니다. JSONAllValues는 JSON 컬럼의 모든 값을 Array(String)으로 반환합니다. 문자열이 아닌 데이터 타입의 값(예: 정수, 배열)은 텍스트 표현으로 변환됩니다. JSONAllValues를 사용해 생성한 텍스트 인덱스는 각 행의 모든 JSON 경로에 있는 이러한 텍스트 표현을 인덱싱합니다. 이 인덱스는 이후 개별 JSON 서브컬럼을 기준으로 필터링하는 쿼리를 가속할 수 있습니다. 쿼리가 특정 서브컬럼(예: data.user_name = 'alice')을 기준으로 필터링할 때, 텍스트 인덱스는 JSON 값 어디에도 검색 토큰이 없는 행(및 그래뉼)을 빠르게 건너뛸 수 있습니다.
서로 다른 JSON 경로에 동일한 토큰이 포함되어 있으면 인덱스에서 거짓 양성(false positive)이 발생할 수 있습니다. 예를 들어, 행 1에 {"a": "hello", "b": "world"}가 있고 쿼리에서 data.a = 'world'를 검색하는 경우, 텍스트 인덱스는 world가 경로 a가 아니라 b에 속한다는 점을 구분할 수 없습니다. 이러한 경우 인덱스는 해당 행을 건너뛰지 않으며, 최종 평가는 실제 컬럼 데이터에 대한 필터가 처리합니다. 이는 인덱스가 빠른 사전 필터로 동작하는 다른 텍스트 인덱스 사용 사례와 동일합니다.
인덱스 생성
인덱스 정의 예시:
지원되는 쿼리 패턴
인덱스가 생성되면 String 컬럼에 사용하는 것과 동일한 함수와 모든 컬럼에 사용할 수 있는 equals 함수를 사용해 JSON 서브컬럼 쿼리 속도를 높일 수 있습니다. 서브컬럼 접근:
명시적 CAST를 사용한 서브컬럼 접근:
IN 연산자:
예를 들어, 일반적인 텍스트 인덱스 검색은
임의의 순서로 주어진 토큰을 모두 포함하는 모든 행과 일치합니다. 예시에서는 While she stayed in Tokyo, the weather was great. 행이 필터와 일치합니다. 반면 구문 검색은 주어진 순서대로 토큰이 일치하는 것을 의미합니다. 예를 들어,
weather in Tokyo 토큰 시퀀스를 포함하는 모든 행(예: How is the weather in Tokyo?)과 일치합니다. 텍스트 인덱스는 구문에 포함된 모든 토큰의 포스팅 리스트를 교집합하여 후보 그래뉼을 식별함으로써 구문 검색을 가속합니다. 그런 다음 ClickHouse는 해당 그래뉼 내에서 토큰이 정확히 인접해 있는지 확인합니다. 이 과정은 비교적 비용이 크며 일반적인 텍스트 검색 쿼리보다 느립니다. 구문 검색 쿼리의 속도를 높이려면 텍스트 인덱스에서 위치 저장을 활성화하십시오(위의 Optional parameters 참고). hasPhrase는 토크나이저 splitByNonAlpha, splitByString, ngrams, asciiCJK와 함께 사용할 수 있습니다. 지정된 구문 문자열은 인덱스의 토크나이저를 사용해 토큰화됩니다. 구문 내 구분자 문자는 무시됩니다. splitByNonAlpha를 토크나이저로 사용한다고 가정하면 hasPhrase(text, 'quick+brown')hasPhrase(text, 'quick brown')와 동일합니다.

예시

Query
Response
2행('New weather in York')은 토큰 순서가 올바르지 않아 일치하지 않습니다. 3행('weather in New Orleans')은 토큰 'York'를 포함하지 않아 일치하지 않습니다。

성능 최적화

직접 읽기

일부 타입의 텍스트 쿼리는 “직접 읽기”라는 최적화를 사용하면 속도를 크게 높일 수 있습니다. 예시:
직접 읽기 최적화는 기본 텍스트 컬럼에 접근하지 않고 텍스트 인덱스(즉, 텍스트 인덱스 조회)만 사용해 쿼리에 응답합니다. 텍스트 인덱스 조회는 읽는 데이터 양이 비교적 적기 때문에 ClickHouse의 일반적인 스킵 인덱스보다 훨씬 빠릅니다(일반적인 스킵 인덱스는 스킵 인덱스 조회를 수행한 다음 남아 있는 그래뉼을 로드하고 필터링합니다). 직접 읽기는 두 가지 설정으로 제어됩니다. 지원되는 함수 직접 읽기 최적화는 hasToken, hasAllTokens, hasAnyTokens 함수를 지원합니다. 텍스트 인덱스가 array 토크나이저로 정의된 경우에는 equals, has, hasAny, hasAll, mapContainsKey, mapContainsValue 함수에도 직접 읽기가 지원됩니다. 이 함수들은 AND, OR, NOT 연산자로 조합할 수도 있습니다. WHERE 또는 PREWHERE 절에는 추가적인 비텍스트 검색 함수 필터(텍스트 컬럼 또는 다른 컬럼에 대한 필터)도 포함될 수 있습니다. 이 경우에도 직접 읽기 최적화는 사용되지만 효과는 다소 떨어집니다(지원되는 텍스트 검색 함수에만 적용됩니다). 쿼리가 직접 읽기를 사용하는지 확인하려면 EXPLAIN PLAN actions = 1과 함께 쿼리를 실행하십시오. 예를 들어, 직접 읽기를 비활성화한 쿼리는
반환값
반면 동일한 쿼리를 query_plan_direct_read_from_text_index = 1 설정으로 실행하면
반환값
두 번째 EXPLAIN PLAN 출력에는 가상 컬럼 __text_index_<index_name>_<function_name>_<id>이 포함됩니다. 이 컬럼이 있으면 직접 읽기가 사용된 것입니다. WHERE 필터 절에 텍스트 검색 함수만 포함되어 있으면 쿼리는 컬럼 데이터를 전혀 읽지 않을 수 있으며, 직접 읽기를 통해 가장 큰 성능 향상을 얻을 수 있습니다. 하지만 쿼리의 다른 부분에서 텍스트 컬럼에 접근하는 경우에도 직접 읽기는 여전히 성능 개선에 도움이 됩니다. 힌트로 사용하는 직접 읽기 힌트로 사용하는 직접 읽기는 일반적인 직접 읽기와 동일한 원리를 따르지만, 기본 텍스트 컬럼을 제거하는 대신 텍스트 인덱스 데이터로 추가 필터를 만듭니다. 이는 텍스트 인덱스만 읽으면 거짓 양성(false positive)이 발생할 수 있는 함수에 사용됩니다. 지원되는 함수는 like, startsWith, endsWith, equals, has, hasPhrase, mapContainsKey, mapContainsValue입니다. 이 추가 필터는 다른 필터와 함께 사용될 때 결과 집합을 더 좁히는 추가 선택도를 제공할 수 있으며, 다른 컬럼에서 읽는 데이터 양을 줄이는 데 도움이 됩니다. 힌트로 사용하는 직접 읽기는 query_plan_text_index_add_hint 설정으로 제어됩니다(기본적으로 활성화됨). 힌트가 없는 쿼리 예시:
반환
반면 query_plan_text_index_add_hint = 1을 설정해 동일한 쿼리를 실행하면
반환값
두 번째 EXPLAIN PLAN 출력에서는 추가 결합 조건(__text_index_...)이 필터 조건에 추가된 것을 확인할 수 있습니다. PREWHERE 최적화 덕분에 필터 조건은 계산 복잡도가 낮은 순서대로 적용되는 3개의 개별 결합 조건으로 분해됩니다. 이 쿼리의 적용 순서는 __text_index_..., 그다음 greaterOrEquals(...), 마지막으로 like(...)입니다. 이 순서를 사용하면 텍스트 인덱스와 원래 필터가 스키핑하는 그래뉼보다 더 많은 데이터 그래뉼을 스키핑할 수 있으며, WHERE 절 뒤에 있는 쿼리에서 사용되는 비용이 큰 컬럼을 읽기 전에 이를 수행하므로 읽어야 하는 데이터 양이 더욱 줄어듭니다.

LIKE/ILIKE 쿼리

LIKE/ILIKE 쿼리 패턴이 %<alpha-numeric-characters-without-spaces>%이고 텍스트 인덱스 토크나이저가 splitByNonAlpha 또는 array인 경우, ClickHouse는 역색인을 활용해 LIKE/ILIKE 쿼리 속도를 크게 높입니다. 이를 위해 ClickHouse는 일치하는 패턴을 찾을 때 전체 테이블 스캔 대신 역색인 딕셔너리를 스캔합니다. 이 최적화가 활성화되면 LIKE/ILIKE 쿼리는 전체 테이블 스캔보다 훨씬 빨라집니다. 하지만 패턴이 딕셔너리 토큰 대부분과 일치하면 전체 테이블 스캔보다 성능이 더 나빠질 수 있습니다. 다행히 이를 방지하는 폴백 메커니즘이 있습니다. 이 최적화는 다음 설정으로 제어됩니다: 폴백 메커니즘은 다음 두 가지 설정으로 제어됩니다: 이 최적화는 likeilike 함수만 지원합니다.

캐싱

메모리에서 텍스트 인덱스의 일부를 버퍼링하기 위한 서버 전역 캐시가 여러 가지 있습니다(섹션 구현 세부 사항 참조). 현재는 I/O를 줄이기 위해 텍스트 인덱스의 역직렬화된 헤더, 토큰, 포스팅 리스트에 대한 캐시가 제공됩니다. 쿼리가 개별 캐시를 읽고 쓰지 않도록 하려면 use_text_index_header_cache, use_text_index_tokens_cache, use_text_index_postings_cache 설정을 사용하십시오. 캐시를 지우려면 SYSTEM CLEAR TEXT INDEX CACHES SQL 문을 사용하십시오. 캐시를 구성하려면 다음 server settings를 참조하십시오.

토큰 캐시 설정

헤더 캐시 설정

포스팅 리스트 캐시 설정

제한 사항

현재 텍스트 인덱스에는 다음과 같은 제한 사항이 있습니다.
  • 토큰 수가 많은 텍스트 인덱스(예: 100억 개의 토큰)를 머티리얼라이즈하는 과정에서 상당한 양의 메모리를 사용할 수 있습니다. 텍스트 인덱스 머티리얼라이즈는 직접(ALTER TABLE <table> MATERIALIZE INDEX <index>) 수행할 수도 있고, 파트 병합 중에 간접적으로 발생할 수도 있습니다.
  • 4,294,967,296개(= 2^32 = 약 42억)보다 많은 행이 있는 파트에서는 텍스트 인덱스를 구체화할 수 없습니다. 구체화된 텍스트 인덱스가 없으면 쿼리는 해당 파트 내에서 느린 브루트포스 검색으로 폴백됩니다. 최악의 경우를 가정해 보겠습니다. 파트에 String 유형의 단일 컬럼만 포함되어 있고, MergeTree 설정 max_bytes_to_merge_at_max_space_in_pool(기본값: 150 GB)이 변경되지 않았다고 가정합니다. 이 경우 해당 컬럼의 행당 평균 문자 수가 29.5자 미만이면 이런 상황이 발생합니다. 실제로는 테이블에 다른 컬럼도 포함되므로 임계값은 이보다 몇 배 더 작아집니다(다른 컬럼의 개수, 유형, 크기에 따라 달라집니다).

텍스트 인덱스와 블룸 필터 기반 인덱스 비교

String 프레디케이트는 텍스트 인덱스와 블룸 필터 기반 인덱스(인덱스 유형 bloom_filter, ngrambf_v1, tokenbf_v1, sparse_grams)를 사용해 더 빠르게 처리할 수 있지만, 두 방식은 설계와 의도된 사용 사례 측면에서 근본적으로 다릅니다. 블룸 필터 인덱스
  • 거짓 양성(false positive)을 발생시킬 수 있는 확률적 데이터 구조를 기반으로 합니다.
  • 집합 멤버십 질의에만 응답할 수 있습니다. 즉, 컬럼에 토큰 X가 포함되어 있을 수 있는지, 아니면 X가 확실히 포함되어 있지 않은지만 판단할 수 있습니다.
  • 쿼리 실행 중 큰 범위를 스키핑할 수 있도록 그래뉼 수준의 정보를 저장합니다.
  • 적절하게 튜닝하기가 어렵습니다(예시는 여기를 참조하십시오).
  • 비교적 크기가 작습니다(파트당 수 킬로바이트 또는 수 메가바이트).
텍스트 인덱스
  • 토큰에 대해 결정적인 역색인을 구축합니다. 인덱스 자체로는 거짓 양성이 발생하지 않습니다.
  • 텍스트 검색 워크로드에 특화되어 최적화되어 있습니다.
  • 효율적인 용어 조회를 위해 행 수준의 정보를 저장합니다.
  • 비교적 크기가 큽니다(파트당 수십에서 수백 메가바이트).
블룸 필터 기반 인덱스는 “부수 효과”로만 전문 검색을 지원합니다.
  • 고급 토큰화와 전처리를 지원하지 않습니다.
  • 여러 토큰을 대상으로 한 검색을 지원하지 않습니다.
  • 역색인에서 기대하는 성능 특성을 제공하지 않습니다.
반면 텍스트 인덱스는 전문 검색을 위해 특별히 설계되었습니다.
  • 토큰화와 전처리를 제공합니다.
  • hasAllTokens, LIKE, match 및 유사한 텍스트 검색 함수를 효율적으로 지원합니다.
  • 대규모 텍스트 코퍼스에서 훨씬 뛰어난 확장성을 제공합니다.

구현 세부 사항

각 텍스트 인덱스는 두 개의 (추상적인) 데이터 구조로 구성됩니다:
  • 각 토큰을 포스팅 리스트에 매핑하는 딕셔너리와
  • 각각이 행 번호 집합을 나타내는 포스팅 리스트 집합입니다.
텍스트 인덱스는 파트 전체를 대상으로 생성됩니다. 다른 스킵 인덱스와 달리, 텍스트 인덱스는 데이터 파트 머지 시 다시 생성하지 않고 머지할 수 있습니다(아래 참조). 인덱스를 생성하는 동안 3개의 파일이 생성됩니다(파트별): 딕셔너리 블록 파일 (.dct) 텍스트 인덱스의 토큰은 정렬된 뒤 각각 512개의 토큰으로 이루어진 딕셔너리 블록에 저장됩니다(블록 크기는 매개변수 dictionary_block_size로 구성할 수 있습니다). 딕셔너리 블록 파일(.dct)은 파트 내 모든 인덱스 그래뉼의 모든 딕셔너리 블록으로 구성됩니다. 인덱스 헤더 파일 (.idx) 인덱스 헤더 파일에는 각 딕셔너리 블록의 첫 번째 토큰과, 딕셔너리 블록 파일 내 해당 블록의 상대 오프셋이 저장됩니다. 이 희소 인덱스 구조는 ClickHouse의 희소 프라이머리 키 인덱스)와 유사합니다. 포스팅 리스트 파일 (.pst) 모든 토큰의 포스팅 리스트는 포스팅 리스트 파일에 순차적으로 배치됩니다. 공간을 절약하면서도 빠른 교집합 및 합집합 연산을 지원하기 위해 포스팅 리스트는 roaring bitmaps로 저장됩니다. 포스팅 리스트가 posting_list_block_size보다 크면 여러 블록으로 분할되어 포스팅 리스트 파일에 순차적으로 저장됩니다. 위치 파일 (.pos) 선택 사항이며, 인덱스 인수 support_phrase_search = 1인 경우에만 생성됩니다. 일치하는 행 내에서 토큰의 위치를 저장합니다. 텍스트 인덱스의 머지 데이터 파트가 머지될 때 텍스트 인덱스를 처음부터 다시 생성할 필요는 없습니다. 대신 머지 프로세스의 별도 단계에서 효율적으로 머지할 수 있습니다. 이 단계에서는 각 입력 파트의 텍스트 인덱스에 있는 정렬된 딕셔너리를 읽어 새로운 통합 딕셔너리로 결합합니다. 포스팅 리스트의 행 번호도 초기 머지 단계에서 생성된 이전 행 번호와 새 행 번호 간 매핑을 사용해, 머지된 데이터 파트에서의 새 위치를 반영하도록 다시 계산됩니다. 이러한 텍스트 인덱스 머지 방식은 _part_offset 컬럼이 있는 프로젝션이 머지되는 방식과 유사합니다. 소스 파트에 인덱스가 구체화되어 있지 않으면 인덱스를 생성해 임시 파일에 기록한 다음, 다른 파트의 인덱스 및 다른 임시 인덱스 파일의 인덱스와 함께 머지합니다. 디버깅 테이블 함수 mergeTreeTextIndex를 사용해 텍스트 인덱스를 검사할 수 있습니다.

예시: Hacker News 데이터셋

텍스트가 많은 대규모 데이터셋에서 텍스트 인덱스의 성능 향상을 살펴보겠습니다. 인기 있는 Hacker News 웹사이트의 댓글 2,870만 행을 사용합니다. 다음은 텍스트 인덱스가 없는 테이블입니다:
28.7M개의 행이 S3의 Parquet 파일에 있습니다 - 이를 hackernews 테이블에 삽입해 보겠습니다:
ALTER TABLE을 사용해 comment 컬럼에 텍스트 인덱스를 추가한 후 이를 구체화합니다:
이제 hasToken, hasAnyTokens, hasAllTokens 함수를 사용해 쿼리를 실행해 보겠습니다. 다음 예시에서는 일반적인 인덱스 스캔과 직접 읽기 최적화 사이의 성능 차이가 얼마나 큰지 보여줍니다.

1. hasToken 사용하기

hasToken은 텍스트에 특정 단일 토큰이 포함되어 있는지 확인합니다. 대소문자를 구분하는 토큰인 ‘ClickHouse’를 검색하겠습니다. 직접 읽기 비활성화 (표준 스캔) 기본적으로 ClickHouse는 스킵 인덱스를 사용해 그래뉼을 필터링한 다음, 해당 그래뉼의 컬럼 데이터를 읽습니다. 직접 읽기를 비활성화하면 이 동작을 시뮬레이션할 수 있습니다.
직접 읽기 활성화(빠른 인덱스 읽기) 이제 직접 읽기가 활성화된 상태(기본값)에서 동일한 쿼리를 실행합니다.
직접 읽기 쿼리는 인덱스만 읽기 때문에 45배 이상 더 빠르며(0.362초 대 0.008초), 처리하는 데이터 양도 훨씬 적습니다(9.51 GB 대 3.15 MB).

2. hasAnyTokens 사용하기

hasAnyTokens는 텍스트에 지정된 토큰이 하나 이상 포함되어 있는지 확인합니다. ‘love’ 또는 ‘ClickHouse’가 포함된 댓글을 검색해 보겠습니다. 직접 읽기 비활성화 (표준 스캔)
직접 읽기 활성화됨(빠른 인덱스 읽기)
이 일반적인 “OR” 검색에서는 속도 향상이 훨씬 더 크게 나타납니다. 전체 컬럼 스캔을 피하면 쿼리 속도가 거의 89배 빨라집니다(1.329초 대비 0.015초).

3. hasAllTokens 사용

hasAllTokens는 텍스트에 지정된 모든 토큰이 포함되어 있는지 확인합니다. ‘love’와 ‘ClickHouse’가 모두 포함된 댓글을 검색하겠습니다. 직접 읽기 비활성화(표준 스캔) 직접 읽기가 비활성화되어 있어도 표준 스킵 인덱스는 여전히 효과적입니다. 28.7M행을 147.46K행으로 줄여 주지만, 여전히 컬럼에서 57.03 MB를 읽어야 합니다.
직접 읽기 활성화됨 (빠른 인덱스 읽기) 직접 읽기는 인덱스 데이터만 사용해 쿼리를 처리하므로, 147.46 KB만 읽습니다.
이 “AND” 검색에서는 직접 읽기 최적화가 일반적인 스킵 인덱스 스캔보다 26배 이상 빠릅니다(0.184초 대비 0.007초). 직접 읽기 최적화는 복합 불리언 표현식에도 적용됩니다. 여기서는 ‘ClickHouse’ OR ‘clickhouse’를 대소문자를 구분하지 않고 검색합니다. 직접 읽기 비활성화(표준 스캔)
직접 읽기 활성화 (빠른 인덱스 읽기)
인덱스 결과를 조합하면 직접 읽기 쿼리는 34배 더 빨라지고(0.450초 대비 0.013초), 9.58 GB의 컬럼 데이터를 읽지 않아도 됩니다. 이 경우에는 hasAnyTokens(comment, ['ClickHouse', 'clickhouse']) 구문이 더 적합하고 효율적입니다. 오래된 자료
마지막 수정일 2026년 7월 23일