정확·근사 벡터 검색

다차원(벡터) 공간에서 주어진 점에 대해 가장 가까운 N개의 점을 찾는 문제는 최근접 이웃 탐색(nearest neighbor search) 또는 줄여서 벡터 검색이라고 알려져 있어요.

벡터 검색을 해결하는 두 가지 일반적인 접근 방식이 있어요.

  • 정확 벡터 검색(Exact vector search)은 주어진 점과 벡터 공간의 모든 점 사이의 거리를 계산해요. 이것은 가능한 최고의 정확도를 보장해요. 즉, 반환된 점이 실제 최근접 이웃임을 보장해요. 벡터 공간이 철저히 탐색되므로 정확 벡터 검색은 실제 사용에 너무 느릴 수 있어요
  • 근사 벡터 검색(Approximate vector search)은 정확 벡터 검색보다 훨씬 빠르게 결과를 계산하는 기법들(예: 그래프와 랜덤 포레스트 같은 특수 데이터 구조)을 말해요. 결과 정확도는 일반적으로 실용적 사용에 "충분히 좋아요". 많은 근사 기법은 결과 정확도와 검색 시간 사이의 트레이드오프를 조정하는 매개변수를 제공해요

벡터 검색(정확 또는 근사)은 SQL로 다음과 같이 작성할 수 있어요.

WITH [...] AS reference_vector
SELECT [...]
FROM table
WHERE [...] -- a WHERE clause is optional
ORDER BY <DistanceFunction>(vectors, reference_vector)
LIMIT <N>

벡터 공간의 점은 Array(Float64), Array(Float32), 또는 Array(BFloat16) 타입의 vectors 컬럼에 저장돼요. 실용적 사용을 위해 일반적으로 BFloat16 배열을 권장해요.

참조 벡터는 상수 배열이며 공통 테이블 표현식으로 주어져요. <DistanceFunction>은 참조 점과 저장된 모든 점 사이의 거리를 계산해요. 사용 가능한 거리 함수 중 어떤 것이든 그렇게 사용할 수 있어요. <N>은 몇 개의 이웃을 반환할지 지정해요.

출처: 문서

본문

정확 벡터 검색

정확 벡터 검색은 위 SELECT 쿼리를 그대로 사용해 수행할 수 있어요. 그런 쿼리의 실행 시간은 일반적으로 저장된 벡터 수, 벡터 요소 수(= 차원), 벡터 요소의 비트 폭에 비례해요. 또한 ClickHouse는 모든 벡터를 무차별(브루트포스) 스캔하므로 실행 시간은 쿼리의 스레드 수에도 의존해요 (max_threads 설정 참고).

예시

CREATE TABLE tab
(
    id Int32,
    vec Array(BFloat16)
)
ENGINE = MergeTree
ORDER BY id;

INSERT INTO tab VALUES (0, [1.0, 0.0]), (1, [1.1, 0.0]), (2, [1.2, 0.0]), (3, [1.3, 0.0]), (4, [1.4, 0.0]), (5, [1.5, 0.0]), (6, [0.0, 2.0]), (7, [0.0, 2.1]), (8, [0.0, 2.2]), (9, [0.0, 2.3]), (10, [0.0, 2.4]), (11, [0.0, 2.5]);

WITH [0., 2.] AS reference_vec
SELECT id, vec
FROM tab
ORDER BY L2Distance(vec, reference_vec) ASC
LIMIT 3;

는 다음을 반환해요.

   ┌─id─┬─vec─────┐
1. │  6 │ [0,2]   │
2. │  7 │ [0,2.1] │
3. │  8 │ [0,2.2] │
   └────┴─────────┘

근사 벡터 검색

벡터 유사도 인덱스 (Vector Similarity Indexes)

ClickHouse는 근사 벡터 검색을 수행하기 위한 특별한 "벡터 유사도" 인덱스를 제공해요. 벡터 유사도 인덱스는 ClickHouse 버전 25.8 이상에서 사용할 수 있어요. 문제가 있으면 ClickHouse 저장소에 이슈를 열어주세요.

벡터 유사도 인덱스 생성하기

벡터 유사도 인덱스는 새 테이블에서 다음과 같이 생성할 수 있어요.

CREATE TABLE table
(
    [...],
    vectors Array([Float64|Float32|BFloat16]),
    INDEX <index_name> vectors TYPE vector_similarity(<type>, <distance_function>, <dimensions>) [GRANULARITY <N>]
)
ENGINE = MergeTree
ORDER BY [...]

또는 기존 테이블에 벡터 유사도 인덱스를 추가하려면:

ALTER TABLE table ADD INDEX <index_name> vectors TYPE vector_similarity(<type>, <distance_function>, <dimensions>) [GRANULARITY <N>];

벡터 유사도 인덱스는 특별한 종류의 스킵 인덱스예요 (여기여기 참고). 따라서 위 ALTER TABLE 문은 인덱스가 향후 테이블에 삽입되는 새 데이터에 대해서만 만들어지게 해요. 기존 데이터에도 인덱스를 만들려면 materialize해야 해요.

ALTER TABLE table MATERIALIZE INDEX <index_name> SETTINGS mutations_sync = 2;

함수 <distance_function>은 다음 중 하나여야 해요.

  • L2Distance, 유클리드 거리, 유클리드 공간에서 두 점 사이의 선 길이를 나타냄
  • cosineDistance, 코사인 거리, 두 비영 벡터 사이의 각도를 나타냄
  • dotProduct, 내적(dot product), 두 벡터의 요소별 곱의 합. 정규화된 데이터에서 cosineDistance와 동일

정규화된 데이터에는 L2Distance가 보통 최선의 선택이고, 그렇지 않으면 스케일을 보상하기 위해 cosineDistance가 권장돼요. 거리 함수 L2DistancecosineDistance의 경우 작은 값이 더 높은 유사도를 의미하는 반면, dotProduct의 경우 높은 값이 더 높은 유사도를 의미해요. 결과적으로 L2DistancecosineDistance를 사용하는 벡터 인덱스는 SELECT [...] ORDER BY [...] ASC 쿼리에서만 사용할 수 있고 (ASCORDER BY의 기본값), dotProduct용으로 만들어진 벡터 인덱스는 SELECT [...] ORDER BY [...] DESC 쿼리에서만 사용할 수 있어요.

<dimensions>는 기본 컬럼의 배열 카디널리티(요소 수)를 지정해요. ClickHouse가 인덱스 생성 중 다른 카디널리티의 배열을 찾으면 인덱스는 버려지고 오류가 반환돼요.

선택적 GRANULARITY 매개변수 <N>은 인덱스 그레인(granule)의 크기를 나타내요 (여기 참고). 기본 인덱스 그래놀래티 1을 사용하는 일반 스킵 인덱스와 달리 벡터 유사도 인덱스는 기본 인덱스 그래놀래티로 1억을 사용해요. 이 값은 큰 파트에서도 내부에 몇 개의 인덱스만 생성되도록 보장해요. 인덱스 그래놀래티 변경은 자신이 무엇을 하는지 이해하는 고급 사용자에게만 권장해요 (아래 참고).

