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

텍스트 인덱스 생성

텍스트 인덱스를 생성하려면 먼저 해당 실험적 설정을 활성화하십시오:
텍스트 인덱스는 다음 구문을 사용해 String, FixedString, Array(String), Array(FixedString), 그리고 Map (mapKeysmapValues 맵 함수를 통해) 컬럼에 정의할 수 있습니다:
토크나이저 인수. tokenizer 인수는 토크나이저를 지정합니다.
  • splitByNonAlpha는 ASCII 영숫자가 아닌 문자를 기준으로 문자열을 분할합니다(함수 splitByNonAlpha도 참조).
  • splitByString(S)는 사용자 정의 구분자 문자열 S를 기준으로 문자열을 분할합니다(함수 splitByString도 참조). 구분자는 선택적 매개변수로 지정할 수 있습니다. 예를 들어 tokenizer = splitByString([', ', '; ', '\n', '\\'])와 같습니다. 각 문자열은 여러 문자로 이루어질 수 있습니다(예시의 ', '). 명시적으로 지정하지 않으면(예: tokenizer = splitByString) 기본 구분자 목록은 단일 공백 문자 [' ']입니다.
  • ngrams(N)는 문자열을 같은 크기의 N-그램으로 분할합니다(함수 ngrams도 참조). n-그램 길이는 2에서 8 사이의 선택적 정수 매개변수로 지정할 수 있습니다. 예를 들어 tokenizer = ngrams(3)와 같습니다. 명시적으로 지정하지 않으면(예: tokenizer = ngrams) 기본 n-그램 크기는 3입니다.
  • array는 토큰화를 수행하지 않습니다. 즉, 각 행의 값 자체가 하나의 토큰이 됩니다(함수 array도 참조).
  • sparseGrams(min_length, max_length, min_cutoff_length)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-그램만 인덱스에 저장합니다.
splitByString 토크나이저는 분할 구분자를 왼쪽에서 오른쪽 순서로 적용합니다. 이로 인해 모호성이 생길 수 있습니다. 예를 들어 구분자 문자열이 ['%21', '%']이면 %21abc['abc']로 토큰화되지만, 구분자 문자열의 순서를 ['%', '%21']로 바꾸면 ['21abc']가 출력됩니다. 대부분의 경우 더 긴 구분자가 먼저 일치하도록 하는 것이 좋습니다. 일반적으로는 구분자 문자열을 길이가 긴 순서대로 전달하면 됩니다. 구분자 문자열이 prefix code를 이루는 경우에는 임의의 순서로 전달해도 됩니다.
현재로서는 중국어와 같은 비서구권 언어 텍스트에 텍스트 인덱스를 구축하는 것을 권장하지 않습니다. 현재 지원되는 토크나이저는 인덱스 크기를 매우 크게 만들고 쿼리 시간을 크게 늘릴 수 있습니다. 앞으로는 이러한 경우를 더 잘 처리할 수 있도록 언어별 특화 토크나이저를 추가할 계획입니다.
토크나이저가 입력 문자열을 어떻게 분할하는지 테스트하려면 ClickHouse의 tokens 함수를 사용할 수 있습니다: 예시:
반환값
전처리기 인수. 선택적 인수 preprocessor는 토큰화 전에 입력 문자열을 변환하는 표현식입니다. 전처리기 인수의 일반적인 사용 사례는 다음과 같습니다.
  1. 대소문자를 구분하지 않는 매칭을 위해 입력 문자열을 소문자(또는 대문자)로 변환합니다. 예: lower, lowerUTF8. 아래 첫 번째 예시를 참조하십시오.
  2. UTF-8 정규화를 수행합니다. 예: normalizeUTF8NFC, normalizeUTF8NFD, normalizeUTF8NFKC, normalizeUTF8NFKD, toValidUTF8.
  3. 불필요한 문자 또는 부분 문자열을 제거하거나 변환합니다. 예: extractTextFromHTML, substring, idnaEncode.
