텍스트 인덱스 생성
텍스트 인덱스는 compatibility 설정과 관계없이 ClickHouse 버전 >= 26.2 이상에서 사용할 수 있습니다.
Query
- String 및 FixedString,
- Array(String) 및 Array(FixedString),
- 맵 (mapKeys 및 mapValues 함수를 사용), 그리고
- JSON (JSONAllPaths 및
JSONAllValues함수를 사용).
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_length와max_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 참조).
splitByString 토크나이저는 분할 구분자를 왼쪽에서 오른쪽 순서로 적용합니다.
이로 인해 모호성이 발생할 수 있습니다.
예를 들어 구분자 문자열이 ['%21', '%']이면 %21abc는 ['abc']로 토큰화되지만, 두 구분자 문자열의 순서를 ['%', '%21']로 바꾸면 ['21abc']가 출력됩니다.
대부분의 경우 더 긴 구분자가 먼저 일치하도록 하는 것이 좋습니다.
일반적으로는 구분자 문자열을 길이 내림차순으로 전달하면 됩니다.
구분자 문자열이 prefix code를 이룬다면 임의의 순서로 전달할 수 있습니다.Query
Response
asciiCJK 토크나이저를 권장합니다.
전처리기 인수(선택 사항). 전처리기는 토큰화 전에 입력 문자열에 적용되는 표현식을 의미합니다.
전처리기 인수의 일반적인 사용 사례는 다음과 같습니다.
- 대소문자를 구분하지 않고 일치하도록 소문자/대문자 변환 또는 case folding을 적용합니다. 예: lower, lowerUTF8, caseFoldUTF8.
- UTF-8 정규화를 적용합니다. 예: normalizeUTF8NFC, normalizeUTF8NFD, normalizeUTF8NFKC, normalizeUTF8NFKD, normalizeUTF8NFKCCasefold, toValidUTF8.
- 악센트처럼 불필요한 문자나 부분 문자열을 제거하거나 변환합니다. 예: extractTextFromHTML, substring, idnaEncode, translate, removeDiacriticsUTF8.
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, [...])는 일치하지 않습니다.
따라서 최적의 사용자 경험을 위해 전처리기 표현식을 사용하는 것을 권장합니다.SETTINGS use_skip_indexes = 0).
예를 들어,
Query
Query
Query
Query
- 불용어(매우 자주 나타나는 토큰) 필터링. “the”, “a”, “is”와 같은 매우 흔한 토큰은 검색 관련성이 거의 없고 인덱스를 불필요하게 키웁니다.
후처리기를 사용해 이를 빈 토큰으로 변환하여 버릴 수 있습니다. 빈 토큰은 무시되며, 즉 인덱스에 추가되지 않습니다.
예시:
if(str IN ('the', 'a', 'an', 'of', 'in', 'is', 'it'), '', str) - 타임스탬프 제거. 로그 줄은 종종
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같은 심각도 단어 제거).
- 후처리기 접근 방식:
- 어간 추출. 각 토큰을 해당 어간으로 매핑하면 같은 어근을 공유하는 형태 변형도 일치하게 되어 검색 재현율이 향상됩니다.
예를 들어 영어 어간 추출에서는 “running”, “runs”, “run”이 모두 “run”으로 어간 추출되므로, 이 변형들 중 어느 것으로 쿼리해도 모두 일치합니다.
ClickHouse는 여러 언어에 대해 내장된 stem 함수를 제공합니다.
예시:
stem(str, 'en') - 대소문자 정규화. 대소문자를 구분하지 않는 일치를 가능하게 하도록 토큰을 소문자 또는 대문자로 변환합니다. 예: lower, lowerUTF8. 소문자화 및 대문자화에는 후처리기보다 전처리기를 권장합니다.
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에만 후처리기가 적용되며, 행 수준 프레디케이트는 계속 원래 컬럼 값과 비교합니다.
- 후처리기 표현식을 사용해 불용어를 제거합니다:
- 후처리기 표현식을 사용하여 타임스탬프를 제거합니다:
- 전처리기 표현식을 사용해 타임스탬프를 제거합니다:
- 전처리기와 후처리기를 결합한 표현식을 사용해 타임스탬프를 제거합니다:
- 후처리기 표현식을 사용해 토큰을 어간화합니다:
=, 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는 정확한 직접 읽기를 사용합니다(후처리기가 있으면 계속 적용됨).
후처리기가 빈 문자열로 매핑하는 검색 토큰은 무시되며, 즉 검색 구문에 없는 것으로 처리됩니다。
¹
LIKE와 match는 나열된 토크나이저에서 힌트로 직접 읽기를 사용하며, 그렇지 않으면 브루트 포스(전체 스캔) 스캔으로 폴백됩니다.
또한 LIKE는 전처리기나 후처리기 없이 splitByNonAlpha 및 array 토크나이저에 대해 *직접 읽기(힌트 없이)*도 지원합니다(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(기본값)으로 설정하십시오. 이 인수를 지정하지 않고 생성된 텍스트 인덱스에는 위치 정보가 저장되지 않습니다.
인덱스 세분화 수준.
텍스트 인덱스는 ClickHouse에서 스킵 인덱스의 한 종류로 구현됩니다.
하지만 다른 스킵 인덱스와 달리 텍스트 인덱스는 무한대로 설정된 세분화 수준(1억)을 사용합니다.
이는 텍스트 인덱스의 테이블 정의에서 확인할 수 있습니다.
예시:
Query
Response
텍스트 인덱스 사용
텍스트 인덱스를 검색할 때는
hasAnyTokens 및 hasAllTokens 함수를 사용하는 것을 권장합니다. 자세한 내용은 아래를 참조하십시오.
이 함수는 사용 가능한 모든 토크나이저와 가능한 모든 전처리기 및 후처리기 표현식에서 작동합니다.
그 외의 지원 함수는 역사적으로 텍스트 인덱스보다 먼저 도입되었기 때문에, 많은 경우 기존 동작을 유지해야 했습니다(예: 전처리기 또는 후처리기 미지원).지원되는 함수
WHERE 절 또는 PREWHERE 절에서 텍스트 함수를 사용하는 경우 텍스트 인덱스를 사용할 수 있습니다:
=
= (equals)은 지정된 검색어 전체와 일치합니다.
예시:
IN
IN (in)은 equals와 비슷하지만, 모든 검색어를 일치 대상으로 합니다.
예시:
텍스트 인덱스에서는
NOT IN (notIn)을 지원하지 않습니다.LIKE 및 match
현재 이러한 함수는 인덱스 토크나이저가
splitByNonAlpha, ngrams 또는 sparseGrams인 경우에만 필터링에 텍스트 인덱스(text index)를 사용합니다.NOT LIKE (notLike)는 텍스트 인덱스에서 지원되지 않습니다.LIKE(like) 및 match 함수를 사용하려면 ClickHouse가 검색어에서 완전한 토큰을 추출할 수 있어야 합니다.
ngrams 토크나이저를 사용하는 인덱스에서는 와일드카드 사이의 검색 문자열 길이가 ngram 길이와 같거나 더 길면 이 조건이 충족됩니다.
splitByNonAlpha 토크나이저를 사용하는 텍스트 인덱스 예시:
support는 support, supports, supporting 등에 일치할 수 있습니다.
이 유형의 쿼리는 부분 문자열(substring) 쿼리이므로 텍스트 인덱스로는 속도를 높일 수 없습니다.
LIKE 쿼리에서 텍스트 인덱스를 활용하려면 LIKE 패턴을 다음과 같이 다시 작성해야 합니다:
support의 좌우 공백은 해당 용어가 token으로 추출되도록 합니다.
다행히 ClickHouse가 inverted index를 활용해 LIKE 쿼리 성능을 크게 높일 수 있는 특별한 경우가 있습니다.
자세한 내용은 LIKE/ILIKE 성능 튜닝 섹션을 참조하십시오.
multiSearchAny and multiMatchAny
LIKE 및 match와 동일한 조건에서 텍스트 인덱스를 사용합니다(위 참조). 즉, ClickHouse가 각 검색 패턴에서 완전한 토큰을 추출할 수 있어야 하며, 검색 패턴 목록은 상수여야 합니다.
검색 패턴 중 하나라도 포함되어 있을 가능성이 있으면 해당 그래뉼(granule)을 읽습니다.
multiMatchAny의 경우, 단일 pattern을 토큰 요구 사항으로 축약할 수 없으면(예: 모든 문서와 일치하는 .*) 텍스트 인덱스를 사용할 수 없으며, 쿼리는 전체 스캔으로 돌아갑니다.
LIKE 및 match와 마찬가지로, 부분 문자열 검색과 정규식 검색은 ngrams 및 sparseGrams 토크나이저에서 가장 잘 동작합니다.
이 토크나이저는 서로 겹치는 문자 n-그램을 인덱싱하므로, 검색 패턴은 부분 문자열로 나타나는 모든 위치에서 인덱스에 존재하는 n-그램으로 분해됩니다. 단어의 중간에서 시작하거나 끝나는 경우도 마찬가지입니다.
따라서 검색 패턴의 길이가 n-그램 크기 이상이면 그대로 사용할 수 있습니다.
ngrams 토크나이저를 사용하는 텍스트 인덱스 예시:
splitByNonAlpha 토크나이저는 완전한 토큰(전체 단어)만 인덱싱합니다.
needle은 단어의 중간에서 시작하거나 끝날 수 있으므로, ClickHouse는 각 needle의 앞뒤 토큰을 삭제합니다. 따라서 인덱스는 완전한 토큰만 사용해 그래뉼을 가지치기할 수 있습니다.
부분 문자열 및 정규 표현식 검색에서 splitByNonAlpha와 함께 인덱스를 사용하려면, 각 needle을 구분 문자(예: 공백)로 감싸 하나 이상의 완전한 토큰이 되도록 하십시오.
splitByNonAlpha 토크나이저를 사용하는 텍스트 인덱스의 예시:
startsWith and endsWith
LIKE와 마찬가지로, startsWith 및 endsWith 함수는 검색어에서 완전한 토큰을 추출할 수 있을 때만 텍스트 인덱스를 사용할 수 있습니다.
ngrams 토크나이저를 사용하는 인덱스에서는 와일드카드 사이의 검색 문자열 길이가 ngram 길이와 같거나 더 길면 이에 해당합니다.
텍스트 인덱스가 후처리기(postprocessor)를 사용하는 경우, 추출된 힌트 토큰이 정규화 후에도 비어 있지 않으면 이러한 함수는 Hint 모드에서도 계속 인덱스를 사용할 수 있습니다. 정규화 과정에서 모든 힌트 토큰이 제거되면 해당 프레디케이트에는 인덱스가 사용되지 않습니다.
splitByNonAlpha 토크나이저를 사용하는 텍스트 인덱스의 예시:
clickhouse만 토큰으로 간주됩니다.
support는 support, supports, supporting 등과 일치할 수 있으므로 토큰이 아닙니다.
clickhouse supports로 시작하는 모든 행을 찾으려면 검색 패턴 끝에 공백을 추가하십시오:
endsWith도 앞에 공백을 넣어 사용해야 합니다:
hasToken
hasToken은 splitByNonAlpha가 아닌 토크나이저 및/또는 전처리기/후처리기 표현식이 있는 텍스트 인덱스에서 조회에 사용할 때 몇 가지 주의할 점이 있습니다.
대신 hasAnyTokens와 hasAllTokens를 사용하는 것이 좋습니다.대소문자를 구분하지 않는 변형인 hasTokenCaseInsensitive와 hasTokenCaseInsensitiveOrNull은 텍스트 인덱스를 인식하지 못합니다. 즉, 텍스트 인덱스가 있는 컬럼에서도 항상 전체 행 스캔으로 실행됩니다. 대소문자를 구분하지 않는 일치가 필요하다면 lower(...) 전처리기 또는 후처리기를 사용하고, 이를 hasToken / hasAllTokens / hasAnyTokens와 함께 조합하십시오.hasAnyTokens 및 hasAllTokens
hasPhrase
hasAllTokens와 달리, hasPhrase는 토큰이 연속된 시퀀스로 나타나야 합니다.
검색 구는 인덱스 컬럼에 구성된 것과 동일한 토크나이저를 사용해 토큰화됩니다.
텍스트 인덱스가 후처리기를 사용하는 경우, 검색 구도 인덱스 lookup 전에 정규화됩니다.
이 함수는 splitByNonAlpha, splitByString, ngrams, asciiCJK 토크나이저 중 하나를 요구합니다.
예시:
has
hasAny 및 hasAll
mapContains
String 컬럼에 대한 equals 함수와 유사합니다.
텍스트 인덱스는 mapKeys(map) 표현식에 생성된 경우에만 사용됩니다.
예시:
mapContainsValue
String 컬럼에 사용하는 equals 함수와 유사합니다.
텍스트 인덱스는 mapValues(map) 표현식에 생성된 경우에만 사용됩니다.
예시:
mapContainsKeyLike and mapContainsValueLike
operator[]
mapKeys(map) 또는 mapValues(map) 표현식에 생성되었거나, 두 표현식 모두에 생성된 경우에만 사용됩니다.
예시:
Array(T) 및 맵(K, V) 유형의 컬럼을 사용하는 예시는 다음과 같습니다.
Array(String) 컬럼 인덱싱
clickhouse)가 포함된 게시물을 찾으려면 모든 항목을 스캔해야 합니다:
keywords 배열을 모두 확인해야 하므로 점점 더 느려집니다.
이 성능 문제를 해결하기 위해 keywords 컬럼에 텍스트 인덱스를 정의합니다:
맵 컬럼 인덱싱
JSON 컬럼 인덱싱
JSON 컬럼에 다음 3가지 방식으로 사용할 수 있습니다:
- 특정 서브컬럼에 대한 인덱스 — 일반 컬럼과 마찬가지로 알려진 JSON 경로에 텍스트 인덱스를 생성합니다. 그러면 해당 경로의 값이 인덱싱됩니다.
- JSONAllPaths를 사용하는 경로 기반 인덱스 — 각 그래뉼에 있는 모든 경로를 인덱싱하여, 쿼리한 경로를 포함할 수 없는 그래뉼을 건너뜁니다.
맵컬럼과 유사합니다. - JSONAllValues를 사용하는 값 기반 인덱스 — 모든 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
'New weather in York')은 토큰 순서가 올바르지 않아 일치하지 않습니다.
3행('weather in New Orleans')은 토큰 'York'를 포함하지 않아 일치하지 않습니다。
성능 최적화
직접 읽기
- Setting query_plan_direct_read_from_text_index (기본값은 true)는 직접 읽기를 전반적으로 활성화할지 지정합니다.
- Setting use_skip_indexes_on_data_read는 ClickHouse 버전 < 26.4에서 직접 읽기를 사용하기 위한 prerequisite였습니다.
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 설정으로 실행하면
__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을 설정해 동일한 쿼리를 실행하면
__text_index_...)이 필터 조건에 추가된 것을 확인할 수 있습니다.
PREWHERE 최적화 덕분에 필터 조건은 계산 복잡도가 낮은 순서대로 적용되는 3개의 개별 결합 조건으로 분해됩니다.
이 쿼리의 적용 순서는 __text_index_..., 그다음 greaterOrEquals(...), 마지막으로 like(...)입니다.
이 순서를 사용하면 텍스트 인덱스와 원래 필터가 스키핑하는 그래뉼보다 더 많은 데이터 그래뉼을 스키핑할 수 있으며, WHERE 절 뒤에 있는 쿼리에서 사용되는 비용이 큰 컬럼을 읽기 전에 이를 수행하므로 읽어야 하는 데이터 양이 더욱 줄어듭니다.
LIKE/ILIKE 쿼리
%<alpha-numeric-characters-without-spaces>%이고 텍스트 인덱스 토크나이저가 splitByNonAlpha 또는 array인 경우, ClickHouse는 역색인을 활용해 LIKE/ILIKE 쿼리 속도를 크게 높입니다. 이를 위해 ClickHouse는 일치하는 패턴을 찾을 때 전체 테이블 스캔 대신 역색인 딕셔너리를 스캔합니다.
이 최적화가 활성화되면 LIKE/ILIKE 쿼리는 전체 테이블 스캔보다 훨씬 빨라집니다. 하지만 패턴이 딕셔너리 토큰 대부분과 일치하면 전체 테이블 스캔보다 성능이 더 나빠질 수 있습니다. 다행히 이를 방지하는 폴백 메커니즘이 있습니다.
이 최적화는 다음 설정으로 제어됩니다:
폴백 메커니즘은 다음 두 가지 설정으로 제어됩니다:
이 최적화는 like 및 ilike 함수만 지원합니다.
캐싱
토큰 캐시 설정
헤더 캐시 설정
포스팅 리스트 캐시 설정
제한 사항
- 토큰 수가 많은 텍스트 인덱스(예: 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자 미만이면 이런 상황이 발생합니다. 실제로는 테이블에 다른 컬럼도 포함되므로 임계값은 이보다 몇 배 더 작아집니다(다른 컬럼의 개수, 유형, 크기에 따라 달라집니다).
텍스트 인덱스와 블룸 필터 기반 인덱스 비교
bloom_filter, ngrambf_v1, tokenbf_v1, sparse_grams)를 사용해 더 빠르게 처리할 수 있지만, 두 방식은 설계와 의도된 사용 사례 측면에서 근본적으로 다릅니다.
블룸 필터 인덱스
- 거짓 양성(false positive)을 발생시킬 수 있는 확률적 데이터 구조를 기반으로 합니다.
- 집합 멤버십 질의에만 응답할 수 있습니다. 즉, 컬럼에 토큰 X가 포함되어 있을 수 있는지, 아니면 X가 확실히 포함되어 있지 않은지만 판단할 수 있습니다.
- 쿼리 실행 중 큰 범위를 스키핑할 수 있도록 그래뉼 수준의 정보를 저장합니다.
- 적절하게 튜닝하기가 어렵습니다(예시는 여기를 참조하십시오).
- 비교적 크기가 작습니다(파트당 수 킬로바이트 또는 수 메가바이트).
- 토큰에 대해 결정적인 역색인을 구축합니다. 인덱스 자체로는 거짓 양성이 발생하지 않습니다.
- 텍스트 검색 워크로드에 특화되어 최적화되어 있습니다.
- 효율적인 용어 조회를 위해 행 수준의 정보를 저장합니다.
- 비교적 크기가 큽니다(파트당 수십에서 수백 메가바이트).
- 고급 토큰화와 전처리를 지원하지 않습니다.
- 여러 토큰을 대상으로 한 검색을 지원하지 않습니다.
- 역색인에서 기대하는 성능 특성을 제공하지 않습니다.
- 토큰화와 전처리를 제공합니다.
hasAllTokens,LIKE,match및 유사한 텍스트 검색 함수를 효율적으로 지원합니다.- 대규모 텍스트 코퍼스에서 훨씬 뛰어난 확장성을 제공합니다.
구현 세부 사항
- 각 토큰을 포스팅 리스트에 매핑하는 딕셔너리와
- 각각이 행 번호 집합을 나타내는 포스팅 리스트 집합입니다.
dictionary_block_size로 구성할 수 있습니다).
딕셔너리 블록 파일(.dct)은 파트 내 모든 인덱스 그래뉼의 모든 딕셔너리 블록으로 구성됩니다.
인덱스 헤더 파일 (.idx)
인덱스 헤더 파일에는 각 딕셔너리 블록의 첫 번째 토큰과, 딕셔너리 블록 파일 내 해당 블록의 상대 오프셋이 저장됩니다.
이 희소 인덱스 구조는 ClickHouse의 희소 프라이머리 키 인덱스)와 유사합니다.
포스팅 리스트 파일 (.pst)
모든 토큰의 포스팅 리스트는 포스팅 리스트 파일에 순차적으로 배치됩니다.
공간을 절약하면서도 빠른 교집합 및 합집합 연산을 지원하기 위해 포스팅 리스트는 roaring bitmaps로 저장됩니다.
포스팅 리스트가 posting_list_block_size보다 크면 여러 블록으로 분할되어 포스팅 리스트 파일에 순차적으로 저장됩니다.
위치 파일 (.pos)
선택 사항이며, 인덱스 인수 support_phrase_search = 1인 경우에만 생성됩니다.
일치하는 행 내에서 토큰의 위치를 저장합니다.
텍스트 인덱스의 머지
데이터 파트가 머지될 때 텍스트 인덱스를 처음부터 다시 생성할 필요는 없습니다. 대신 머지 프로세스의 별도 단계에서 효율적으로 머지할 수 있습니다.
이 단계에서는 각 입력 파트의 텍스트 인덱스에 있는 정렬된 딕셔너리를 읽어 새로운 통합 딕셔너리로 결합합니다.
포스팅 리스트의 행 번호도 초기 머지 단계에서 생성된 이전 행 번호와 새 행 번호 간 매핑을 사용해, 머지된 데이터 파트에서의 새 위치를 반영하도록 다시 계산됩니다.
이러한 텍스트 인덱스 머지 방식은 _part_offset 컬럼이 있는 프로젝션이 머지되는 방식과 유사합니다.
소스 파트에 인덱스가 구체화되어 있지 않으면 인덱스를 생성해 임시 파일에 기록한 다음, 다른 파트의 인덱스 및 다른 임시 인덱스 파일의 인덱스와 함께 머지합니다.
디버깅
테이블 함수 mergeTreeTextIndex를 사용해 텍스트 인덱스를 검사할 수 있습니다.
예시: Hacker News 데이터셋
hackernews 테이블에 삽입해 보겠습니다:
ALTER TABLE을 사용해 comment 컬럼에 텍스트 인덱스를 추가한 후 이를 구체화합니다:
hasToken, hasAnyTokens, hasAllTokens 함수를 사용해 쿼리를 실행해 보겠습니다.
다음 예시에서는 일반적인 인덱스 스캔과 직접 읽기 최적화 사이의 성능 차이가 얼마나 큰지 보여줍니다.
1. hasToken 사용하기
hasToken은 텍스트에 특정 단일 토큰이 포함되어 있는지 확인합니다.
대소문자를 구분하는 토큰인 ‘ClickHouse’를 검색하겠습니다.
직접 읽기 비활성화 (표준 스캔)
기본적으로 ClickHouse는 스킵 인덱스를 사용해 그래뉼을 필터링한 다음, 해당 그래뉼의 컬럼 데이터를 읽습니다.
직접 읽기를 비활성화하면 이 동작을 시뮬레이션할 수 있습니다.
2. hasAnyTokens 사용하기
hasAnyTokens는 텍스트에 지정된 토큰이 하나 이상 포함되어 있는지 확인합니다.
‘love’ 또는 ‘ClickHouse’가 포함된 댓글을 검색해 보겠습니다.
직접 읽기 비활성화 (표준 스캔)
3. hasAllTokens 사용
hasAllTokens는 텍스트에 지정된 모든 토큰이 포함되어 있는지 확인합니다.
‘love’와 ‘ClickHouse’가 모두 포함된 댓글을 검색하겠습니다.
직접 읽기 비활성화(표준 스캔)
직접 읽기가 비활성화되어 있어도 표준 스킵 인덱스는 여전히 효과적입니다.
28.7M행을 147.46K행으로 줄여 주지만, 여전히 컬럼에서 57.03 MB를 읽어야 합니다.
4. 복합 검색: OR, AND, NOT, …
hasAnyTokens(comment, ['ClickHouse', 'clickhouse']) 구문이 더 적합하고 효율적입니다.
- 블로그: ClickHouse 전문 검색 일반 제공 발표
- 블로그: 객체 스토리지를 위한 고성능 전문 검색 구축
- 동영상: ClickHouse 전문 검색 소개
- 동영상: 내부 살펴보기: ClickHouse 규모와 속도에 맞춘 전문 검색
- 발표 자료: ClickHouse 전문 검색의 내부: 빠르고, 네이티브하며, 열 지향적입니다
- 발표 자료: 역방향 데이터베이스 인덱스: 왜 필요한가, 무엇인가, 어떻게 구현하는가, FOSDEM 2026