벡터 유사도 인덱스는 서로 다른 근사 검색 방법을 수용할 수 있다는 점에서 범용적이에요. 실제 사용되는 방법은 매개변수 <type>으로 지정돼요. 현재 사용 가능한 유일한 방법은 HNSW(학술 논문)로, 계층적 근접 그래프에 기반한 유명하고 최신의 근사 벡터 검색 기법이에요.

type으로 HNSW를 사용하면 사용자가 선택적으로 추가 HNSW 특정 매개변수를 지정할 수 있어요.

CREATE TABLE table
(
    [...],
    vectors Array([Float64|Float32|BFloat16]),
    INDEX index_name vectors TYPE vector_similarity('hnsw', <distance_function>, <dimensions>[, <quantization>, <hnsw_max_connections_per_layer>, <hnsw_candidate_list_size_for_construction>]) [GRANULARITY N]
)
ENGINE = MergeTree
ORDER BY [...]

이 HNSW 특정 매개변수들을 사용할 수 있어요.

  • <quantization>은 근접 그래프의 벡터 양자화를 제어해요. 가능한 값은 f64, f32, f16, bf16, i8, b1이에요. 기본값은 bf16이에요. 이 매개변수는 기본 컬럼의 벡터 표현에 영향을 주지 않는다는 점을 유의해요
  • <hnsw_max_connections_per_layer>는 그래프 노드당 이웃 수, 일명 HNSW 하이퍼파라미터 M을 제어해요. 기본값은 32이에요. 값 0은 기본값을 사용한다는 뜻
  • <hnsw_candidate_list_size_for_construction>는 HNSW 그래프 생성 중 동적 후보 목록의 크기, 일명 HNSW 하이퍼파라미터 ef_construction을 제어해요. 기본값은 128이에요. 값 0은 기본값을 사용한다는 뜻

모든 HNSW 특정 매개변수의 기본값은 대부분의 사용 사례에서 합리적으로 잘 동작해요. 따라서 HNSW 특정 매개변수 커스터마이즈는 권장하지 않아요.

추가 제한이 적용돼요.

  • 벡터 유사도 인덱스는 Array(Float32), Array(Float64), Array(BFloat16) 타입의 컬럼에만 만들 수 있어요. Array(Nullable(Float32))Array(LowCardinality(Float32)) 같은 nullable 및 low-cardinality 부동소수점 배열은 허용되지 않아요
  • 벡터 유사도 인덱스는 단일 컬럼에만 만들어야 해요
  • 벡터 유사도 인덱스는 계산된 표현식(예: INDEX index_name arraySort(vectors) TYPE vector_similarity([...]))에 만들 수 있지만, 그런 인덱스는 나중에 근사 이웃 검색에 사용할 수 없어요
  • 벡터 유사도 인덱스는 기본 컬럼의 모든 배열이 <dimension>-개의 요소를 가져야 해요 - 이는 인덱스 생성 중 확인돼요. 이 요구 사항의 위반을 가능한 한 빨리 감지하기 위해 사용자는 벡터 컬럼에 constraint를 추가할 수 있어요. 예: CONSTRAINT same_length CHECK length(vectors) = 256
  • 마찬가지로 기본 컬럼의 배열 값은 비어 있으면 안 되고 ([]) 기본값(역시 [])을 가져도 안 돼요

저장 및 메모리 소비 추정하기

일반적인 AI 모델(예: 대형 언어 모델, LLMs)과 함께 사용하기 위해 생성된 벡터는 수백 또는 수천 개의 부동소수점 값으로 구성돼요. 따라서 단일 벡터 값은 여러 킬로바이트의 메모리를 소비할 수 있어요. 테이블의 기본 벡터 컬럼에 필요한 저장 공간과 벡터 유사도 인덱스에 필요한 주 메모리를 추정하고 싶은 사용자는 아래 두 공식을 사용할 수 있어요.

테이블의 벡터 컬럼 저장 소비(압축되지 않음):

Storage consumption = Number of vectors * Dimension * Size of column data type

dbpedia 데이터셋 예시:

Storage consumption = 1 million * 1536 * 4 (for Float32) = 6.1 GB

벡터 유사도 인덱스는 검색을 수행하려면 디스크에서 주 메모리로 완전히 로드되어야 해요. 마찬가지로 벡터 인덱스도 완전히 메모리에서 생성된 다음 디스크에 저장돼요. 벡터 인덱스를 로드하는 데 필요한 메모리 소비:

Memory for vectors in the index (mv) = Number of vectors * Dimension * Size of quantized data type
Memory for in-memory graph (mg) = Number of vectors * hnsw_max_connections_per_layer * Bytes_per_node_id (= 4) * Layer_node_repetition_factor (= 2)

Memory consumption: mv + mg

dbpedia 데이터셋 예시:

Memory for vectors in the index (mv) = 1 million * 1536 * 2 (for BFloat16) = 3072 MB
Memory for in-memory graph (mg) = 1 million * 64 * 2 * 4 = 512 MB

Memory consumption = 3072 + 512 = 3584 MB

위 공식은 pre-allocated 버퍼와 캐시 같은 런타임 데이터 구조를 할당하기 위해 벡터 유사도 인덱스가 요구하는 추가 메모리를 고려하지 않아요.

벡터 유사도 인덱스 사용하기

벡터 유사도 인덱스를 사용하려면 compatibility 설정이 ''(기본값) 또는 '25.1' 이상이어야 해요.

벡터 유사도 인덱스는 다음 형태의 SELECT 쿼리를 지원해요.

WITH [...] AS reference_vector
SELECT [...]
FROM table
WHERE [...] -- a WHERE clause is optional
ORDER BY <DistanceFunction>(vectors, reference_vector)
LIMIT <N>

ClickHouse의 쿼리 최적화 프로그램은 위 쿼리 템플릿과 일치시키고 사용 가능한 벡터 유사도 인덱스를 활용하려 시도해요. 쿼리는 SELECT 쿼리의 거리 함수가 인덱스 정의의 거리 함수와 같을 때만 벡터 유사도 인덱스를 사용할 수 있어요.

고급 사용자는 검색 중 후보 목록의 크기를 조정하기 위해 hnsw_candidate_list_size_for_search 설정(일명 HNSW 하이퍼파라미터 "ef_search")에 커스텀 값을 제공할 수 있어요 (예: SELECT [...] SETTINGS hnsw_candidate_list_size_for_search = <value>). 설정의 기본값 256은 대부분의 사용 사례에서 잘 동작해요. 설정 값이 높을수록 더 나은 정확도를 의미하지만 성능은 느려져요.