전처리기 표현식은 String 또는 FixedString 타입의 입력값을 동일한 타입의 값으로 변환해야 합니다. 예시:
  • INDEX idx(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(col))
  • INDEX idx(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = substringIndex(col, '\n', 1))
  • INDEX idx(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(extractTextFromHTML(col))
또한 전처리기 표현식은 텍스트 인덱스가 정의된 컬럼만 참조해야 합니다. 비결정적 함수를 사용하는 것은 허용되지 않습니다. 함수 hasToken, hasAllTokens, hasAnyTokens는 검색어를 토큰화하기 전에 먼저 전처리기를 사용해 검색어를 변환합니다. 예를 들면 다음과 같습니다.
다음과 같습니다:
기타 인수. ClickHouse의 텍스트 인덱스는 보조 인덱스로 구현됩니다. 하지만 다른 스키핑 인덱스와 달리 텍스트 인덱스의 기본 인덱스 GRANULARITY는 64입니다. 이 값은 경험적으로 선택되었으며, 대부분의 사용 사례에서 속도와 인덱스 크기 사이에 적절한 균형을 제공합니다. 고급 사용자는 다른 인덱스 세분화 수준을 지정할 수 있지만, 이는 권장하지 않습니다.
다음 고급 매개변수의 기본값은 거의 모든 상황에서 잘 작동합니다. 이를 변경하는 것은 권장하지 않습니다.선택적 매개변수 dictionary_block_size (기본값: 128)는 딕셔너리 블록의 크기를 행 수 기준으로 지정합니다.선택적 매개변수 dictionary_block_frontcoding_compression (기본값: 1)은 딕셔너리 블록이 압축 방식으로 프론트 코딩을 사용하는지 여부를 지정합니다.선택적 매개변수 max_cardinality_for_embedded_postings (기본값: 16)는 포스팅 리스트를 딕셔너리 블록에 내장할지 결정하는 카디널리티 임계값을 지정합니다.선택적 매개변수 bloom_filter_false_positive_rate (기본값: 0.1)는 딕셔너리 블룸 필터의 위양성 비율을 지정합니다.
테이블이 생성된 후에도 컬럼에 텍스트 인덱스를 추가하거나 제거할 수 있습니다:

텍스트 인덱스 사용

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

지원되는 함수

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

=!=

= (equals) 및 != (notEquals )는 지정한 검색어와 전체가 일치하는 경우에 매칭됩니다. 예시:
텍스트 인덱스는 =!=를 지원하지만, 동등/부등 검색은 array 토크나이저를 사용할 때만 유의미합니다(이 경우 인덱스는 각 행의 전체 값을 저장합니다).

IN and NOT IN

IN (in) 및 NOT IN (notIn)은 함수 equalsnotEquals와 비슷하지만, 검색어 전체와 일치하는 경우(IN) 또는 검색어와 전혀 일치하지 않는 경우(NOT IN)에 사용됩니다. 예시:
=!=와 동일한 제약이 적용됩니다. 즉, INNOT INarray 토크나이저와 함께 사용할 때만 의미가 있습니다.

LIKE, NOT LIKEmatch

현재 이러한 함수는 인덱스 토크나이저가 splitByNonAlpha 또는 ngrams인 경우에만 필터링에 텍스트 인덱스를 사용합니다.
텍스트 인덱스와 함께 LIKE like, NOT LIKE (notLike), 그리고 match 함수를 사용하려면 ClickHouse가 검색어에서 완전한 토큰을 추출할 수 있어야 합니다. 예시:
예시의 supportsupport, supports, supporting 등에 일치할 수 있습니다. 이러한 쿼리는 부분 문자열 쿌리이며, 텍스트 인덱스로는 속도를 높일 수 없습니다. LIKE 쿼리에서 텍스트 인덱스를 활용하려면 LIKE 패턴을 다음과 같이 다시 작성해야 합니다:
support의 왼쪽과 오른쪽에 있는 공백은 해당 용어를 토큰으로 추출할 수 있게 합니다.

startsWithendsWith

LIKE와 마찬가지로 startsWithendsWith 함수도 검색어에서 완전한 토큰을 추출할 수 있는 경우에만 텍스트 인덱스를 사용할 수 있습니다. 예시:
이 예시에서는 clickhouse만 토큰으로 간주됩니다. supportsupport, supports, supporting 등에 일치할 수 있으므로 토큰이 아닙니다. clickhouse supports로 시작하는 모든 행을 찾으려면 검색 패턴 끝에 공백을 추가하십시오:
마찬가지로 endsWith도 앞에 공백을 넣어 사용해야 합니다:

hasToken and hasTokenOrNull

hasTokenhasTokenOrNull 함수는 지정된 단일 토큰과 일치하는지 확인합니다. 앞서 설명한 함수들과 달리, 이 함수들은 검색어를 토큰화하지 않습니다(입력이 단일 토큰이라고 가정합니다). 예시:
함수 hasTokenhasTokenOrNulltext 인덱스와 함께 사용할 때 가장 높은 성능을 제공하는 함수입니다.

hasAnyTokenshasAllTokens

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

has

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

mapContains

함수 mapContains(별칭: mapContainsKey)은 맵의 키에 있는 단일 토큰과 일치하는지 확인합니다. 예시:

operator[]

접근 operator[] 연산자를 텍스트 인덱스와 함께 사용하면 키와 값을 필터링할 수 있습니다. 예시:
다음 예시에서 텍스트 인덱스와 함께 Array(T)Map(K, V)를 사용하는 예를 확인할 수 있습니다.

텍스트 인덱스의 ArrayMap 지원 예시

Array(String) 인덱싱

간단한 블로깅 플랫폼에서는 작성자가 콘텐츠를 분류하기 위해 게시물에 키워드를 지정합니다. 일반적으로 사용자는 키워드를 클릭하거나 주제를 검색해 관련 콘텐츠를 찾을 수 있습니다. 다음 테이블 정의를 살펴보겠습니다.
텍스트 인덱스가 없으면 특정 키워드(예: clickhouse)가 포함된 게시물을 찾기 위해 모든 항목을 스캔해야 합니다:
플랫폼이 커질수록 쿼리가 모든 행의 keywords 배열을 전부 검사해야 하므로 점점 더 느려집니다. 이 성능 문제를 해결하기 위해 keywords에 텍스트 인덱스를 정의할 수 있습니다. 이렇게 하면 모든 키워드를 사전 처리하는 검색 최적화 구조가 생성되어 즉시 조회할 수 있습니다:
중요: 텍스트 인덱스를 추가한 후에는 기존 데이터에도 인덱스를 다시 빌드해야 합니다:

맵 인덱싱

로깅 시스템에서는 서버 요청 메타데이터를 key-value 쌍으로 저장하는 경우가 많습니다. 운영 팀은 디버깅, 보안 사고 대응, 모니터링을 위해 로그를 효율적으로 검색해야 합니다. 다음 로그 테이블을 살펴보겠습니다:
텍스트 인덱스가 없으면 데이터 검색 시 전체 테이블 스캔이 필요합니다:
  1. rate limiting이 적용된 모든 로그를 찾습니다:
  1. 특정 IP의 모든 logs를 찾습니다:
로그 양이 늘어날수록 이러한 쿼리는 느려집니다. 해결 방법은 Map 키와 값에 텍스트 인덱스를 생성하는 것입니다. 필드 이름이나 속성 타입으로 로그를 찾아야 할 때는 mapKeys를 사용해 텍스트 인덱스를 생성합니다:
속성의 실제 내용에서 검색해야 하는 경우 mapValues를 사용해 텍스트 인덱스를 생성합니다:
중요: 텍스트 인덱스를 추가한 후에는 기존 데이터에 대해 인덱스를 다시 빌드해야 합니다:
  1. 속도 제한이 적용된 모든 요청을 찾습니다:
  1. 특정 IP에서 생성된 모든 로그를 찾습니다:

구현

인덱스 레이아웃

각 텍스트 인덱스는 두 가지 (추상적인) 데이터 구조로 이루어집니다.
  • 각 토큰을 포스팅 리스트에 매핑하는 딕셔너리
  • 그리고 각각이 행 번호 집합을 나타내는 포스팅 리스트 집합입니다.
텍스트 인덱스는 스킵 인덱스이므로 이러한 데이터 구조는 논리적으로 각 인덱스 그래뉼마다 존재합니다. 인덱스를 생성하는 동안 파일 3개가 생성됩니다(파트별). 딕셔너리 블록 파일 (.dct) 인덱스 그래뉼 내 토큰은 정렬된 후, 각각 128개의 토큰을 담는 딕셔너리 블록에 저장됩니다(블록 크기는 매개변수 dictionary_block_size로 설정할 수 있습니다). 딕셔너리 블록 파일(.dct)은 하나의 파트에 있는 모든 인덱스 그래뉼의 모든 딕셔너리 블록으로 구성됩니다. 인덱스 그래뉼 파일 (.idx) 인덱스 그래뉼 파일에는 각 딕셔너리 블록마다 블록의 첫 번째 토큰, 딕셔너리 블록 파일 내 상대 오프셋, 그리고 블록의 모든 토큰에 대한 블룸 필터가 포함됩니다. 이 희소 인덱스 구조는 ClickHouse의 희소 프라이머리 키 인덱스)와 유사합니다. 블룸 필터를 사용하면 검색 중인 토큰이 딕셔너리 블록에 없을 경우 해당 딕셔너리 블록을 미리 건너뛸 수 있습니다. 포스팅 리스트 파일 (.pst) 모든 토큰의 포스팅 리스트는 포스팅 리스트 파일에 순차적으로 배치됩니다. 공간을 절약하면서도 빠른 intersect 및 union 연산을 지원하기 위해 포스팅 리스트는 roaring bitmaps로 저장됩니다. 포스팅 리스트의 카디널리티가 16보다 작으면(매개변수 max_cardinality_for_embedded_postings로 설정 가능) 딕셔너리에 내장됩니다.

