> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> 정확 벡터 검색 및 근사 벡터 검색 문서

# 정확 벡터 검색 및 근사 벡터 검색

주어진 점을 기준으로 다차원(벡터) 공간에서 가장 가까운 N개의 점을 찾는 문제를 [최근접 이웃 검색](https://en.wikipedia.org/wiki/Nearest_neighbor_search), 줄여서 벡터 검색이라고 합니다.
벡터 검색을 해결하는 일반적인 접근 방식은 두 가지입니다.

* 정확 벡터 검색은 주어진 점과 벡터 공간의 모든 점 사이의 거리를 계산합니다. 이 방식은 가능한 최고 수준의 정확도를 보장하며, 즉 반환된 점이 실제 최근접 이웃임을 보장합니다. 벡터 공간 전체를 전수 탐색하므로 실제 환경에서는 정확 벡터 검색이 너무 느릴 수 있습니다.
* 근사 벡터 검색은 정확 벡터 검색보다 훨씬 빠르게 결과를 계산하는 기법들의 집합을 의미합니다(예: 그래프나 랜덤 포리스트 같은 특수한 데이터 구조). 결과 정확도는 일반적으로 실용적인 용도에 충분한 수준입니다. 많은 근사 기법은 결과 정확도와 검색 시간 사이의 절충점을 조정할 수 있는 매개변수를 제공합니다.

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

```sql theme={null}
WITH [...] AS reference_vector
SELECT [...]
FROM table
WHERE [...] -- WHERE 절은 선택 사항임
ORDER BY <DistanceFunction>(vectors, reference_vector)
LIMIT <N>
```

벡터 공간의 점은 배열 타입의 `vectors` 컬럼에 저장됩니다. 예를 들면 [Array(Float64)](/docs/ko/reference/data-types/array), [Array(Float32)](/docs/ko/reference/data-types/array), 또는 [Array(BFloat16)](/docs/ko/reference/data-types/array)입니다.
기준 벡터는 상수 배열이며 공통 테이블 표현식으로 지정됩니다.
`<DistanceFunction>`은 기준점과 저장된 모든 점 사이의 거리를 계산합니다.
이때 사용 가능한 [거리 함수](/docs/ko/reference/functions/regular-functions/distance-functions)는 어느 것이나 사용할 수 있습니다.
`<N>`은 반환할 이웃의 수를 지정합니다.

<div id="exact-nearest-neighbor-search">
  ## 정확 벡터 검색
</div>

정확 벡터 검색은 위의 SELECT 쿼리를 그대로 사용해 수행할 수 있습니다.
이러한 쿼리의 런타임은 일반적으로 저장된 벡터 수와 해당 벡터의 차원, 즉 배열 요소 수에 비례합니다.
또한 ClickHouse는 모든 벡터를 브루트포스 방식으로 스캔하므로, 런타임은 쿼리가 사용하는 스레드 수에도 영향을 받습니다([max\_threads](/docs/ko/reference/settings/session-settings#max_threads) 설정 참조).

<div id="exact-nearest-neighbor-search-example">
  ### 예시
</div>

```sql theme={null}
CREATE TABLE tab(id Int32, vec Array(Float32)) 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;
```

반환값

```result theme={null}
   ┌─id─┬─vec─────┐
1. │  6 │ [0,2]   │
2. │  7 │ [0,2.1] │
3. │  8 │ [0,2.2] │
   └────┴─────────┘
```

<div id="approximate-nearest-neighbor-search">
  ## 근사 벡터 검색
</div>

<div id="vector-similarity-index">
  ### 벡터 유사성 인덱스
</div>

ClickHouse는 근사 벡터 검색을 수행할 수 있도록 특수한 "벡터 유사성" 인덱스를 제공합니다.

<Note>
  벡터 유사성 인덱스는 ClickHouse 버전 25.8 이상에서 사용할 수 있습니다.
  문제가 발생하면 [ClickHouse 리포지토리](https://github.com/clickhouse/clickhouse/issues)에 이슈를 등록해 주십시오.
</Note>

<div id="creating-a-vector-similarity-index">
  #### 벡터 유사성 인덱스 생성
</div>

새 테이블에 벡터 유사성 인덱스를 다음과 같이 생성할 수 있습니다.

```sql theme={null}
CREATE TABLE table
(
  [...],
  vectors Array(Float*),
  INDEX <index_name> vectors TYPE vector_similarity(<type>, <distance_function>, <dimensions>) [GRANULARITY <N>]
)
ENGINE = MergeTree
ORDER BY [...]
```

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

```sql theme={null}
ALTER TABLE table ADD INDEX <index_name> vectors TYPE vector_similarity(<type>, <distance_function>, <dimensions>) [GRANULARITY <N>];
```

벡터 유사성 인덱스는 스키핑 인덱스의 특수한 유형입니다([여기](/docs/ko/reference/engines/table-engines/mergetree-family/mergetree#table_engine-mergetree-data_skipping-indexes) 및 [여기](/docs/ko/concepts/features/performance/skip-indexes/skipping-indexes) 참조).
따라서 위 `ALTER TABLE` 문은 앞으로 테이블에 삽입되는 새 데이터에 대해서만 인덱스가 빌드되도록 합니다.
기존 데이터에도 인덱스를 빌드하려면 이를 구체화해야 합니다:

```sql theme={null}
ALTER TABLE table MATERIALIZE INDEX <index_name> SETTINGS mutations_sync = 2;
```

함수 `<distance_function>`은 다음 중 하나여야 합니다.

* `L2Distance`: [Euclidean distance](https://en.wikipedia.org/wiki/Euclidean_distance)로, 유클리드 공간에서 두 점 사이를 잇는 선분의 길이를 나타냅니다.
* `cosineDistance`: [cosine distance](https://en.wikipedia.org/wiki/Cosine_similarity#Cosine_distance)로, 0이 아닌 두 벡터 사이의 각도를 나타내거나
* `dotProduct`: [dot product](https://en.wikipedia.org/wiki/Dot_product)(내적)로, 두 벡터의 각 원소별 곱을 합한 값을 나타냅니다. 정규화된 데이터에서는 `cosineDistance`와 동일합니다.

정규화된 데이터에는 일반적으로 `L2Distance`가 가장 적합하며, 그렇지 않은 경우에는 스케일 차이를 보정하기 위해 `cosineDistance`를 권장합니다.

<Note>
  거리 함수 `L2Distance`와 `cosineDistance`는 값이 작을수록 유사성이 높고, `dotProduct`는 값이 클수록 유사성이 높습니다.
  따라서 `L2Distance`와 `cosineDistance`를 사용하는 벡터 인덱스는 `SELECT [...] ORDER BY [...] ASC` 쿼리에서만 사용할 수 있으며(`ASC`는 `ORDER BY`의 기본값입니다), `dotProduct`용으로 생성된 벡터 인덱스는 `SELECT [...] ORDER BY [...] DESC` 쿼리에서만 사용할 수 있습니다.
</Note>

`<dimensions>`는 기반 컬럼에 있는 배열의 cardinality(원소 개수)를 지정합니다.
ClickHouse가 인덱스를 생성하는 동안 cardinality가 다른 배열을 발견하면 해당 인덱스는 폐기되고 오류가 반환됩니다.

선택적 GRANULARITY 매개변수 `<N>`은 인덱스 그래뉼의 크기를 의미합니다([여기](/docs/ko/concepts/features/performance/skip-indexes/skipping-indexes) 참조).
기본 인덱스 세분화 수준으로 1을 사용하는 일반적인 스킵 인덱스와 달리, 벡터 유사성 인덱스는 기본 인덱스 세분화 수준으로 1억을 사용합니다.
이 값은 큰 파트에서도 내부적으로 소수의 인덱스만 생성되도록 합니다.
인덱스 세분화 수준 변경은 그 영향 범위를 충분히 이해하는 고급 사용자에게만 권장합니다([아래](#differences-to-regular-skipping-indexes) 참조).

벡터 유사성 인덱스는 다양한 근사 검색 메서드를 지원할 수 있다는 의미에서 범용적입니다.
실제로 사용할 메서드는 매개변수 `<type>`으로 지정합니다.
현재 사용할 수 있는 유일한 메서드는 HNSW([academic paper](https://arxiv.org/abs/1603.09320))이며, 계층적 근접 그래프를 기반으로 하는 널리 사용되는 최신 근사 벡터 검색 기법입니다.
`<type>`으로 HNSW를 사용하는 경우, 추가적인 HNSW 전용 매개변수를 선택적으로 지정할 수 있습니다:

```sql theme={null}
CREATE TABLE table
(
  [...],
  vectors Array(Float*),
  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)](/docs/ko/reference/data-types/array), [Array(Float64)](/docs/ko/reference/data-types/array), [Array(BFloat16)](/docs/ko/reference/data-types/array) 타입의 컬럼에만 생성할 수 있습니다. `Array(Nullable(Float32))` 및 `Array(LowCardinality(Float32))`와 같은 널 허용 또는 LowCardinality float 배열은 허용되지 않습니다.
* 벡터 유사성 인덱스는 단일 컬럼에만 생성해야 합니다.
* 벡터 유사성 인덱스는 계산 표현식(예: `INDEX index_name arraySort(vectors) TYPE vector_similarity([...])`)에 생성할 수도 있지만, 이렇게 생성한 인덱스는 이후 근사 최근접 이웃 검색에 사용할 수 없습니다.
* 벡터 유사성 인덱스를 생성하려면 기반 컬럼의 모든 배열에 `<dimension>`개의 요소가 있어야 하며, 이는 인덱스 생성 시 확인됩니다. 이 요구 사항 위반을 가능한 한 빨리 감지하려면 벡터 컬럼에 [제약 조건](/docs/ko/reference/statements/create/table#constraints)을 추가할 수 있습니다. 예: `CONSTRAINT same_length CHECK length(vectors) = 256`.
* 마찬가지로 기반 컬럼의 배열 값은 비어 있어서는 안 되며(`[]`), 기본값(역시 `[]`)이어서도 안 됩니다.

**스토리지 및 메모리 사용량 추정**

일반적인 AI 모델(예: 대규모 언어 모델, [LLMs](https://en.wikipedia.org/wiki/Large_language_model))과 함께 사용하기 위해 생성된 벡터는 수백 개 또는 수천 개의 부동소수점 값으로 이루어집니다.
따라서 단일 벡터 값의 메모리 사용량은 수 KB에 이를 수 있습니다.
테이블의 기반 벡터 컬럼에 필요한 스토리지와 벡터 유사성 인덱스에 필요한 메인 메모리(RAM)를 추정하려는 경우 아래 두 공식을 사용할 수 있습니다.

테이블의 벡터 컬럼에 대한 스토리지 사용량(비압축):

```text theme={null}
Storage consumption = Number of vectors * Dimension * Size of column data type
```

[dbpedia 데이터셋](https://huggingface.co/datasets/KShivendu/dbpedia-entities-openai-1M) 예시:

```text theme={null}
Storage consumption = 1 million * 1536 * 4 (for Float32) = 6.1 GB
```

검색을 수행하려면 벡터 유사성 인덱스를 디스크에서 메인 메모리(main memory)로 완전히 로드해야 합니다.
마찬가지로 벡터 인덱스도 메모리에서 완전히 생성한 후 디스크에 저장됩니다.

벡터 인덱스를 로드하는 데 필요한 메모리 사용량:

```text theme={null}
인덱스 내 벡터의 메모리 (mv) = 벡터 수 * 차원 * 양자화된 데이터 타입의 크기
인메모리 그래프의 메모리 (mg) = 벡터 수 * hnsw_max_connections_per_layer * Bytes_per_node_id (= 4) * Layer_node_repetition_factor (= 2)

메모리 사용량: mv + mg
```

[dbpedia 데이터셋](https://huggingface.co/datasets/KShivendu/dbpedia-entities-openai-1M) 예시:

```text theme={null}
인덱스 내 벡터 메모리 (mv) = 1 million * 1536 * 2 (BFloat16 기준) = 3072 MB
인메모리 그래프 메모리 (mg) = 1 million * 64 * 2 * 4 = 512 MB

메모리 사용량 = 3072 + 512 = 3584 MB
```

위 공식에는 벡터 유사성 인덱스가 미리 할당된 버퍼와 캐시 같은 런타임 데이터 구조를 할당하는 데 필요한 추가 메모리가 고려되어 있지 않습니다.

<div id="using-a-vector-similarity-index">
  #### 벡터 유사도 인덱스 사용하기
</div>

<Note>
  벡터 유사성 인덱스를 사용하려면 [compatibility](/docs/ko/reference/settings/session-settings) 설정이 `''`(기본값)이거나 `'25.1'` 이상이어야 합니다.
</Note>

벡터 유사성 인덱스는 다음과 같은 형태의 SELECT 쿼리를 지원합니다:

```sql theme={null}
WITH [...] AS reference_vector
SELECT [...]
FROM table
WHERE [...] -- WHERE 절은 선택 사항입니다.
ORDER BY <DistanceFunction>(vectors, reference_vector)
LIMIT <N>
```

ClickHouse의 쿼리 최적화기는 위의 쿼리 템플릿과 일치하는지 확인하고 사용 가능한 벡터 유사성 인덱스를 활용하려 시도합니다.
쿼리가 벡터 유사성 인덱스를 사용하려면 SELECT 쿼리의 거리 함수가 인덱스 정의의 거리 함수와 동일해야 합니다.

고급 사용자는 검색 중 후보 목록의 크기를 조정하기 위해 설정 [hnsw\_candidate\_list\_size\_for\_search](/docs/ko/reference/settings/session-settings#hnsw_candidate_list_size_for_search)(HNSW 하이퍼파라미터 "ef\_search"라고도 함)에 사용자 지정 값을 제공할 수 있습니다(예: `SELECT [...] SETTINGS hnsw_candidate_list_size_for_search = <value>`).
해당 설정의 기본값인 256은 대부분의 사용 사례에서 잘 동작합니다.
설정값이 높을수록 정확도는 향상되지만 성능은 저하됩니다.

쿼리가 벡터 유사도 인덱스를 사용할 수 있는 경우, ClickHouse는 SELECT 쿼리에 지정된 LIMIT `<N>` 값이 적절한 범위 내에 있는지 확인합니다.
구체적으로, `<N>`이 설정 [max\_limit\_for\_vector\_search\_queries](/docs/ko/reference/settings/session-settings#max_limit_for_vector_search_queries)의 값(기본값: 100)보다 크면 오류가 반환됩니다.
LIMIT 값이 너무 크면 검색 속도가 저하될 수 있으며, 일반적으로 잘못된 사용을 의미합니다.

SELECT 쿼리가 벡터 유사성 인덱스를 사용하는지 확인하려면 쿼리 앞에 `EXPLAIN indexes = 1`을 붙이십시오.

예시로, 다음 쿼리를

```sql theme={null}
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;
```

반환될 수 있습니다

```result theme={null}
┌─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 dataset](https://huggingface.co/datasets/KShivendu/dbpedia-entities-openai-1M)의 벡터 100만 개가 575개의 그래뉼에 저장되며, 각 벡터의 차원은 1536이고 그래뉼당 약 1,700개의 행이 포함됩니다.
해당 쿼리는 10개의 최근접 이웃을 요청하며, vector similarity index는 10개의 개별 그래뉼에서 이 10개의 이웃을 찾아냅니다.
쿼리 실행 시 이 10개의 그래뉼이 읽힙니다.

출력에 `Skip`과 벡터 인덱스의 이름 및 유형(예시에서는 `idx`와 `vector_similarity`)이 포함되어 있으면 벡터 유사성 인덱스가 사용된 것입니다.
이 경우, 벡터 유사성 인덱스가 4개의 그래뉼 중 2개를 건너뛰었으며, 이는 전체 데이터의 50%에 해당합니다.
건너뛸 수 있는 그래뉼이 많을수록 인덱스 활용 효율이 높아집니다.

<Tip>
  인덱스 사용을 강제하려면 [force\_data\_skipping\_indexes](/docs/ko/reference/settings/session-settings#force_data_skipping_indices) 설정을 사용해 SELECT 쿼리를 실행할 수 있습니다(설정 값으로 인덱스 이름을 지정하십시오).
</Tip>

**포스트 필터링 및 프리 필터링**

SELECT 쿼리에 추가 필터 조건을 포함하는 `WHERE` 절을 선택적으로 지정할 수 있습니다.
ClickHouse는 포스트필터링 또는 프리필터링 전략을 사용하여 이러한 필터 조건을 평가합니다.
두 전략의 차이는 필터가 평가되는 순서에 있습니다:

* 포스트필터링은 먼저 벡터 유사성 인덱스를 평가한 후, ClickHouse가 `WHERE` 절에 지정된 추가 필터를 평가하는 방식을 의미합니다.
* 사전 필터링은 필터의 평가 순서가 반대로 적용된다는 뜻입니다.

각 전략마다 트레이드오프가 다릅니다:

* 포스트필터링은 일반적으로 `LIMIT <N>` 절에서 요청한 행 수보다 적은 수의 결과를 반환할 수 있다는 문제가 있습니다. 이는 벡터 유사성 인덱스가 반환한 결과 행 중 하나 이상이 추가 필터 조건을 만족하지 못할 때 발생합니다.
* 프리필터링은 일반적으로 아직 해결되지 않은 문제입니다. 일부 특화된 벡터 데이터베이스는 프리필터링 알고리즘을 제공하지만, 대부분의 관계형 데이터베이스(ClickHouse 포함)는 정확한 최근접 이웃 검색으로 폴백하며, 즉 인덱스 없이 브루트포스로 스캔합니다.

어떤 전략을 사용할지는 필터 조건에 따라 달라집니다.

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

추가 필터 조건이 파티션 키의 일부이면 ClickHouse는 파티션 프루닝을 적용합니다.
예시로, 테이블이 `year` 컬럼을 기준으로 범위 파티셔닝되어 있고 다음 쿼리를 실행한다고 가정합니다.

```sql theme={null}
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는 포스트필터링을 적용합니다.

*추가 필터는 프라이머리 키 인덱스를 사용해 평가할 수 있습니다*

추가 필터 조건을 [프라이머리 키](/docs/ko/reference/engines/table-engines/mergetree-family/mergetree#primary-key)로 평가할 수 있고(즉, 프라이머리 키의 prefix를 이룰 때)

* 필터 조건이 파트 내에서 하나 이상의 행을 제외하면, ClickHouse는 해당 파트에서 "남아 있는" ranges에 대해 프리필터링으로 전환합니다.
* 필터 조건이 파트 내에서 어떤 행도 제외하지 않으면, ClickHouse는 해당 파트에 대해 포스트필터링을 수행합니다.

실제 사용 사례에서는 후자의 경우가 발생할 가능성은 매우 낮습니다.

*추가 필터는 스키핑 인덱스를 사용해 평가할 수 있습니다*

추가 필터 조건을 [스키핑 인덱스](/docs/ko/reference/engines/table-engines/mergetree-family/mergetree#table_engine-mergetree-data_skipping-indexes)(minmax 인덱스, set 인덱스 등)로 평가할 수 있는 경우, ClickHouse는 포스트필터링을 수행합니다.
이러한 경우에는 다른 스키핑 인덱스보다 더 많은 행을 제외할 것으로 예상되므로 벡터 유사성 인덱스를 먼저 평가합니다.

포스트필터링과 프리필터링을 더 세밀하게 제어하려면 두 가지 설정을 사용할 수 있습니다.

설정 [vector\_search\_filter\_strategy](/docs/ko/reference/settings/session-settings#vector_search_filter_strategy)는 `prefilter`로 설정할 수 있습니다(기본값: 위 휴리스틱을 구현하는 `auto`).
이는 추가 필터 조건의 선택도가 매우 높은 경우 프리필터링을 강제할 때 유용합니다.
예를 들어, 다음 쿼리는 프리필터링의 이점을 얻을 수 있습니다:

```sql theme={null}
SELECT bookid, author, title
FROM books
WHERE price < 2.00
ORDER BY cosineDistance(book_vector, getEmbedding('Books on ancient Asian empires'))
LIMIT 10
```

2달러 미만인 책이 극히 적다고 가정하면, 포스트필터링은 0개의 행을 반환할 수 있습니다. 벡터 인덱스가 반환한 상위 10개의 일치 결과가 모두 2달러를 초과하는 책일 수 있기 때문입니다.
프리필터링을 강제하려면(쿼리에 `SETTINGS vector_search_filter_strategy = 'prefilter'` 추가) ClickHouse는 먼저 가격이 2달러 미만인 모든 책을 찾은 다음, 찾은 책들에 대해 브루트포스 벡터 검색을 수행합니다.

위 문제를 해결하는 또 다른 방법으로, [vector\_search\_index\_fetch\_multiplier](/docs/ko/reference/settings/session-settings#vector_search_index_fetch_multiplier) (기본값: `1.0`, 최댓값: `1000.0`)를 `1.0`보다 큰 값(예: `2.0`)으로 설정할 수 있습니다.
벡터 인덱스에서 가져오는 최근접 이웃 수에 이 설정값을 곱한 뒤, 해당 행들에 추가 필터를 적용하여 LIMIT 개수만큼의 행을 반환합니다.
예를 들어, multiplier를 `3.0`으로 설정해 다시 쿼리할 수 있습니다:

```sql theme={null}
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는 각 part에서 벡터 인덱스를 통해 가장 가까운 이웃 3.0 x 10 = 30개를 가져온 후 추가 필터를 적용합니다.
가장 가까운 이웃 10개만 반환됩니다.
`vector_search_index_fetch_multiplier`를 설정하면 이 문제를 완화할 수 있지만, 극단적인 경우(WHERE 조건의 선택도가 매우 높은 경우)에는 요청한 N개보다 적은 행만 반환될 수도 있습니다.

**재점수화**

ClickHouse의 스킵 인덱스는 일반적으로 그래뉼 수준에서 필터링합니다. 즉, 스킵 인덱스에서 내부적으로 조회를 수행하면 잠재적으로 일치하는 그래뉼 목록이 반환되며, 그 결과 후속 스캔에서 읽어야 할 데이터의 양이 줄어듭니다.
이는 일반적인 스킵 인덱스에서는 잘 작동하지만, 벡터 유사성 인덱스에서는 "세분화 수준 불일치"가 발생합니다.
좀 더 자세히 설명하면, 벡터 유사성 인덱스는 주어진 기준 벡터에 대해 가장 유사한 N개의 벡터에 해당하는 행 번호를 결정합니다.
`vector_search_with_rescoring = 1` 설정을 사용하면, ClickHouse는 후보 행에 대한 원본 전체 정밀도 벡터를 읽고 일반적인 SQL 파이프라인에서 최종 거리를 계산합니다.
쿼리 계획에서 허용되는 경우, ClickHouse는 최종 거리 계산 전에 벡터 인덱스가 반환한 후보 행으로 스캔을 필터링합니다.
이 단계를 rescoring이라고 하며, 최종 순위가 인덱스 거리 대신 저장된 벡터를 사용하므로 특히 양자화된 벡터 인덱스에서 정확도를 높일 수 있습니다.
추가 필터로 인해 너무 많은 후보가 제거되거나 더 높은 재현율이 필요하면, 벡터 인덱스가 rescoring을 위해 더 많은 후보 행을 반환하도록 `vector_search_index_fetch_multiplier` 설정을 늘리십시오.

따라서 ClickHouse는 rescoring을 비활성화하고 인덱스에서 가장 유사한 벡터와 그 거리를 직접 반환하는 최적화를 제공합니다.
이 최적화는 기본적으로 활성화되어 있습니다. [vector\_search\_with\_rescoring](/docs/ko/reference/settings/session-settings#vector_search_with_rescoring) 설정을 참조하십시오.
개략적으로 보면, ClickHouse는 가장 유사한 벡터와 그 거리를 가상 컬럼 `_distance`를 통해 제공합니다.
이를 확인하려면 `EXPLAIN header = 1`과 함께 벡터 검색 쿼리를 실행하십시오:

```sql theme={null}
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
```

```result theme={null}
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(Float64), 'Array(Float64)'_String)) Float64     │
 5. │           __table1.id Int32                                                                             │
 6. │     Sorting (Sorting for ORDER BY)                                                                      │
 7. │     Header: L2Distance(__table1.vec, _CAST([0., 2.]_Array(Float64), 'Array(Float64)'_String)) Float64   │
 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(Float64), 'Array(Float64)'_String)) Float64 │
11. │               __table1.id Int32                                                                         │
12. │         ReadFromMergeTree (default.tab)                                                                 │
13. │         Header: id Int32                                                                                │
14. │                 _distance Float32                                                                       │
    └─────────────────────────────────────────────────────────────────────────────────────────────────────────┘
```

<Note>
  rescoring 없이 실행되는 쿼리(`vector_search_with_rescoring = 0`)라도 병렬 레플리카가 활성화되어 있으면 rescoring으로 폴백될 수 있습니다.
</Note>

<div id="performance-tuning">
  #### 성능 튜닝
</div>

**압축 튜닝**

거의 모든 사용 사례에서 기반 컬럼의 벡터는 조밀하여 압축 효율이 높지 않습니다.
그 결과, 벡터 컬럼에 대한 [압축](/docs/ko/reference/statements/create/table#column_compression_codec)은 삽입 및 읽기 성능을 저하시킵니다.
따라서 압축을 비활성화할 것을 권장합니다.
이를 위해 다음과 같이 벡터 컬럼에 `CODEC(NONE)`를 지정하십시오:

```sql theme={null}
CREATE TABLE tab(id Int32, vec Array(Float32) CODEC(NONE), INDEX idx vec TYPE vector_similarity('hnsw', 'L2Distance', 2)) ENGINE = MergeTree ORDER BY id;
```

**인덱스 생성 튜닝**

벡터 유사성 인덱스의 수명 주기는 파트의 수명 주기와 연동됩니다.
즉, 벡터 유사성 인덱스가 정의된 새 파트가 생성될 때마다 해당 인덱스도 함께 생성됩니다.
이는 일반적으로 데이터가 [삽입될 때](/docs/ko/concepts/features/operations/insert/inserting-data) 또는 [머지](/docs/ko/concepts/core-concepts/merges) 중에 발생합니다.
안타깝게도 HNSW는 인덱스 생성 시간이 긴 것으로 알려져 있어 삽입과 머지 속도를 크게 저하시킬 수 있습니다.
벡터 유사성 인덱스는 데이터가 불변이거나 거의 변경되지 않는 경우에만 사용하는 것이 가장 바람직합니다.

인덱스 생성을 더 빠르게 하려면 다음 기법을 사용할 수 있습니다:

첫째, 인덱스 생성을 병렬화할 수 있습니다.
인덱스 생성 스레드의 최대 개수는 서버 설정 [max\_build\_vector\_similarity\_index\_thread\_pool\_size](/docs/ko/reference/settings/server-settings/settings#max_build_vector_similarity_index_thread_pool_size)로 구성할 수 있습니다.
최적의 성능을 위해 이 설정값은 CPU 코어 수에 맞게 구성해야 합니다.

둘째, INSERT SQL 문의 속도를 높이기 위해 사용자는 세션 설정 [materialize\_skip\_indexes\_on\_insert](/docs/ko/reference/settings/session-settings#materialize_skip_indexes_on_insert)를 사용해 새로 삽입된 파트에서 스키핑 인덱스 생성을 비활성화할 수 있습니다.
이러한 파트에 대한 SELECT 쿼리는 정확 검색으로 대체됩니다.
삽입된 파트는 전체 테이블 크기에 비해 대체로 작으므로, 이로 인한 성능 영향은 무시해도 될 수준일 것으로 예상됩니다.

셋째, 머지 속도를 높이기 위해 사용자는 세션 설정 [materialize\_skip\_indexes\_on\_merge](/docs/ko/reference/settings/merge-tree-settings#materialize_skip_indexes_on_merge)를 사용해 병합된 파트에서 스키핑 인덱스 생성을 비활성화할 수 있습니다.
이 방식은 SQL 문 [ALTER TABLE \[...\] MATERIALIZE INDEX \[...\]](/docs/ko/reference/statements/alter/skipping-index#materialize-index)과 함께 벡터 유사성 인덱스의 수명 주기를 명시적으로 제어할 수 있게 해줍니다.
예를 들어, 인덱스 생성은 모든 데이터가 수집될 때까지 또는 주말처럼 시스템 부하가 낮은 시점까지 미룰 수 있습니다.

**인덱스 사용 튜닝**

SELECT 쿼리가 벡터 유사성 인덱스를 사용하려면 해당 인덱스를 메인 메모리에 로드해야 합니다.
동일한 벡터 유사성 인덱스가 메인 메모리에 반복해서 로드되지 않도록 ClickHouse는 이러한 인덱스를 위한 전용 인메모리 캐시를 제공합니다.
이 캐시가 클수록 불필요한 로드 발생은 줄어듭니다.
최대 캐시 크기는 서버 설정 [vector\_similarity\_index\_cache\_size](/docs/ko/reference/settings/server-settings/settings#vector_similarity_index_cache_size)로 구성할 수 있습니다.
기본적으로 캐시는 최대 5 GB까지 커질 수 있습니다.

다음 로그 메시지(`system.text_log`)는 벡터 유사성 인덱스가 로드되고 있음을 나타냅니다.
이러한 메시지가 서로 다른 벡터 검색 쿼리에서 반복적으로 나타난다면, 캐시 크기가 너무 작다는 뜻입니다.

```text theme={null}
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
```

<Note>
  벡터 유사성 인덱스 캐시는 벡터 인덱스 그래뉼을 저장합니다.
  개별 벡터 인덱스 그래뉼의 크기가 캐시 크기보다 크면 캐시되지 않습니다.
  따라서 "스토리지 및 메모리 사용량 추정"의 공식 또는 [system.data\_skipping\_indices](/docs/ko/reference/system-tables/data_skipping_indices)를 기준으로 벡터 인덱스 크기를 계산한 뒤, 이에 맞춰 캐시 크기를 설정하십시오.
</Note>

*느린 벡터 검색 쿼리를 조사할 때는 먼저 벡터 인덱스 캐시를 확인하고, 필요하면 크기를 늘려야 한다는 점을 다시 강조합니다.*

현재 벡터 유사성 인덱스 캐시 크기는 [system.metrics](/docs/ko/reference/system-tables/metrics)에서 확인할 수 있습니다:

```sql theme={null}
SELECT metric, value
FROM system.metrics
WHERE metric = 'VectorSimilarityIndexCacheBytes'
```

특정 query id를 가진 쿼리의 캐시 적중 및 누락은 [system.query\_log](/docs/ko/reference/system-tables/query_log)에서 확인할 수 있습니다:

```sql theme={null}
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;
```

프로덕션 환경에서는 모든 벡터 인덱스가 항상 메모리에 상주할 수 있도록 캐시 크기를 충분히 크게 설정하는 것을 권장합니다.

**양자화 조정**

[양자화](https://huggingface.co/blog/embedding-quantization)는 벡터의 메모리 사용량을 줄이고 벡터 인덱스를 구축하고 탐색하는 데 드는 계산 비용을 낮추는 기법입니다.
ClickHouse 벡터 인덱스는 다음 양자화 옵션을 지원합니다.

| 양자화        | 이름                | 차원당 저장 공간 |
| ---------- | ----------------- | --------- |
| f32        | 단정밀도              | 4바이트      |
| f16        | 반정밀도              | 2바이트      |
| bf16 (기본값) | 반정밀도(brain float) | 2바이트      |
| i8         | 1/4 정밀도           | 1바이트      |
| b1         | 바이너리              | 1비트       |

양자화를 적용하면 원래의 전체 정밀도 부동소수점 값(`f32`)을 사용하는 검색과 비교해 벡터 검색의 정확도가 낮아집니다.
하지만 대부분의 데이터셋에서는 반정밀도 brain float 양자화(`bf16`)의 정확도 손실이 미미하므로, 벡터 유사성 인덱스는 기본적으로 이 양자화 기법을 사용합니다.
1/4 정밀도(`i8`) 및 바이너리(`b1`) 양자화는 벡터 검색에서 눈에 띄는 정확도 손실을 초래합니다.
벡터 유사성 인덱스의 크기가 사용 가능한 DRAM 크기보다 상당히 클 때에만 이 두 양자화를 권장합니다.
이 경우 정확도를 높이기 위해 rescoring([vector\_search\_index\_fetch\_multiplier](/docs/ko/reference/settings/session-settings#vector_search_index_fetch_multiplier), [vector\_search\_with\_rescoring](/docs/ko/reference/settings/session-settings#vector_search_with_rescoring))도 활성화할 것을 권장합니다.
바이너리 양자화는 1) 정규화된 임베딩(즉, 벡터 길이 = 1이며 OpenAI 모델은 일반적으로 정규화되어 있음), 2) 거리 함수로 cosine distance를 사용하는 경우에만 권장합니다.
바이너리 양자화는 내부적으로 Hamming distance를 사용해 근접 그래프를 구성하고 검색합니다.
rescoring 단계에서는 테이블에 저장된 원래의 전체 정밀도 벡터를 사용해 cosine distance를 통해 nearest neighbours를 식별합니다.

**데이터 전송 조정**

벡터 검색 쿼리의 기준 벡터는 사용자가 제공하며, 일반적으로 Large Language Model (LLM)을 호출해 가져옵니다.
ClickHouse에서 벡터 검색을 실행하는 일반적인 Python 코드는 다음과 같습니다

```python theme={null}
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 driver가 임베딩 벡터를 사람이 읽을 수 있는 문자열로 치환한 뒤 `SELECT` 쿼리 전체를 문자열로 전송합니다.
임베딩 벡터가 단정밀도 부동소수점 값 1536개로 이루어져 있다고 가정하면, 전송되는 문자열 길이는 20 kB에 달합니다.
이로 인해 토큰화, 파싱, 그리고 수천 번의 문자열-부동소수점 변환을 수행하느라 CPU 사용량이 높아집니다.
또한 ClickHouse 서버 로그 파일에도 상당한 공간이 필요하며, 그 결과 `system.query_log` 역시 비대해집니다.

대부분의 LLM 모델은 임베딩 벡터를 네이티브 float의 목록 또는 NumPy 배열로 반환합니다.
따라서 Python 애플리케이션에서는 다음과 같은 방식으로 참조 벡터 매개변수를 바이너리 형식으로 바인딩하는 것을 권장합니다:

```python theme={null}
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)
```

이 예시에서는 기준 벡터를 바이너리 형식 그대로 전송한 뒤, 서버에서 float 배열로 재해석합니다.
이렇게 하면 서버 측 CPU 시간을 절약할 수 있고, 서버 로그와 `system.query_log`가 불필요하게 커지는 것도 방지할 수 있습니다.

<div id="administration">
  #### 관리 및 모니터링
</div>

벡터 유사성 인덱스가 디스크에서 차지하는 크기는 [system.data\_skipping\_indices](/docs/ko/reference/system-tables/data_skipping_indices)에서 확인할 수 있습니다:

```sql theme={null}
SELECT database, table, name, formatReadableSize(data_compressed_bytes)
FROM system.data_skipping_indices
WHERE type = 'vector_similarity';
```

출력 예시:

```result theme={null}
┌─database─┬─table─┬─name─┬─formatReadab⋯ssed_bytes)─┐
│ default  │ tab   │ idx  │ 348.00 MB                │
└──────────┴───────┴──────┴──────────────────────────┘
```

<div id="differences-to-regular-skipping-indexes">
  #### 일반 스키핑 인덱스와의 차이점
</div>

모든 일반 [스키핑 인덱스](/docs/ko/concepts/features/performance/skip-indexes/skipping-indexes)와 마찬가지로, 벡터 유사성 인덱스도 그래뉼을 기준으로 구성되며 각 인덱싱된 블록은 `GRANULARITY = [N]`개의 그래뉼로 이루어집니다(일반 스키핑 인덱스의 기본값은 `[N]` = 1).
예를 들어 테이블의 프라이머리 인덱스 세분화 수준이 8192이고(`index_granularity = 8192` 설정) `GRANULARITY = 2`이면, 각 인덱싱된 블록에는 16384개의 행이 포함됩니다.
하지만 근사 최근접 이웃 검색을 위한 데이터 구조와 알고리즘은 본질적으로 행 지향입니다.
이들은 행 집합의 압축된 표현을 저장하고, 벡터 검색 쿼리에 대해서도 행을 반환합니다.
이 때문에 벡터 유사성 인덱스는 일반 스키핑 인덱스와 비교했을 때 동작 방식에 다소 직관적이지 않은 차이가 있습니다.

사용자가 컬럼에 벡터 유사성 인덱스를 정의하면, ClickHouse는 내부적으로 각 인덱스 블록마다 벡터 유사성 "서브 인덱스"를 생성합니다.
이 서브 인덱스는 자신이 속한 인덱스 블록의 행만 알고 있다는 점에서 "로컬"입니다.
앞선 예시에서 컬럼에 65536개의 행이 있다고 가정하면, 4개의 인덱스 블록(8개의 그래뉼에 걸쳐 있음)과 각 인덱스 블록에 대한 벡터 유사성 서브 인덱스를 얻게 됩니다.
이론적으로 서브 인덱스는 자신이 담당하는 인덱스 블록 내에서 가장 가까운 N개의 점에 해당하는 행을 직접 반환할 수 있습니다.
`vector_search_with_rescoring = 1`인 쿼리의 경우, 쿼리 계획이 이 최적화를 허용하면 ClickHouse는 저장된 벡터로부터 최종 거리를 계산하기 전에 이러한 행 위치를 사용해 행을 필터링할 수 있습니다.
rescoring을 사용하지 않으면 ClickHouse는 가상 컬럼 `_distance`를 통해 벡터 인덱스의 거리를 직접 사용합니다.
두 모드 모두 여전히 주변 그래뉼 범위를 사용해 읽기를 스케줄링하며, 이는 일반 스키핑 인덱스가 인덱스 블록 단위로 데이터를 스키핑하는 것과 다릅니다.

`GRANULARITY` 매개변수는 생성되는 벡터 유사성 서브 인덱스의 개수를 결정합니다.
`GRANULARITY` 값이 클수록 벡터 유사성 서브 인덱스 수는 줄어들지만 각 서브 인덱스는 더 커지며, 결국 컬럼(또는 컬럼의 데이터 파트)에 서브 인덱스가 하나만 남는 수준까지 갈 수 있습니다.
이 경우 서브 인덱스는 컬럼의 모든 행을 "전역적"으로 볼 수 있으며, 관련 행이 있는 컬럼(파트)의 그래뉼을 직접 모두 반환할 수 있습니다(그러한 그래뉼 수는 최대 `LIMIT [N]`개입니다).
`vector_search_with_rescoring = 1`이면, ClickHouse는 그런 다음 일치하는 행 위치를 읽고 해당 행에 대해 정확한 거리를 계산할 수 있습니다.
`GRANULARITY` 값이 작으면 각 서브 인덱스는 최대 `LIMIT N`개의 후보 행을 반환할 수 있습니다.
그 결과 더 많은 후보 행을 읽은 후 후처리 필터링해야 할 수 있습니다.
검색 정확도는 두 경우 모두 동일하게 우수하며, 차이가 나는 것은 처리 성능뿐이라는 점에 유의하십시오.
일반적으로 벡터 유사성 인덱스에는 큰 `GRANULARITY`를 사용하는 것이 권장되며, 벡터 유사성 구조의 메모리 사용량이 지나치게 커지는 등의 문제가 있을 때만 더 작은 `GRANULARITY` 값으로 낮추는 것이 좋습니다.
벡터 유사성 인덱스에 `GRANULARITY`를 지정하지 않으면 기본값은 1억입니다.

<div id="approximate-nearest-neighbor-search-example">
  #### 예시
</div>

쿼리:

```sql title="Query" theme={null}
CREATE TABLE tab(id Int32, vec Array(Float32), 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;
```

```result title="Response" theme={null}
   ┌─id─┬─vec─────┐
1. │  6 │ [0,2]   │
2. │  7 │ [0,2.1] │
3. │  8 │ [0,2.2] │
   └────┴─────────┘
```

근사 벡터 검색을 사용하는 다른 예시 데이터셋은 다음과 같습니다:

* [LAION-400M](/docs/ko/get-started/sample-datasets/laion)
* [LAION-5B](/docs/ko/get-started/sample-datasets/laion5b)
* [dbpedia](/docs/ko/get-started/sample-datasets/dbpedia)
* [hackernews](/docs/ko/get-started/sample-datasets/hacker-news-vector-search)

<div id="vector-search-with-quantized-codecs">
  ### 양자화된 코덱을 사용한 벡터 검색
</div>

<Note>
  `Quantized` 코덱은 실험적 기능입니다. `SET allow_experimental_codecs = 1`을 설정하여 활성화하십시오.
  문제가 발생하면 [ClickHouse 리포지토리](https://github.com/clickhouse/clickhouse/issues)에 이슈를 등록해 주십시오.
</Note>

<div id="quantized-codecs-introduction">
  #### 소개
</div>

[벡터 유사성 인덱스](#vector-similarity-index)는 그래프를 순회해 최근접 이웃 쿼리에 응답하며, 그래프를 메모리에 유지할 수 있을 때 매우 뛰어난 성능을 발휘합니다.
다음 두 가지 조건이 이 방식의 적용 범위를 제한합니다:

* **규모.** 벡터 자체와 별도로 그래프를 빌드하는 데 걸리는 시간과 이를 저장하는 데 필요한 메모리가 지배적인 비용이 됩니다.
* **필터링.** 선택적인 `WHERE` 필터가 있는 경우 그래프 순회는 비효율적입니다. 프레디케이트를 만족하는 소수의 행에 도달하지 못하거나, 그 행들을 찾기 위해 지나치게 많은 후보를 검사해야 하기 때문입니다.

전수 스캔은 이 두 가지 제약을 모두 받지 않습니다. 보조 구조가 필요 없고, 파트는 이어 붙이는 방식으로 머지되며, 필터는 단순히 스캔할 행 수만 줄입니다.
유일한 단점은 읽어야 할 데이터의 양입니다. 전체 `Float32` 정밀도로 저장된 벡터를 스캔하면 스토리지 I/O가 지배적인 비용이 됩니다. 전체 벡터 컬럼을 디스크(또는 객체 스토리지)에서 읽어야 하기 때문입니다. 조밀한 임베딩 컬럼에서는 이 컬럼이 테이블에서 가장 크고, 압축도 잘되지 않습니다.

`Quantized` 컬럼 코덱은 이 단점을 해결합니다.
각 벡터는 두 번 저장됩니다. 하나는 변경되지 않은 원래의 전체 정밀도 값이고, 다른 하나는 별도 스트림에 저장되는 컴팩트한 *양자화 코드*입니다.
벡터 검색 쿼리는 먼저 비용이 낮고 SIMD에 적합한 거리 함수를 사용해 코드를 스캔하여 가장 유망한 후보의 후보군을 만든 다음, 이 후보군을 전체 정밀도 벡터를 기준으로 다시 순위화합니다.
코드는 원시 벡터보다 훨씬 작으므로, 후보군 스캔은 스토리지에서 읽는 바이트 수를 크게 줄일 수 있고, 전체 정밀도 컬럼은 후보군에 포함된 소수의 후보에 대해서만 읽으면 됩니다. 그 결과 최종 순위화의 정확성은 그대로 유지됩니다.

<div id="quantized-codecs-declaring">
  #### 코덱 선언
</div>

`Array(Float32)`(또는 `Array(Float64)` / `Array(BFloat16)`) 컬럼에 `Quantized(...)` 코덱을 적용합니다.
이 코덱은 실험적 기능이므로 먼저 `allow_experimental_codecs`를 활성화하십시오:

```sql theme={null}
SET allow_experimental_codecs = 1;

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

전체 정밀도 데이터는 기존과 동일하게 저장되며, 코덱은 보조 code stream만 추가합니다.
코덱은 테이블(table) 생성 시 고정되며, `ALTER TABLE`로 추가하거나 변경할 수 없습니다.

<div id="quantized-codecs-methods">
  #### 양자화 방식
</div>

각 방식은 크기 / 정확도 / 메트릭 사이의 트레이드오프에서 서로 다른 지점에 해당합니다. `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` 코드 1개와 벡터 노름을 사용합니다(`dimensions + 4`바이트). 크기는 가장 크지만 원본 보존도가 가장 높은 플랫 코드입니다. `L2Distance`와 `cosineDistance`를 지원합니다.
* `Quantized('prefix', dimensions, leading_dimensions, 'int8'|'bf16')` — Matryoshka: 앞의 `leading_dimensions`개 좌표만 유지하고, `Int8`(벡터별 스케일 포함) 또는 `BFloat16`으로 저장합니다. Matryoshka Representation Learning으로 학습된 임베딩에 매우 작은 코드를 제공합니다. `L2Distance`와 `cosineDistance`를 지원합니다.
* `Quantized('product', dimensions, nbits, m)` — Product Quantization: 파트별 코드북을 k-means로 학습하며, 각 벡터는 `nbits`비트 코드 `m`개로 표현됩니다(따라서 `dimensions`는 `m`의 배수여야 합니다). 가장 컴팩트한 옵션이며 바이트당 리콜도 가장 높지만, 삽입 시 학습 단계가 필요합니다. `L2Distance`와 `cosineDistance`를 지원합니다.

`rabitq`와 `turboquant`는 `dimensions`가 8의 배수여야 합니다.

<div id="quantized-codecs-searching">
  #### 투명한 검색
</div>

별도의 쿼리 구문은 필요하지 않습니다. [정확한 검색](#exact-nearest-neighbor-search)에 사용하는 것과 동일한 top-`k` 쿼리를 작성하십시오:

```sql theme={null}
WITH [/* reference vector of `dimensions` floats */] 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단계 계획으로 재작성합니다. 먼저 양자화된 코드를 스캔해 후보군(후보군)을 추린 다음, 이 후보군을 전체 정밀도의 `vec`에 대해 다시 점수화합니다.
이 설정은 기본적으로 비활성화되어 있으므로, 사용하지 않으면 동일한 쿼리는 일반적인 정확 스캔으로 실행됩니다. 코덱은 결과를 바꾸지 않으며, 명시적으로 활성화한 경우에만 더 빠른 경로를 제공합니다.
선택한 메서드가 지원하는 거리 함수를 사용하십시오. 모든 메서드에서는 `cosineDistance`를 사용하고, `int8`, `prefix`, `product`에서는 추가로 `L2Distance`도 사용할 수 있습니다.

<div id="quantized-codecs-settings">
  #### 설정
</div>

* `allow_experimental_codecs` — `Quantized` 코덱을 선언하려면 이 설정을 활성화해야 합니다(기본값: `0`).
* `vector_search_use_quantized_codes` — 2단계 후보군 선정 및 재채점 재작성을 활성화합니다(기본값: `0`). 비활성화되면 일치하는 쿼리는 전체 정밀도의 벡터를 정확하게 스캔합니다.
* `vector_search_index_fetch_multiplier` — 쿼리의 `LIMIT` 대비 후보군에 포함할 후보 수를 지정합니다. 스캔은 재채점 전에 상위 `LIMIT × multiplier`개의 코드만 유지합니다. 값이 클수록 재채점 비용은 늘어나지만 재현율은 향상됩니다. 기본값은 `1`(오버샘플링 없음)이므로, 좋은 재현율을 얻으려면 일반적으로 이 값을 높여야 하며, 예를 들어 `10` 이상으로 설정합니다.

<div id="quantized-codecs-built-for-scale">
  #### 대규모 확장에 맞게 설계됨
</div>

이 코덱이 ClickHouse에 잘 맞는 이유는 비용이 큰 부분인 스캔이야말로 ClickHouse 엔진이 특히 잘 처리하도록 설계된 작업이기 때문입니다.

* **벡터화.** 스캔 커널은 SIMD에 맞게 작성되었으며, CPU가 지원하는 가장 넓은 명령어를 사용할 수 있도록 런타임 디스패치를 수행합니다. 부호 코드 메서드(`rabitq`, `turboquant`)에는 하드웨어 `popcount`를 사용하고, 그 외에는 폭이 넓은 fused-multiply-add를 사용합니다.
* **코어와 파트 전반에서 병렬 처리.** 플랫 스캔은 본질적으로 병렬화가 쉬우며, ClickHouse도 이를 그대로 활용합니다. 거리는 사용 가능한 모든 스레드와 테이블의 모든 파트에 걸쳐 동시에 계산되고, 최종 top-`k` 머지만 직렬로 수행됩니다.
* **분산.** 세그먼트로 나뉜 cluster에서는 작업이 여러 머신으로 분산됩니다. 각 세그먼트가 자체 slice를 병렬로 스캔하고 coordinator가 후보군을 머지합니다.
* **열 지향이며 필터에 친화적.** 양자화된 코드는 자체 컬럼에 저장되며, 압축된 상태로 다른 모든 컬럼과 동일한 I/O 경로를 통해 읽힙니다. 따라서 선택적인 `WHERE`를 사용하면 스캔할 코드 수만 줄어듭니다.
* **별도의 build 단계가 없음.** 코드는 벡터가 기록될 때 생성되고 concatenation으로 머지됩니다. 즉, 생성, 튜닝, 재빌드해야 하는 인덱스가 없으므로 데이터가 적재되는 즉시 table을 검색할 수 있습니다.

이 코드들은 후보를 생성하는 역할만 하며, 그대로 유지되는 전체 정밀도 컬럼이 최종적으로 정확한 순위를 제공합니다.

<div id="approximate-nearest-neighbor-search-qbit">
  ### Quantized Bit (QBit)
</div>

정확 벡터 검색의 속도를 높이는 일반적인 방법 중 하나는 더 낮은 정밀도의 [float data type](/docs/ko/reference/data-types/float)을 사용하는 것입니다.
예를 들어 벡터를 `Array(Float32)` 대신 `Array(BFloat16)`로 저장하면 데이터 크기가 절반으로 줄어들고, 쿼리 런타임도 그에 비례해 감소할 것으로 예상됩니다.
이 방법을 양자화라고 합니다. 계산 속도는 빨라지지만, 모든 벡터를 전수 스캔하더라도 결과의 정확도가 떨어질 수 있습니다.

기존의 양자화 방식에서는 검색 시점과 데이터 저장 시점 모두에서 정밀도가 손실됩니다. 위 예시에서는 `Float32` 대신 `BFloat16`를 저장하므로, 나중에 원하더라도 더 정확한 검색을 수행할 수 없습니다. 한 가지 대안은 데이터를 양자화된 버전과 전체 정밀도 버전으로 각각 저장하는 것입니다. 이 방식은 가능하지만 저장 공간이 중복으로 필요합니다. 원본 데이터가 `Float64`이고 서로 다른 정밀도(16비트, 32비트 또는 전체 64비트)로 검색을 실행하려는 상황을 생각해 보십시오. 이 경우 데이터를 3개의 별도 복사본으로 저장해야 합니다.

ClickHouse는 이러한 한계를 해결하기 위해 Quantized Bit (`QBit`) 데이터 타입을 제공합니다. 주요 특징은 다음과 같습니다.

1. 원본의 전체 정밀도 데이터를 저장합니다.
2. 쿼리 시점에 양자화 정밀도를 지정할 수 있습니다.

이는 데이터를 비트 그룹화된 포맷으로 저장해(즉, 모든 벡터의 i번째 비트를 함께 저장해) 요청한 정밀도 수준만큼만 읽을 수 있도록 함으로써 구현됩니다. 이를 통해 필요할 때는 원본 데이터를 그대로 활용하면서도, 양자화로 I/O와 계산량을 줄여 속도 향상 효과를 얻을 수 있습니다. 최대 정밀도를 선택하면 검색은 정확해집니다.

`QBit` 타입의 컬럼을 선언하려면 다음 구문을 사용하십시오:

```sql theme={null}
column_name QBit(element_type, dimension[, stride])
```

여기서:

* `element_type` – 각 벡터 요소의 타입입니다. 지원되는 타입은 `Int8`, `BFloat16`, `Float32`, `Float64`입니다.
* `dimension` – 각 벡터의 차원입니다.
* `stride` – 선택 사항입니다. `dimension`의 제수로, 차원을 각각 별도의 스트림에 저장되는 `dimension / stride`개의 연속된 그룹으로 나눕니다. 따라서 앞쪽 차원만 대상으로 검색할 경우 더 적은 수의 스트림만 읽으면 됩니다(Matryoshka embeddings에 유용함). 기본값은 `dimension`이며, 이 경우 해당 타입은 stride가 없는 `QBit`와 바이트 단위로 동일합니다. 자세한 내용은 [`QBit` 데이터 타입 페이지](/docs/ko/reference/data-types/qbit)를 참조하십시오.

<div id="qbit-create">
  #### `QBit` 테이블 만들기 및 데이터 추가
</div>

```sql theme={null}
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]);
```

<div id="qbit-search">
  #### `QBit`을 사용한 벡터 검색
</div>

L2 거리를 사용해 단어 'lemon'을 나타내는 벡터의 최근접 이웃을 찾아보겠습니다. 거리 함수의 세 번째 매개변수는 비트 단위의 정밀도를 지정하며, 값이 클수록 정확도는 높아지지만 더 많은 계산이 필요합니다.

`QBit`에서 사용할 수 있는 모든 거리 함수는 [여기](/docs/ko/reference/data-types/qbit#vector-search-functions)에서 확인할 수 있습니다.

**전체 정밀도 검색(64비트):**

```sql theme={null}
SELECT
    word,
    L2DistanceTransposed(vec, [-0.88693672, 1.31532824, -0.51182908, -0.99652702, 0.59907770], 64) AS distance
FROM fruit_animal
ORDER BY distance;
```

```text theme={null}
   ┌─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 │
   └────────┴─────────────────────┘
```

**정밀도를 낮춘 검색:**

```sql theme={null}
SELECT
    word,
    L2DistanceTransposed(vec, [-0.88693672, 1.31532824, -0.51182908, -0.99652702, 0.59907770], 12) AS distance
FROM fruit_animal
ORDER BY distance;
```

```text theme={null}
   ┌─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'이 여전히 가장 가까운 일치 항목입니다.

<div id="qbit-performance">
  #### 성능 고려 사항
</div>

`QBit`의 성능상 이점은 I/O 작업이 줄어든다는 점에 있습니다. 정밀도를 낮추면 스토리지에서 읽어야 할 데이터가 줄어들기 때문입니다. 또한 `QBit`에 `Float32` 데이터가 포함된 경우, 정밀도 매개변수가 16 이하이면 계산량이 감소해 추가적인 이점도 얻을 수 있습니다. 정밀도 매개변수는 정확도와 속도 사이의 절충 관계를 직접 제어합니다.

* **더 높은 정밀도**(원본 데이터 폭에 더 가까움): 결과는 더 정확하지만 쿼리는 더 느림
* **더 낮은 정밀도**: 근사 결과를 더 빠르게 얻을 수 있고, 메모리 사용량도 줄어듦

<div id="references">
  ### 참고
</div>

블로그:

* [ClickHouse를 사용한 벡터 검색 - 1부](https://clickhouse.com/blog/vector-search-clickhouse-p1)
* [ClickHouse를 사용한 벡터 검색 - 2부](https://clickhouse.com/blog/vector-search-clickhouse-p2)
* [쿼리 시점에 정밀도를 선택할 수 있는 벡터 검색 엔진을 만들었습니다](https://clickhouse.com/blog/qbit-vector-search)