쿼리가 벡터 유사도 인덱스를 사용할 수 있다면 ClickHouse는 SELECT 쿼리에 제공된 LIMIT <N>이 합리적인 범위 내에 있는지 확인해요. 더 구체적으로 <N>이 기본값 100인 max_limit_for_vector_search_queries 설정의 값보다 크면 오류가 반환돼요. 너무 큰 LIMIT 값은 검색을 느리게 하고 보통 사용 오류를 나타내요.

SELECT 쿼리가 벡터 유사도 인덱스를 사용하는지 확인하려면 쿼리 앞에 EXPLAIN indexes = 1을 붙일 수 있어요. 예를 들어 쿼리

EXPLAIN indexes = 1
WITH [0.462, 0.084, ..., -0.110] AS reference_vec
SELECT id, vec
FROM tab
ORDER BY L2Distance(vec, reference_vec) ASC
LIMIT 10;

은 다음을 반환할 수 있어요.

    ┌─explain─────────────────────────────────────────────────────────────────────────────────────────┐
 1. │ Expression (Project names)                                                                      │
 2. │   Limit (preliminary LIMIT (without OFFSET))                                                    │
 3. │     Sorting (Sorting for ORDER BY)                                                              │
 4. │       Expression ((Before ORDER BY + (Projection + Change column names to column identifiers))) │
 5. │         ReadFromMergeTree (default.tab)                                                         │
 6. │         Indexes:                                                                                │
 7. │           PrimaryKey                                                                            │
 8. │             Condition: true                                                                     │
 9. │             Parts: 1/1                                                                          │
10. │             Granules: 575/575                                                                   │
11. │           Skip                                                                                  │
12. │             Name: idx                                                                           │
13. │             Description: vector_similarity GRANULARITY 100000000                                │
14. │             Parts: 1/1                                                                          │
15. │             Granules: 10/575                                                                    │
    └─────────────────────────────────────────────────────────────────────────────────────────────────┘

이 예시에서 dbpedia 데이터셋의 각 차원 1536인 1백만 벡터가 575개 그레인에 저장되어 있어요. 즉 그레인당 1.7k 행이에요. 쿼리는 10개의 이웃을 요청하고 벡터 유사도 인덱스는 이 10개의 이웃을 10개의 별도 그레인에서 찾아요. 이 10개 그레인은 쿼리 실행 중 읽혀져요.

벡터 유사도 인덱스는 출력에 Skip과 벡터 인덱스의 이름 및 유형(예시에서 idxvector_similarity)이 포함되면 사용된 것이에요. 이 경우 벡터 유사도 인덱스는 네 개의 그레인 중 두 개, 즉 데이터의 50%를 버렸어요. 버려질 수 있는 그레인이 많을수록 인덱스 사용이 더 효과적이 돼요.

인덱스 사용을 강제하려면 force_data_skipping_indices 설정으로 SELECT 쿼리를 실행할 수 있어요 (설정 값으로 인덱스 이름 제공).

포스트 필터링과 프리 필터링

사용자는 SELECT 쿼리에 추가 필터 조건이 있는 WHERE 절을 선택적으로 지정할 수 있어요. ClickHouse는 포스트 필터링 또는 프리 필터링 전략을 사용해 이 필터 조건을 평가해요. 간단히 말해 두 전략 모두 필터가 평가되는 순서를 결정해요.

  • 포스트 필터링(Post-filtering)은 벡터 유사도 인덱스가 먼저 평가되고, 이후 ClickHouse가 WHERE 절에 지정된 추가 필터를 평가한다는 뜻이에요
  • 프리 필터링(Pre-filtering)은 필터 평가 순서가 반대라는 뜻이에요

전략은 서로 다른 트레이드오프가 있어요.

  • 포스트 필터링은 LIMIT <N> 절에 요청된 행 수보다 적게 반환할 수 있다는 일반적인 문제가 있어요. 이 상황은 벡터 유사도 인덱스가 반환한 결과 행 하나 이상이 추가 필터를 만족하지 못할 때 발생해요
  • 프리 필터링은 일반적으로 해결되지 않은 문제예요. 일부 특수 벡터 데이터베이스는 프리 필터링 알고리즘을 제공하지만 대부분의 관계형 데이터베이스(ClickHouse 포함)는 정확 이웃 검색, 즉 인덱스 없는 무차별 스캔으로 빠져요

어떤 전략이 사용되는지는 필터 조건에 따라 달라져요.

추가 필터가 파티션 키의 일부인 경우

추가 필터 조건이 파티션 키의 일부라면 ClickHouse는 파티션 가지치기(partition pruning)를 적용해요. 예를 들어 테이블이 컬럼 year로 범위 파티셔닝되어 있고 다음 쿼리가 실행된다고 해요.

WITH [0., 2.] AS reference_vec
SELECT id, vec
FROM tab
WHERE year = 2025
ORDER BY L2Distance(vec, reference_vec) ASC
LIMIT 3;

ClickHouse는 2025 파티션을 제외한 모든 파티션을 가지치기해요.

추가 필터를 인덱스로 평가할 수 없는 경우

추가 필터 조건을 인덱스(기본 키 인덱스, 스킵 인덱스)로 평가할 수 없다면 ClickHouse는 포스트 필터링을 적용해요.

추가 필터를 기본 키 인덱스로 평가할 수 있는 경우

추가 필터 조건을 기본 키(즉, 기본 키의 접두사를 형성)로 평가할 수 있고,

  • 필터 조건이 파트 내에서 적어도 하나의 행을 제거하면 ClickHouse는 파트 내 "살아남은" 범위에 대해 프리 필터링으로 빠져요
  • 필터 조건이 파트 내에서 행을 제거하지 않으면 ClickHouse는 파트에 대해 포스트 필터링을 수행해요

실용적 사용 사례에서 후자의 경우는 오히려 드물어요.

추가 필터를 스킵 인덱스로 평가할 수 있는 경우

추가 필터 조건을 스킵 인덱스(minmax 인덱스, set 인덱스 등)로 평가할 수 있다면 ClickHouse는 포스트 필터링을 수행해요. 그런 경우 벡터 유사도 인덱스가 다른 스킵 인덱스에 비해 가장 많은 행을 제거할 것으로 예상되므로 먼저 평가돼요.

포스트 필터링 대 프리 필터링에 대한 더 세밀한 제어를 위해 두 설정을 사용할 수 있어요. vector_search_filter_strategy 설정(기본: auto, 위 휴리스틱 구현)을 prefilter로 설정할 수 있어요. 이것은 추가 필터 조건이 극도로 선택적일 때 프리 필터링을 강제하는 데 유용해요.

예를 들어 다음 쿼리는 프리 필터링의 혜택을 받을 수 있어요.