직접 읽기

특정 타입의 텍스트 쿼리는 “직접 읽기”라는 최적화를 통해 속도를 크게 높일 수 있습니다. 구체적으로는 SELECT 쿼리가 텍스트 컬럼을 결과로 선택하지 않는 경우 이 최적화를 적용할 수 있습니다. 예시:
ClickHouse의 직접 읽기 최적화는 원본 텍스트 컬럼에 접근하지 않고 텍스트 인덱스(즉, 텍스트 인덱스 조회)만 사용해 쿼리를 처리합니다. 텍스트 인덱스 조회는 읽는 데이터 양이 비교적 적기 때문에 ClickHouse의 일반적인 스킵 인덱스보다 훨씬 빠릅니다(일반적인 스킵 인덱스는 스킵 인덱스 조회를 수행한 다음, 남은 그래뉼을 로드하고 필터링합니다). 직접 읽기는 두 가지 설정으로 제어됩니다:
  • query_plan_direct_read_from_text_index 설정(기본값: 1): 직접 읽기를 기본적으로 활성화할지 지정합니다.
  • use_skip_indexes_on_data_read 설정(기본값: 1): 직접 읽기를 사용하기 위한 또 다른 필수 조건입니다. ClickHouse 데이터베이스에서 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로 쿼리를 실행하십시오. 예를 들어, 직접 읽기가 비활성화된 쿼리는 다음과 같습니다.
반환값
반면 동일한 쿼리를 query_plan_direct_read_from_text_index = 1로 실행하면
반환값
두 번째 EXPLAIN PLAN 출력에는 가상 컬럼 __text_index_<index_name>_<function_name>_<id>이 포함되어 있습니다. 이 컬럼이 있으면 직접 읽기가 사용된 것입니다.

예시: Hacker News 데이터셋

텍스트가 많은 대규모 데이터셋에서 텍스트 인덱스가 성능을 얼마나 개선하는지 살펴보겠습니다. 인기 웹사이트인 Hacker News의 댓글 28.7M행을 사용합니다. 다음은 텍스트 인덱스가 없는 테이블입니다:
S3의 Parquet 파일에 28.7M개의 행이 있습니다 - 이제 이를 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는 텍스트에 지정된 토큰(token) 중 하나 이상이 포함되어 있는지 확인합니다. ‘love’ 또는 ‘ClickHouse’가 포함된 댓글을 검색하겠습니다. 직접 읽기 비활성화(표준 스캔)
직접 읽기 활성화(빠른 인덱스 읽기)
이 일반적인 “OR” 검색에서는 속도 향상이 훨씬 더 극적입니다. 전체 컬럼 스캔을 피하면 쿼리 속도가 거의 89배 빨라집니다(1.329s 대 0.015s).

3. hasAllTokens 사용하기

hasAllTokens는 텍스트에 지정된 모든 token이 포함되어 있는지 확인합니다. ‘love’와 ‘ClickHouse’가 모두 포함된 댓글을 검색하겠습니다. 직접 읽기 비활성화됨 (표준 스캔) 직접 읽기가 비활성화된 경우에도 표준 스킵 인덱스는 여전히 효과적입니다. 28.7M행을 147.46K행으로 줄여 주지만, 여전히 컬럼에서 57.03 MB를 읽어야 합니다.
직접 읽기 활성화(빠른 인덱스 읽기) 직접 읽기는 인덱스 데이터만 사용해 쿼리에 응답하므로, 147.46 KB만 읽습니다.
이 “AND” 검색에서는 직접 읽기 최적화가 일반적인 스킵 인덱스 스캔보다 26배 이상 빠릅니다(0.184초 대 0.007초).

4. 복합 검색: OR, AND, NOT, …

직접 읽기 최적화는 복합 불리언 표현식에도 적용됩니다. 여기서는 ‘ClickHouse’ OR ‘clickhouse’에 대해 대소문자를 구분하지 않는 검색을 수행해 보겠습니다. 직접 읽기 비활성화 (표준 스캔)
직접 읽기 활성화(빠른 인덱스 읽기)
인덱스 결과를 조합하면 직접 읽기 쿼리는 34배 더 빨라지며(0.450초 대비 0.013초), 9.58 GB의 컬럼 데이터를 읽지 않아도 됩니다. 이 경우에는 hasAnyTokens(comment, ['ClickHouse', 'clickhouse']) 구문을 사용하는 것이 더 효율적이며 권장됩니다.

텍스트 인덱스 튜닝

현재 I/O를 줄이기 위해 텍스트 인덱스의 역직렬화된 딕셔너리 블록, 헤더, 포스팅 리스트에 대한 캐시가 제공됩니다. 각각 use_text_index_dictionary_cache, use_text_index_header_cache, use_text_index_postings_cache 설정을 통해 활성화할 수 있습니다. 기본적으로는 비활성화되어 있습니다. 캐시를 구성하려면 다음 서버 설정을 참고하십시오.

서버 설정

딕셔너리 블록 캐시 설정

헤더 캐시 설정

포스팅 리스트 캐시 설정

마지막 수정일 2026년 6월 12일