SELECT bookid, author, title
FROM books
WHERE price < 2.00
ORDER BY cosineDistance(book_vector, getEmbedding('Books on ancient Asian empires'))
LIMIT 10

매우 적은 수의 책만 2달러 미만이라고 가정하면, 벡터 인덱스가 반환한 상위 10개 일치가 모두 2달러 이상일 수 있으므로 포스트 필터링은 0행을 반환할 수 있어요. 프리 필터링을 강제하면(쿼리에 SETTINGS vector_search_filter_strategy = 'prefilter' 추가) ClickHouse는 먼저 2달러 미만인 모든 책을 찾은 다음 찾은 책에 대해 무차별 벡터 검색을 실행해요.

위 문제를 해결하기 위한 대안 접근 방식으로 vector_search_index_fetch_multiplier 설정(기본: 1.0, 최대: 1000.0)을 1.0보다 큰 값(예: 2.0)으로 구성할 수 있어요. 벡터 인덱스에서 가져온 최근접 이웃 수에 설정 값을 곱한 다음 그 행들에 추가 필터가 적용되어 LIMIT-개의 행을 반환해요.

예를 들어 배율 3.0으로 다시 조회할 수 있어요.

SELECT bookid, author, title
FROM books
WHERE price < 2.00
ORDER BY cosineDistance(book_vector, getEmbedding('Books on ancient Asian empires'))
LIMIT 10
SETTING vector_search_index_fetch_multiplier = 3.0;

ClickHouse는 각 파트에서 벡터 인덱스로부터 3.0 x 10 = 30개의 최근접 이웃을 가져온 다음 추가 필터를 평가해요. 가장 가까운 10개의 이웃만 반환돼요. vector_search_index_fetch_multiplier를 설정하면 문제를 완화할 수 있지만 극단적인 경우(매우 선택적인 WHERE 조건)에는 여전히 요청된 N개보다 적은 행이 반환될 수 있다는 점을 유의해요.

리스코어링(Rescoring)

ClickHouse의 스킵 인덱스는 일반적으로 그레인 레벨에서 필터링해요. 즉, 스킵 인덱스에서의 조회는 (내부적으로) 잠재적으로 일치하는 그레인 목록을 반환해 이후 스캔에서 읽는 데이터 수를 줄여요. 이것은 일반적인 스킵 인덱스에는 잘 동작하지만 벡터 유사도 인덱스의 경우 "그래놀래티 불일치"를 만들어요. 더 자세히 말하면 벡터 유사도 인덱스는 주어진 참조 벡터에 대해 가장 유사한 N개 벡터의 행 번호를 결정해요. vector_search_with_rescoring = 1 설정으로 ClickHouse는 후보 행에 대한 원래 전체 정밀도 벡터를 읽고 일반 SQL 파이프라인에서 최종 거리를 계산해요. 쿼리 계획이 허용하면 ClickHouse는 최종 거리 계산 전에 스캔을 벡터 인덱스가 반환한 후보 행으로 필터링해요. 이 단계를 리스코어링이라고 하며, 특히 양자화된 벡터 인덱스에서 정확도를 향상시킬 수 있어요. 최종 순위가 인덱스 거리 대신 저장된 벡터를 사용하기 때문이에요.

추가 필터가 후보를 너무 많이 제거하거나 더 많은 recall이 필요하면 vector_search_index_fetch_multiplier를 증가시켜 벡터 인덱스가 리스코어링을 위해 더 많은 후보 행을 반환하게 해요.

따라서 ClickHouse는 리스코어링을 비활성화하고 인덱스에서 직접 가장 유사한 벡터와 그 거리를 반환하는 최적화를 제공해요. 이 최적화는 기본적으로 활성화되며 vector_search_with_rescoring 설정을 참고해요.

높은 수준으로 동작 방식은 ClickHouse가 가장 유사한 벡터와 그 거리를 가상 컬럼 _distance로 제공하는 것이에요. 이것을 보려면 EXPLAIN header = 1로 벡터 검색 쿼리를 실행해요.

EXPLAIN header = 1
WITH [0., 2.] AS reference_vec
SELECT id
FROM tab
ORDER BY L2Distance(vec, reference_vec) ASC
LIMIT 3
SETTINGS vector_search_with_rescoring = 0
Query id: a2a9d0c8-a525-45c1-96ca-c5a11fa66f47

    ┌─explain────────────────────────────────────────────────────────────────────────────────────────────────────┐
 1. │ Expression (Project names)                                                                                 │
 2. │ Header: id Int32                                                                                           │
 3. │   Limit (preliminary LIMIT (without OFFSET))                                                               │
 4. │   Header: L2Distance(__table1.vec, _CAST([0., 2.]_Array(BFloat16), 'Array(BFloat16)'_String)) BFloat16     │
 5. │           __table1.id Int32                                                                                │
 6. │     Sorting (Sorting for ORDER BY)                                                                         │
 7. │     Header: L2Distance(__table1.vec, _CAST([0., 2.]_Array(BFloat16), 'Array(BFloat16)'_String)) BFloat16   │
 8. │             __table1.id Int32                                                                              │
 9. │       Expression ((Before ORDER BY + (Projection + Change column names to column identifiers)))            │
10. │       Header: L2Distance(__table1.vec, _CAST([0., 2.]_Array(BFloat16), 'Array(BFloat16)'_String)) BFloat16 │
11. │               __table1.id Int32                                                                            │
12. │         ReadFromMergeTree (default.tab)                                                                    │
13. │         Header: id Int32                                                                                   │
14. │                 _distance BFloat16                                                                         │
    └────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

리스코어링 없이 실행된 쿼리(vector_search_with_rescoring = 0)와 병렬 복제본이 활성화된 쿼리는 리스코어링으로 빠질 수 있어요.

성능 튜닝

압축 튜닝

거의 모든 사용 사례에서 기본 컬럼의 벡터는 밀집하고 잘 압축되지 않아요. 결과적으로 compression은 벡터 컬럼으로/에서의 삽입과 읽기를 느리게 해요. 따라서 압축을 비활성화할 것을 권장해요. 그렇게 하려면 벡터 컬럼에 CODEC(NONE)을 다음과 같이 지정해요.

CREATE TABLE tab
(
    id Int32,
    vec Array(BFloat16) CODEC(NONE),
    INDEX idx vec TYPE vector_similarity('hnsw', 'L2Distance', 2)
)
ENGINE = MergeTree ORDER BY id;

인덱스 생성 튜닝

벡터 유사도 인덱스의 수명 주기는 파트의 수명 주기와 연결돼요. 즉, 정의된 벡터 유사도 인덱스가 있는 새 파트가 생성될 때마다 인덱스도 생겨요. 이것은 일반적으로 데이터가 삽입되거나 병합 중에 발생해요. 불행히도 HNSW는 긴 인덱스 생성 시간으로 알려져 있어 삽입과 병합을 크게 느리게 할 수 있어요. 벡터 유사도 인덱스는 데이터가 불변이거나 거의 변하지 않을 때만 사용되는 것이 이상적이에요.

인덱스 생성을 빠르게 하기 위해 다음 기법을 사용할 수 있어요.

첫째, 인덱스 생성을 병렬화할 수 있어요. 최대 인덱스 생성 스레드 수는 서버 설정 max_build_vector_similarity_index_thread_pool_size로 구성할 수 있어요. 최적 성능을 위해 설정 값은 CPU 코어 수로 구성해야 해요.

둘째, INSERT 문을 빠르게 하기 위해 사용자는 세션 설정 materialize_skip_indexes_on_insert를 사용해 새로 삽입된 파트에서 스킵 인덱스 생성을 비활성화할 수 있어요. 그런 파트의 SELECT 쿼리는 정확 검색으로 빠져요. 삽입된 파트는 전체 테이블 크기에 비해 작은 경향이 있으므로 성능 영향은 무시할 만할 것으로 예상돼요.

셋째, 병합을 빠르게 하기 위해 사용자는 세션 설정 materialize_skip_indexes_on_merge를 사용해 병합된 파트에서 스킵 인덱스 생성을 비활성화할 수 있어요. 이것은 문 ALTER TABLE […] MATERIALIZE INDEX […]와 함께 벡터 유사도 인덱스의 수명 주기에 대한 명시적 제어를 제공해요. 예를 들어 인덱스 생성을 모든 데이터가 수집될 때까지 또는 주말 같은 시스템 부하가 낮은 기간까지 연기할 수 있어요.

인덱스 사용 튜닝

SELECT 쿼리는 벡터 유사도 인덱스를 주 메모리에 로드해 사용해야 해요. 같은 벡터 유사도 인덱스가 주 메모리에 반복적으로 로드되는 것을 피하기 위해 ClickHouse는 그런 인덱스를 위한 전용 인메모리 캐시를 제공해요. 이 캐시가 클수록 불필요한 로드가 더 적게 발생해요. 최대 캐시 크기는 서버 설정 vector_similarity_index_cache_size로 구성할 수 있어요. 기본적으로 캐시는 최대 5GB까지 커질 수 있어요.

다음 로그 메시지(system.text_log)는 벡터 유사도 인덱스가 로드되고 있음을 나타내요. 다른 벡터 검색 쿼리에 대해 이런 메시지가 반복적으로 나타나면 캐시 크기가 너무 낮다는 것을 나타내요.

2026-02-03 07:39:10.351635 [1386] f0ac5c85-1b1c-4f35-8848-87a1d1aa00ba : VectorSimilarityIndex Start loading vector similarity index

<...>

2026-02-03 07:40:25.217603 [1386] f0ac5c85-1b1c-4f35-8848-87a1d1aa00ba : VectorSimilarityIndex Loaded vector similarity index: max_level = 2, connectivity = 64, size = 1808111, capacity = 1808111, memory_usage = 8.00 GiB, bytes_per_vector = 4096, scalar_words = 1024, nodes = 1808111, edges = 51356964, max_edges = 233395072

벡터 유사도 인덱스 캐시는 벡터 인덱스 그레인을 저장해요. 개별 벡터 인덱스 그레인이 캐시 크기보다 크면 캐시되지 않아요. 따라서 벡터 인덱스 크기("Estimating storage and memory consumption" 또는 system.data_skipping_indices의 공식 기반)를 계산하고 캐시를 그에 맞게 크기 조정해야 해요.

벡터 인덱스 캐시를 검증하고 필요시 늘리는 것이 느린 벡터 검색 쿼리를 조사할 때 첫 단계가 되어야 한다고 다시 강조해요.

벡터 유사도 인덱스 캐시의 현재 크기는 system.metrics에 표시돼요.

SELECT metric, value
FROM system.metrics
WHERE metric = 'VectorSimilarityIndexCacheBytes'

쿼리 id가 있는 쿼리에 대한 캐시 히트와 미스는 system.query_log에서 얻을 수 있어요.

SYSTEM FLUSH LOGS query_log;

SELECT ProfileEvents['VectorSimilarityIndexCacheHits'], ProfileEvents['VectorSimilarityIndexCacheMisses']
FROM system.query_log
WHERE type = 'QueryFinish' AND query_id = '<...>'
ORDER BY event_time_microseconds;

프로덕션 사용 사례의 경우 모든 벡터 인덱스가 항상 메모리에 남아 있도록 캐시를 충분히 크게 조정할 것을 권장해요.

양자화 튜닝

양자화(Quantization)는 벡터의 메모리 풋프린트와 벡터 인덱스 구축·탐색의 계산 비용을 줄이는 기법이에요. ClickHouse 벡터 인덱스는 다음 양자화 옵션을 지원해요.

Quantization Name Storage per dimension
f32 Single precision 4 bytes
f16 Half precision 2 bytes
bf16 (default) Half precision (brain float) 2 bytes
i8 Quarter precision 1 byte
b1 Binary 1 bit

양자화는 원래 전체 정밀도 부동소수점 값(f32)을 검색하는 것과 비교해 벡터 검색의 정밀도를 줄여요. 그러나 대부분의 데이터셋에서 반정밀 brain float 양자화(bf16)는 무시할 만한 정밀도 손실을 가져오므로 벡터 유사도 인덱스는 이 양자화 기법을 기본적으로 사용해요.

쿼터 정밀도(i8)와 이진(b1) 양자화는 벡터 검색에서 상당한 정밀도 손실을 일으켜요. 우리는 벡터 유사도 인덱스 크기가 사용 가능한 DRAM 크기보다 훨씬 클 때만 두 양자화를 모두 권장해요. 이 경우에도 정확도를 개선하기 위해 리스코어링(vector_search_index_fetch_multiplier, vector_search_with_rescoring)을 활성화할 것을 제안해요.

이진 양자화는 1) 정규화된 임베딩(즉, 벡터 길이 = 1, OpenAI 모델은 보통 정규화됨), 2) 코사인 거리를 거리 함수로 사용하는 경우에만 권장돼요. 이진 양자화는 내부적으로 근접 그래프를 구축하고 검색하는 데 Hamming 거리를 사용해요. 리스코어링 단계는 테이블에 저장된 원래 전체 정밀도 벡터를 사용해 코사인 거리로 최근접 이웃을 식별해요.

데이터 전송 튜닝

벡터 검색 쿼리의 참조 벡터는 사용자가 제공하며 일반적으로 대형 언어 모델(LLM)을 호출해 검색해요. ClickHouse에서 벡터 검색을 실행하는 일반적인 Python 코드는 다음과 같을 수 있어요.

search_v = openai_client.embeddings.create(input = "[Good Books]", model='text-embedding-3-large', dimensions=1536).data[0].embedding

params = {'search_v': search_v}
result = chclient.query(
   "SELECT id FROM items
    ORDER BY cosineDistance(vector, %(search_v)s)
    LIMIT 10",
    parameters = params)

임베딩 벡터(위 스니펫의 search_v)는 매우 큰 차원을 가질 수 있어요. 예를 들어 OpenAI는 1536 또는 심지어 3072 차원의 임베딩 벡터를 생성하는 모델을 제공해요. 위 코드에서 ClickHouse Python 드라이버는 임베딩 벡터를 사람이 읽을 수 있는 문자열로 대체한 다음 SELECT 쿼리를 통째로 문자열로 전송해요. 임베딩 벡터가 1536개의 단정밀도 부동소수점 값으로 구성된다고 가정하면 전송된 문자열은 20kB 길이에 도달해요. 이것은 토큰화, 파싱, 수천 개의 문자열-부동소수점 변환에 높은 CPU 사용을 만든다.

또한 ClickHouse 서버 로그 파일에 상당한 공간이 필요해 system.query_log도 부풀릴 수 있어요. 대부분의 LLM 모델은 임베딩 벡터를 원시 부동소수점의 목록이나 NumPy 배열로 반환한다는 점을 유의해요. 따라서 Python 애플리케이션은 다음 스타일을 사용해 참조 벡터 매개변수를 이진 형태로 바인딩할 것을 권장해요.

search_v = openai_client.embeddings.create(input = "[Good Books]", model='text-embedding-3-large', dimensions=1536).data[0].embedding

params = {'$search_v_binary$': np.array(search_v, dtype=np.float32).tobytes()}
result = chclient.query(
   "SELECT id FROM items
    ORDER BY cosineDistance(vector, reinterpret($search_v_binary$, 'Array(Float32)'))
    LIMIT 10"
    parameters = params)

예시에서 참조 벡터는 이진 형태로 그대로 전송되고 서버에서 부동소수점 배열로 재해석돼요. 이것은 서버 측 CPU 시간을 절약하고 서버 로그와 system.query_log의 부풀림을 피해요.

관리 및 모니터링

벡터 유사도 인덱스의 디스크 크기는 system.data_skipping_indices에서 얻을 수 있어요.

SELECT database, table, name, formatReadableSize(data_compressed_bytes)
FROM system.data_skipping_indices
WHERE type = 'vector_similarity';

예시 출력:

┌─database─┬─table─┬─name─┬─formatReadab⋯ssed_bytes)─┐
│ default  │ tab   │ idx  │ 348.00 MB                │
└──────────┴───────┴──────┴──────────────────────────┘

일반 스킵 인덱스와의 차이점

모든 일반 스킵 인덱스와 마찬가지로 벡터 유사도 인덱스는 그레인 위에 구축되고 각 인덱스 블록은 GRANULARITY = [N]-개의 그레인(일반 스킵 인덱스의 경우 [N] = 1 기본)으로 구성돼요. 예를 들어 테이블의 기본 인덱스 그래놀래티가 8192(index_granularity = 8192 설정)이고 GRANULARITY = 2라면 각 인덱스 블록은 16384행을 포함해요.

그러나 근사 이웃 검색을 위한 데이터 구조와 알고리즘은 본래 행 지향적이에요. 그것들은 행 집합의 압축 표현을 저장하고 벡터 검색 쿼리에 대해 행도 반환해요. 이것은 벡터 유사도 인덱스가 일반 스킵 인덱스와 비교해 다소 직관적이지 않은 차이를 일으켜요.

사용자가 컬럼에 벡터 유사도 인덱스를 정의하면 ClickHouse는 내부적으로 각 인덱스 블록에 대해 벡터 유사도 "하위 인덱스"를 만들어요. 하위 인덱스는 자신을 포함하는 인덱스 블록의 행만 안다는 점에서 "로컬"이에요. 앞선 예시에서 컬럼이 65536행을 가진다 가정하면, 우리는 네 개의 인덱스 블록(8개 그레인에 걸쳐)을 얻고 각 인덱스 블록에 대한 벡터 유사도 하위 인덱스를 얻어요.

하위 인덱스는 이론적으로 자신의 인덱스 블록 안에서 N개의 가장 가까운 점을 가진 행을 직접 반환할 수 있어요. vector_search_with_rescoring = 1 쿼리의 경우 ClickHouse는 쿼리 계획이 이 최적화를 허용할 때 저장된 벡터에서 최종 거리를 계산하기 전에 행을 필터링하기 위해 이 행 위치를 사용할 수 있어요. 리스코어링 없이 ClickHouse는 가상 컬럼 _distance를 통해 벡터 인덱스의 거리를 직접 사용해요.

두 모드 모두 여전히 주변 그레인 범위를 사용해 읽기를 스케줄하는데, 이는 인덱스 블록의 그래놀래티로 데이터를 건너뛰는 일반 스킵 인덱스와 다르다. GRANULARITY 매개변수는 몇 개의 벡터 유사도 하위 인덱스가 생성되는지 결정해요. 더 큰 GRANULARITY 값은 더 적지만 더 큰 벡터 유사도 하위 인덱스를 의미하며, 컬럼(또는 컬럼의 데이터 파트)이 단일 하위 인덱스만 가질 때까지 늘어나요. 그 경우 하위 인덱스는 모든 컬럼 행에 대한 "전역" 뷰를 가지며 관련 행이 있는 컬럼(파트)의 모든 그레인을 직접 반환할 수 있어요 (그런 그레인은 기껏해야 LIMIT [N]-개). vector_search_with_rescoring = 1이면 ClickHouse는 일치하는 행 위치를 읽고 그 행들에 대한 정확한 거리를 계산할 수 있어요.

작은 GRANULARITY 값으로 각 하위 인덱스는 최대 LIMIT N개의 후보 행을 반환할 수 있어요. 결과적으로 더 많은 후보 행을 읽고 포스트 필터링해야 할 수 있어요. 두 경우 모두 검색 정확도는 동등하게 좋고 처리 성능만 다르다는 점을 유의해요. 일반적으로 벡터 유사도 인덱스에는 큰 GRANULARITY를 사용하고, 벡터 유사도 구조의 과도한 메모리 소비 같은 문제가 있을 때만 더 작은 GRANULARITY 값으로 빠지는 것이 권장돼요. 벡터 유사도 인덱스에 GRANULARITY가 지정되지 않으면 기본값은 1억이에요.

예시

쿼리:

Query

CREATE TABLE tab
(
    id Int32,
    vec Array(BFloat16),
    INDEX idx vec TYPE vector_similarity('hnsw', 'L2Distance', 2)
)
ENGINE = MergeTree
ORDER BY id;

INSERT INTO tab VALUES (0, [1.0, 0.0]), (1, [1.1, 0.0]), (2, [1.2, 0.0]), (3, [1.3, 0.0]), (4, [1.4, 0.0]), (5, [1.5, 0.0]), (6, [0.0, 2.0]), (7, [0.0, 2.1]), (8, [0.0, 2.2]), (9, [0.0, 2.3]), (10, [0.0, 2.4]), (11, [0.0, 2.5]);

WITH [0., 2.] AS reference_vec
SELECT id, vec
FROM tab
ORDER BY L2Distance(vec, reference_vec) ASC
LIMIT 3;

Response

   ┌─id─┬─vec─────┐
1. │  6 │ [0,2]   │
2. │  7 │ [0,2.1] │
3. │  8 │ [0,2.2] │
   └────┴─────────┘

근사 벡터 검색을 사용하는 추가 예시 데이터셋:

양자화 코덱을 사용한 벡터 검색

Quantized 코덱을 활성화하려면 먼저 SET enable_quantized_codec = 1을 실행해요. 문제가 있으면 ClickHouse 저장소에 이슈를 열어주세요.

소개

벡터 유사도 인덱스는 그래프를 탐색해 최근접 이웃 쿼리에 답하며 그래프를 메모리에 유지할 수 있을 때 매우 잘 동작해요. 두 가지 조건이 그것의 적용 가능성을 제한해요.

  • 높은 구축 비용. 벡터 유사도 인덱스 구축은 비싼 과정이에요
  • 높은 메모리 소비. 벡터 유사도 인덱스는 상당한 메모리를 소비하며, 벡터 자체에 추가로 지배적인 비용이 돼요
  • 필터링. 선택적인 WHERE 필터로 그래프 탐색은 비효율적이 돼요. 조건을 만족하는 작은 행 집합에 도달할 수 없거나, 그것을 찾기 위해 불균형한 수의 후보를 조사해야 하기 때문이에요

철저한 스캔은 두 제한 모두 받지 않아요: 보조 데이터 구조가 필요 없고, 파트는 단순히 연결함으로써 자연스럽게 병합되며, 필터링은 단순히 스캔할 행 수를 줄일 뿐이에요. 철저한 스캔의 가장 큰 단점은 읽어야 하는 데이터 볼륨이 크다는 것이에요: 전체 Float32 또는 BFloat16 정밀도로 저장된 벡터에 대한 스캔은 전체 벡터 컬럼을 디스크에서 로드해야 하므로 저장 I/O가 지배적이에요.

Quantized 컬럼 코덱은 이 단점을 해결해요. 각 벡터를 두 번 저장해요: 원래 전체 정밀도 값과 압축된 양자화 표현. 벡터 검색 쿼리는 먼저 저렴하고 SIMD 친화적인 거리 함수를 사용해 양자화 코드를 스캔해 가장 유망한 결과 후보의 숏리스트(shortlist)를 만들고, 두 번째 단계에서 전체 정밀도 벡터에 대해 이 후보들을 다시 순위를 매겨요. 양자화 코드에 대한 초기 스캔은 원래 벡터에 대한 스캔이 하는 것보다 저장소에서 훨씬 적은 바이트를 읽어요.

이 코덱은 ClickHouse에 잘 맞는데, 비싼 부분(스캔)이 정확히 ClickHouse 엔진이 잘하도록 만들어진 것이기 때문이에요.

  • 벡터화(Vectorized). 스캔 커널은 SIMD 명령을 활용해 CPU 소비를 줄여요
  • 코어와 파트에 걸쳐 병렬로. 스캔은 사소하게 병렬이에요: 거리는 모든 사용 가능한 스레드와 테이블의 모든 파트에 걸쳐 한 번에 계산돼요
  • 분산(Distributed). 샤딩된 클러스터에서 작업은 머신들로 분산돼요 - 각 샤드는 자신의 조각을 병렬로 스캔하고 코디네이터가 숏리스트를 병합해요
  • 추가 구축 비용 없음. 양자화 코드는 벡터가 쓰여질 때 생성돼요. 구축, 튜닝, 재구축할 추가 인덱스가 없으므로 테이블은 데이터가 도착하자마자 검색할 준비가 돼요

코덱 적용

Array(Float32)(또는 Array(Float64) / Array(BFloat16)) 컬럼에 Quantized(...) 코덱을 지정해요.

SET enable_quantized_codec = 1;

CREATE TABLE vectors
(
    id UInt32,
    vec Array(BFloat16) CODEC(Quantized('rabitq', 1536))
)
ENGINE = MergeTree
ORDER BY ...;

코덱은 ALTER TABLE로 나중에 추가하거나 변경할 수 없어요.

양자화 방법

각 양자화 방법은 서로 다른 크기/정확도 트레이드오프가 있어요. dimensions 인자는 벡터 길이예요.

  • Quantized('rabitq', dimensions) — 좌표당 1개의 부호 비트와 편향되지 않은 코사인 보정 인자(dimensions/8 + 4 바이트). 작고 popcount-저렴한 강력한 기본값. cosineDistance
  • Quantized('turboquant', dimensions) — 좌표당 2비트(1비트 MSE 코드와 1비트 잔차 코드)로 더 높은 충실도의 후보(dimensions/4 + 4 바이트). cosineDistance
  • Quantized('int8', dimensions) — 좌표당 하나의 Int8 코드와 벡터 노름(dimensions + 4 바이트); 가장 크지만 가장 충실한 평면 코드. L2DistancecosineDistance 지원
  • Quantized('prefix', dimensions, leading_dimensions, 'int8'|'bf16') — Matryoshka: Int8(벡터당 스케일 포함) 또는 BFloat16으로 leading_dimensions개의 앞 좌표만 유지. Matryoshka Representation Learning으로 훈련된 임베딩을 위한 아주 작은 코드. L2DistancecosineDistance 지원
  • Quantized('product', dimensions, nbits, m) — Product Quantization: k-means로 훈련된 per-part 코드북; 각 벡터는 nbits 비트의 m개 코드가 됨 (따라서 dimensionsm의 배수여야 함). 가장 압축적인 옵션이고 바이트당 최고 recall이지만, 삽입 중 훈련 단계 비용이 있음. L2DistancecosineDistance 지원

rabitqturboquantdimensions가 8의 배수여야 해요.

코덱 사용

코덱은 일반적인 상위-k 쿼리 형태에 자동으로 사용돼요 (exact search 참고).

WITH [...] AS reference_vec
SELECT id
FROM vectors
ORDER BY cosineDistance(vec, reference_vec) ASC
LIMIT 10
SETTINGS vector_search_use_quantized_codes = 1;

vector_search_use_quantized_codes = 1 설정은 최적화 프로그램이 쿼리를 2단계 스캔으로 다시 쓰게 해요. 설정은 기본적으로 꺼져 있어요. 그것 없이 같은 쿼리는 원래 벡터에 대한 일반 정확 스캔으로 실행돼요.

모든 방법이 거리 함수 cosineDistance를 지원하고, int8, prefix, product 방법은 추가로 L2Distance를 거리 함수로 지원해요. vector_search_index_fetch_multiplier 설정은 쿼리의 LIMIT에 상대적으로 몇 개의 후보를 숏리스트할지 지정해요. 더 큰 배율은 더 많은 리스코어링 비용으로 recall을 개선해요. 기본값은 1(오버샘플링 없음), 좋은 recall을 위해 (예: 10으로) 올리면 보통 필요해요.

Quantized Bit (QBit)

정확 벡터 검색을 빠르게 하는 한 가지 일반적인 접근은 더 낮은 정밀도의 float 데이터 타입을 사용하는 것이에요. 예를 들어 벡터가 Array(Float32) 대신 Array(BFloat16)으로 저장되면 데이터 크기는 절반으로 줄고 쿼리 실행 시간도 비례해 줄어들 것으로 예상돼요. 이 방법은 양자화(quantization)로 알려져 있어요. 계산을 빠르게 하지만, 모든 벡터의 철저한 스캔을 수행함에도 결과 정확도를 줄일 수 있어요.

전통적인 양자화로 우리는 검색 중과 데이터 저장 중 둘 다에서 정밀도를 잃어요. 위 예시에서 Float32 대신 BFloat16을 저장하므로, 나중에 원하면 더 정확한 검색을 결코 수행할 수 없어요. 한 가지 대안은 양자화된 것과 전체 정밀도의 두 가지 데이터 복사본을 저장하는 것이에요. 이것은 동작하지만 중복 저장이 필요해요. 원본 데이터로 Float64를 갖고 다른 정밀도(16비트, 32비트, 또는 전체 64비트)로 검색을 실행하고 싶은 시나리오를 고려해보세요. 우리는 데이터의 세 개의 별도 복사본을 저장해야 할 거예요.

ClickHouse는 다음으로 이러한 제한을 해결하는 Quantized Bit(QBit) 데이터 타입을 제공해요.

  • 원래 전체 정밀도 데이터를 저장
  • 양자화 정밀도를 쿼리 시점에 지정할 수 있게 허용

이것은 데이터를 비트 그룹화 형식으로 저장해 (즉, 모든 벡터의 모든 i번째 비트가 함께 저장됨) 요청된 정밀도 수준에서만 읽을 수 있게 해 달성돼요. 양자화의 줄어든 I/O와 계산의 속도 이점을 얻으면서도 필요할 때 모든 원본 데이터를 사용할 수 있어요. 최대 정밀도가 선택되면 검색은 정확해져요.

QBit 타입의 컬럼을 선언하려면 다음 문법을 사용해요.

column_name QBit(element_type, dimension[, stride])

여기서:

  • element_type – 각 벡터 요소의 타입. 지원 타입은 Int8, BFloat16, Float32, Float64
  • dimension – 각 벡터의 요소 수
  • stride – 선택. dimension의 약수로, 차원을 별도의 스트림에 저장된 dimension / stride개의 연속 그룹으로 나눠, 앞쪽 차원만 검색하면 더 적은 스트림을 읽게 해요 (Matryoshka 임베딩에 유용). 기본값은 dimension이며, 이 경우 타입은 non-strided QBit와 바이트 동일해요. 자세한 내용은 QBit 데이터 타입 페이지를 참고해요

QBit 테이블 생성 및 데이터 추가

CREATE TABLE fruit_animal (
    word String,
    vec QBit(Float64, 5)
) ENGINE = MergeTree
ORDER BY word;

INSERT INTO fruit_animal VALUES
    ('apple', [-0.99105519, 1.28887844, -0.43526649, -0.98520696, 0.66154391]),
    ('banana', [-0.69372815, 0.25587061, -0.88226235, -2.54593015, 0.05300475]),
    ('orange', [0.93338752, 2.06571317, -0.54612565, -1.51625717, 0.69775337]),
    ('dog', [0.72138876, 1.55757105, 2.10953259, -0.33961248, -0.62217325]),
    ('cat', [-0.56611276, 0.52267331, 1.27839863, -0.59809804, -1.26721048]),
    ('horse', [-0.61435682, 0.48542571, 1.21091247, -0.62530446, -1.33082533]);

QBit로 벡터 검색

'lemon'이라는 단어를 나타내는 벡터의 최근접 이웃을 L2 거리로 찾아봐요. 거리 함수의 세 번째 매개변수는 비트 단위 정밀도를 지정해요 - 값이 높을수록 더 정확하지만 더 많은 계산이 필요해요. QBit에 대한 모든 사용 가능한 거리 함수는 여기에서 찾을 수 있어요.

전체 정밀도 검색 (64-bit):

SELECT
    word,
    L2DistanceTransposed(vec, [-0.88693672, 1.31532824, -0.51182908, -0.99652702, 0.59907770], 64) AS distance
FROM fruit_animal
ORDER BY distance;
   ┌─word───┬────────────distance─┐
1. │ apple  │ 0.14639757188169716 │
2. │ banana │   1.998961369007679 │
3. │ orange │   2.039041552613732 │
4. │ cat    │   2.752802631487914 │
5. │ horse  │  2.7555776805484813 │
6. │ dog    │   3.382295083120104 │
   └────────┴─────────────────────┘

줄어든 정밀도 검색:

SELECT
    word,
    L2DistanceTransposed(vec, [-0.88693672, 1.31532824, -0.51182908, -0.99652702, 0.59907770], 12) AS distance
FROM fruit_animal
ORDER BY distance;
   ┌─word───┬───────────distance─┐
1. │ apple  │  0.757668703053566 │
2. │ orange │ 1.5499475034938677 │
3. │ banana │ 1.6168396735102937 │
4. │ cat    │  2.429752230904804 │
5. │ horse  │  2.524650475528617 │
6. │ dog    │   3.17766975527459 │
   └────────┴────────────────────┘

12비트 양자화로 더 빠른 쿼리 실행으로 거리의 좋은 근사치를 얻는다는 점을 유의해요. 상대적 순서는 대체로 일관되며 'apple'이 여전히 가장 가까운 일치예요.

성능 고려 사항

QBit의 성능 이점은 감소된 I/O 연산에서 오는데, 더 낮은 정밀도를 사용할 때 저장소에서 읽어야 할 데이터가 적기 때문이에요. 또한 QBitFloat32 데이터를 포함하면 정밀도 매개변수가 16 이하일 때 계산 감소의 추가 이점이 있어요. 정밀도 매개변수는 정확도와 속도 사이의 트레이드오프를 직접 제어해요.

  • 더 높은 정밀도 (원본 데이터 폭에 가까움): 더 정확한 결과, 더 느린 쿼리
  • 더 낮은 정밀도: 근사 결과로 더 빠른 쿼리, 감소된 메모리 사용

참고 자료

블로그:

더 알아보기 (Learn more)