> ## 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/zh/reference/data-types/array)、[Array(Float32)](/docs/zh/reference/data-types/array) 或 [Array(BFloat16)](/docs/zh/reference/data-types/array)。
参考向量是一个常量数组，通过公用表表达式给出。
`<DistanceFunction>` 用于计算参考点与所有已存储点之间的距离。
这里可以使用任意一种可用的[距离函数](/docs/zh/reference/functions/regular-functions/distance-functions)。
`<N>` 指定要返回多少个邻居。

<div id="exact-nearest-neighbor-search">
  ## 精确向量搜索
</div>

可以直接使用上述 SELECT 查询按原样执行精确向量搜索。
这类查询的运行时间通常与已存储向量的数量及其维度 (即数组元素个数) 成正比。
此外，由于 ClickHouse 会对所有向量执行暴力扫描，运行时间还取决于该查询使用的线程数 (请参见设置 [max\_threads](/docs/zh/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)中提交 issue。
</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/zh/reference/engines/table-engines/mergetree-family/mergetree#table_engine-mergetree-data_skipping-indexes)和[此处](/docs/zh/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) (余弦距离) ，表示两个非零向量之间的夹角，或
* `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>` 指定底层列中数组的元素个数。
如果 ClickHouse 在创建索引时发现数组的元素个数不一致，则会放弃该索引并返回错误。

可选的 GRANULARITY 参数 `<N>` 表示索引粒度的大小 (见[此处](/docs/zh/concepts/features/performance/skip-indexes/skipping-indexes)) 。
与默认索引粒度为 1 的常规跳过索引不同，向量相似度索引默认使用 1 亿作为索引粒度。
这个值可确保即使对于较大的 parts，内部构建的索引数量也很少。
我们建议仅由理解其影响的高级用户修改索引粒度 (见[下文](#differences-to-regular-skipping-indexes)) 。

向量相似度索引是通用的，也就是说它们可以支持不同的近似搜索方法。
实际使用的方法由参数 `<type>` 指定。
截至目前，唯一可用的方法是 HNSW ([学术论文](https://arxiv.org/abs/1603.09320)) ，这是一种流行且先进的近似向量搜索技术，基于分层近邻图。
如果使用 HNSW 作为 `<type>`，用户还可以选择指定更多 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/zh/reference/data-types/array)、[Array(Float64)](/docs/zh/reference/data-types/array) 或 [Array(BFloat16)](/docs/zh/reference/data-types/array) 的列上。不允许使用可空浮点数组和低基数浮点数组，例如 `Array(Nullable(Float32))` 和 `Array(LowCardinality(Float32))`。
* 向量相似度索引必须构建在单个列上。
* 向量相似度索引可以构建在计算表达式上 (例如 `INDEX index_name arraySort(vectors) TYPE vector_similarity([...])`) ，但这类索引之后无法用于近似邻居搜索。
* 向量相似度索引要求底层列中的所有数组都必须包含 `<dimension>` 个元素——这一点会在创建索引时进行检查。为了尽早发现不满足此要求的情况，用户可以为向量列添加一个[约束](/docs/zh/reference/statements/create/table#constraints)，例如 `CONSTRAINT same_length CHECK length(vectors) = 256`。
* 同样，底层列中的数组值不能为空 (`[]`) ，也不能为默认值 (默认值同样是 `[]`) 。

**估算存储和内存消耗**

为典型 AI 模型 (例如大语言模型 [LLM](https://en.wikipedia.org/wiki/Large_language_model)) 生成的向量，通常由数百个或数千个浮点值组成。
因此，单个向量值可能会占用数 KB 的内存。
如果用户想估算表中底层向量列所需的存储空间，以及向量相似度索引所需的主内存，可以使用下面两个公式：

表中向量列的存储消耗 (未压缩) ：

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

以 [dbpedia dataset](https://huggingface.co/datasets/KShivendu/dbpedia-entities-openai-1M) 为例：

```text theme={null}
存储消耗 = 100万 * 1536 * 4（Float32）= 6.1 GB
```

必须先将向量相似度索引从磁盘完整加载到主内存中，才能执行搜索。
同样，向量索引也是先在内存中完整构建，然后再保存到磁盘。

加载向量索引所需的内存占用：

```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}
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
```

上述公式未将向量相似度索引用于分配预分配缓冲区、缓存等运行时数据结构所需的额外内存计算在内。

<div id="using-a-vector-similarity-index">
  #### 使用向量相似度索引
</div>

<Note>
  要使用向量相似度索引，[compatibility](/docs/zh/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/zh/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/zh/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 万个向量 (每个向量维度为 1536) 存储在 575 个粒度中，即每个粒度约 1.7k 行。该查询请求 10 个最近邻，向量相似度索引在 10 个独立粒度中找到这 10 个最近邻。查询执行期间将读取这 10 个粒度。

如果输出中包含 `Skip` 以及向量索引的名称和类型 (在本示例中为 `idx` 和 `vector_similarity`) ，则表示向量相似度索引已生效。
在本例中，向量相似度索引跳过了四个粒度中的两个，即 50% 的数据。
可跳过的粒度越多，索引的使用效率就越高。

<Tip>
  要强制使用索引，你可以在运行 SELECT 查询时启用设置 [force\_data\_skipping\_indexes](/docs/zh/reference/settings/session-settings#force_data_skipping_indices) (将索引名称作为该设置的值) 。
</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/zh/reference/engines/table-engines/mergetree-family/mergetree#primary-key)评估 (即它们构成主键的前缀) ，并且

* 如果过滤条件在某个 part 内至少排除一行，ClickHouse 将回退为对该 part 内“保留下来”的范围执行前过滤，
* 如果过滤条件在某个 part 内没有排除任何行，ClickHouse 将对该 part 执行后过滤。

在实际场景中，后一种情况通常不太可能发生。

*可以使用跳过索引评估额外过滤条件*

如果额外过滤条件可以通过[跳过索引](/docs/zh/reference/engines/table-engines/mergetree-family/mergetree#table_engine-mergetree-data_skipping-indexes) (minmax 索引、set 索引等) 评估，ClickHouse 会执行后过滤。
在这种情况下，会先评估向量相似度索引，因为预计与其他跳过索引相比，它能过滤掉更多行。

若要更细致地控制后过滤与前过滤，可以使用两个设置：

设置 [vector\_search\_filter\_strategy](/docs/zh/reference/settings/session-settings#vector_search_filter_strategy) (默认值：`auto`，即实现上述启发式策略) 可以设为 `prefilter`。
这对于额外过滤条件选择性极高的场景很有用，可用于强制启用前过滤。
例如，以下查询可能会从前过滤中受益：

```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 美元的图书极少，后过滤可能会返回零行，因为向量索引返回的前 10 个匹配项价格都可能高于 2 美元。
通过强制使用前过滤 (在查询中添加 `SETTINGS vector_search_filter_strategy = 'prefilter'`) ，ClickHouse 会先找出所有价格低于 2 美元的图书，然后对这些图书执行暴力穷举向量搜索。

作为解决上述问题的另一种方法，可以将 [vector\_search\_index\_fetch\_multiplier](/docs/zh/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 将从每个分片中的向量索引拉取 3.0 x 10 = 30 个最近邻，然后再评估额外的过滤器。
最终只会返回距离最近的十个近邻。
需要注意的是，设置 `vector_search_index_fetch_multiplier` 可以缓解这个问题，但在极端情况下 (WHERE 条件选择性非常强) ，返回的行数仍可能少于请求的 N 行。

**重评分**

ClickHouse 中的跳过索引通常在粒度级别进行过滤。也就是说， (在内部) 查询一次跳过索引会返回一个可能匹配的粒度列表，从而减少后续扫描中需要读取的数据量。
这对一般的跳过索引来说效果很好，但对于向量相似度索引，则会造成“粒度不匹配”。
更具体地说，向量相似度索引会针对给定的参考向量找出最相似的 N 个向量的行号。
在设置 `vector_search_with_rescoring = 1` 时，ClickHouse 会读取候选行中原始的全精度向量，并在常规 SQL 管道中计算最终距离。
当查询计划允许时，ClickHouse 会在最终距离计算之前，将扫描结果过滤为向量索引返回的候选行。
这一步称为重评分，它可以提高准确性，尤其是在量化向量索引中，因为最终排名使用的是存储的向量，而不是索引距离。
如果附加过滤器移除了过多候选项，或者需要更高的召回率，请增大设置 `vector_search_index_fetch_multiplier`，以便向量索引返回更多候选行用于重评分。

因此，ClickHouse 提供了一种优化，可禁用重评分，并直接从索引返回最相似的向量及其距离。
该优化默认启用，参见设置 [vector\_search\_with\_rescoring](/docs/zh/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>
  在未启用重评分 (`vector_search_with_rescoring = 0`) 且启用并行副本的情况下运行的查询，可能仍会回退到重评分。
</Note>

<div id="performance-tuning">
  #### 性能调优
</div>

**压缩调优**

在几乎所有使用场景中，底层列中的向量都是稠密的，因此通常难以有效压缩。
因此，[压缩](/docs/zh/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;
```

**调优索引创建**

向量相似度索引的生命周期与 parts 的生命周期密切相关。
换句话说，每当创建一个定义了向量相似度索引的新 part 时，对应的索引也会同时创建。
这通常发生在数据[插入](/docs/zh/concepts/features/operations/insert/inserting-data)时，或在[合并](/docs/zh/concepts/core-concepts/merges)过程中。
遗憾的是，HNSW 的索引创建耗时较长是众所周知的问题，这会显著拖慢插入和合并操作。
因此，向量相似度索引更适合用于数据不可变或很少发生变化的场景。

要加快索引创建，可以采用以下方法：

首先，可以将索引创建并行化。
可通过服务器设置 [max\_build\_vector\_similarity\_index\_thread\_pool\_size](/docs/zh/reference/settings/server-settings/settings#max_build_vector_similarity_index_thread_pool_size) 配置用于创建索引的最大线程数。
为获得最佳性能，建议将该设置值配置为 CPU 核心数。

其次，为了加快 INSERT 语句的执行，用户可以通过会话设置 [materialize\_skip\_indexes\_on\_insert](/docs/zh/reference/settings/session-settings#materialize_skip_indexes_on_insert) 禁止在新插入的 parts 上创建跳过索引。
对此类 parts 执行的 SELECT 查询将回退为精确搜索。
由于新插入的 parts 相对于整个表通常较小，因此预计对性能的影响可以忽略不计。

第三，为了加快合并，用户可以通过会话设置 [materialize\_skip\_indexes\_on\_merge](/docs/zh/reference/settings/merge-tree-settings#materialize_skip_indexes_on_merge) 禁止在已合并的 parts 上创建跳过索引。
配合语句 [ALTER TABLE \[...\] MATERIALIZE INDEX \[...\]](/docs/zh/reference/statements/alter/skipping-index#materialize-index)，这可以显式控制向量相似度索引的生命周期。
例如，可以将索引创建延后到所有数据都已摄取完成之后，或延后到系统负载较低的时段 (如周末) 再进行。

**调优索引使用**

SELECT 查询要使用向量相似度索引，需要先将其加载到主内存中。
为避免重复将同一个向量相似度索引加载到主内存，ClickHouse 为这类索引提供了专用的内存缓存。
缓存越大，不必要的加载就越少。
可通过服务器设置 [vector\_similarity\_index\_cache\_size](/docs/zh/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/zh/reference/system-tables/data_skipping_indices)) ，并据此设置相应的缓存容量。
</Note>

*我们再次强调，在排查向量搜索查询缓慢的问题时，首先应检查向量索引缓存，并在必要时增大其容量。*

当前向量相似度索引缓存的大小可在 [system.metrics](/docs/zh/reference/system-tables/metrics) 中查看：

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

可从 [system.query\_log](/docs/zh/reference/system-tables/query_log) 获取具有特定查询 id 的查询的缓存命中和未命中信息：

```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 向量索引支持以下量化选项：

| Quantization   | Name              | Storage per dimension |
| -------------- | ----------------- | --------------------- |
| f32            | 单精度               | 4 字节                  |
| f16            | 半精度               | 2 字节                  |
| bf16 (default) | 半精度 (brain float) | 2 字节                  |
| i8             | 四分之一精度            | 1 字节                  |
| b1             | 二进制               | 1 比特                  |

与搜索原始全精度浮点值 (`f32`) 相比，量化会降低向量搜索的精度。
不过，在大多数数据集上，半精度 brain float 量化 (`bf16`) 带来的精度损失微乎其微，因此向量相似度索引默认采用这种量化方式。
四分之一精度 (`i8`) 和二进制 (`b1`) 量化会给向量搜索带来较明显的精度损失。
只有在向量相似度索引的大小显著超过可用 DRAM 容量时，我们才建议使用这两种量化方式。
在这种情况下，我们还建议启用重评分 ([vector\_search\_index\_fetch\_multiplier](/docs/zh/reference/settings/session-settings#vector_search_index_fetch_multiplier)、[vector\_search\_with\_rescoring](/docs/zh/reference/settings/session-settings#vector_search_with_rescoring)) 以提高准确性。
只有在以下情况下才建议使用二进制量化：1) 嵌入向量已归一化 (即向量长度 = 1，OpenAI 模型通常是归一化的) ；2) 距离函数使用的是余弦距离。
二进制量化在内部使用 Hamming 距离来构建和搜索邻近图。
重评分步骤会使用表中存储的原始全精度向量，通过余弦距离识别最近邻。

**调整数据传输**

向量搜索查询中的参考向量由用户提供，通常通过调用大型语言模型 (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 server 日志文件也需要占用大量空间，同时还会导致 `system.query_log` 膨胀。

请注意，大多数 LLM 模型都会将嵌入向量作为原生浮点数列表或 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)
```

在该示例中，参考向量会以原始二进制形式发送，并在服务器端重新解释为浮点数数组。
这样可以节省服务器端的 CPU 时间，并避免服务器日志和 `system.query_log` 过度膨胀。

<div id="administration">
  #### 管理与监控
</div>

向量相似度索引的磁盘占用大小可从 [system.data\_skipping\_indices](/docs/zh/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/zh/concepts/features/performance/skip-indexes/skipping-indexes)一样，向量相似度索引也是基于粒度构建的，每个索引块由 `GRANULARITY = [N]` 个粒度组成 (对普通跳过索引来说，`[N]` 的默认值为 1) 。
例如，如果表的主索引粒度为 8192 (设置 `index_granularity = 8192`) ，且 `GRANULARITY = 2`，那么每个索引块将包含 16384 行。
但用于近似邻居搜索的数据结构和算法本质上是面向行的。
它们会存储一组行的紧凑表示，并且也会为向量搜索查询返回行。
因此，向量相似度索引的行为方式与普通跳过索引相比，会表现出一些相当不直观的差异。

当用户在某一列上定义向量相似度索引时，ClickHouse 会在内部为每个索引块创建一个向量相似度“子索引”。
这里所说的“局部”是指，子索引只了解其所属索引块中的行。
沿用前面的示例，假设某一列有 65536 行，那么会得到四个索引块 (跨越八个粒度) ，并为每个索引块创建一个向量相似度子索引。
从理论上讲，一个子索引可以直接返回其索引块内距离最近的 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 亿。

<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/zh/get-started/sample-datasets/laion)
* [LAION-5B](/docs/zh/get-started/sample-datasets/laion5b)
* [dbpedia](/docs/zh/get-started/sample-datasets/dbpedia)
* [hackernews](/docs/zh/get-started/sample-datasets/hacker-news-vector-search)

<div id="vector-search-with-quantized-codecs">
  ### 使用量化编解码器进行向量搜索
</div>

<Note>
  `Quantized` 编解码器属于 Experimental 功能。请使用 `SET allow_experimental_codecs = 1` 启用它。
  如果你遇到问题，请在 [ClickHouse repository](https://github.com/clickhouse/clickhouse/issues) 中提交 issue。
</Note>

<div id="quantized-codecs-introduction">
  #### 引言
</div>

[向量相似度索引](#vector-similarity-index)通过图遍历来响应最近邻查询，并且在图能够驻留内存时性能非常出色。
但有两个因素限制了它的适用性：

* **规模。** 构建图所需的时间以及存储图所需的内存——还不包括向量本身——会成为主要成本。
* **筛选。** 在选择性较强的 `WHERE` 过滤器下，图遍历会变得低效，因为它要么无法到达满足谓词条件的那一小部分行，要么必须检查数量明显偏多的候选项才能找到它们。

穷举扫描不受这两种限制：它不需要任何辅助结构，parts 通过拼接进行合并，而过滤器只会减少需要扫描的行数。
它唯一的缺点是必须读取的数据量：对于以全精度 `Float32` 存储的向量，扫描的开销主要受存储 I/O 限制，因为必须从磁盘 (或对象存储) 读取整个向量列——而对于稠密嵌入列，这通常是表中最大的列，并且压缩效果较差。

`Quantized` 列编解码器正是为了解决这个问题。
每个向量会存储两份：一份是保持不变的原始全精度值，另一份是存放在配套 stream 中的紧凑*量化编码*。
向量搜索查询会先使用一种低成本、对 SIMD 友好的距离函数扫描这些编码，筛出一份最有希望的候选短名单，然后再基于全精度向量对这份短名单重新排序。
由于编码的大小只是原始向量的一小部分，短名单扫描从存储中读取的字节数会少得多——并且只会为少数入围候选访问全精度列——同时最终排序仍能保持准确性。

<div id="quantized-codecs-declaring">
  #### 声明编解码器
</div>

将 `Quantized(...)` 编解码器应用于 `Array(Float32)` (或 `Array(Float64)` / `Array(BFloat16)`) 列。
该编解码器属于实验性功能，因此请先启用 `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;
```

全精度数据仍按常规方式存储；该编解码器只会额外添加配套的码流。
编解码器在创建表时即已固定，无法通过 `ALTER TABLE` 添加或修改。

<div id="quantized-codecs-methods">
  #### 量化方法
</div>

每种方法都对应着大小 / 准确性 / 指标之间权衡的不同取舍点。`dimensions` 参数表示向量长度。

* `Quantized('rabitq', dimensions)` — 每个坐标使用一个符号位，再加上一个无偏余弦校正因子 (`dimensions/8 + 4` 字节) 。体积小、`popcount` 成本低，是一个很强的默认选择。仅支持 `cosineDistance`。
* `Quantized('turboquant', dimensions)` — 每个坐标使用两位 (1 位 MSE 编码和 1 位残差编码) ，可获得保真度更高的候选结果 (`dimensions/4 + 4` 字节) 。仅支持 `cosineDistance`。
* `Quantized('int8', dimensions)` — 每个坐标使用一个 `Int8` 编码，再加上向量范数 (`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 训练得到的码本；每个向量会变成 `m` 个、每个 `nbits` 位的编码 (因此 `dimensions` 必须是 `m` 的倍数) 。这是最紧凑的方案，且单位字节召回率最高，但代价是在 insert 时需要执行训练步骤。支持 `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` 时，优化器会自动将查询改写为两阶段执行计划：先扫描量化后的编码以生成候选短名单，然后再用全精度 `vec` 对候选短名单重新评分。
该 setting 默认关闭，因此如果不启用它，同一查询就会作为普通的精确扫描运行——codec 绝不会改变结果；它只是在你选择启用时提供一条更快的执行路径。
请使用所选 method 支持的距离函数：所有 method 都支持 `cosineDistance`，此外，`int8`、`prefix` 和 `product` 也支持 `L2Distance`。

<div id="quantized-codecs-settings">
  #### 设置
</div>

* `allow_experimental_codecs` — 必须启用后才能声明 `Quantized` 编解码器 (默认值：`0`) 。
* `vector_search_use_quantized_codes` — 启用“两阶段候选短名单筛选与重评分重写” (默认值：`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`，对其他方法则使用宽幅融合乘加。
* **跨 CPU 核心和 parts 并行。** 平面扫描天然适合并行执行，而 ClickHouse 也正是这样处理的：会同时在所有可用线程以及表的所有 parts 上计算距离，只有最终的 top-`k` merge 是串行的。
* **分布式。** 在分片集群上，这项工作会分散到多台机器上——每个分片并行扫描自己的那部分数据，再由协调器合并候选短名单。
* **列式且便于过滤。** 量化编码存放在独立的列中，像其他列一样经过相同的 I/O 路径压缩和读取，因此有选择性的 `WHERE` 只会留下更少需要扫描的编码。
* **无需单独构建。** 这些编码会在写入向量时生成，并通过拼接完成 merge——无需构建、调优或重建索引，因此数据一落表，就能立即执行搜索。

这些编码始终只用于生成候选；原地保留的全精度列则提供最终的精确排序。

<div id="approximate-nearest-neighbor-search-qbit">
  ### 量化位 (QBit)
</div>

加速精确向量搜索的一种常见方法是使用较低精度的[浮点数据类型](/docs/zh/reference/data-types/float)。
例如，如果向量存储为 `Array(BFloat16)` 而不是 `Array(Float32)`，数据大小会减少一半，查询运行时间预计也会相应缩短。
这种方法称为量化。虽然它能加快计算速度，但即使对所有向量进行穷尽扫描，也可能会降低结果的准确性。

使用传统量化时，我们在搜索和数据存储这两个阶段都会损失精度。以上述示例为例，我们存储的是 `BFloat16` 而不是 `Float32`，这意味着即使之后有需要，也无法再进行更高精度的搜索。另一种方案是同时存储两份数据：一份量化后的数据和一份全精度数据。虽然这样可行，但会带来冗余存储。设想这样一种场景：原始数据为 `Float64`，并且希望使用不同精度 (16 位、32 位或完整 64 位) 进行搜索。这样一来，就需要存储三份独立的数据副本。

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 嵌入向量很有用) 。默认值为 `dimension`，此时该类型在字节级别上与非 stride 的 `QBit` 完全相同。详见 [`QBit` 数据类型页面](/docs/zh/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' 的向量最接近的邻居。距离函数中的第三个参数用于指定精度 (以位为单位) ——值越高，精度越高，但所需计算也越多。

你可以在[这里](/docs/zh/reference/data-types/qbit#vector-search-functions)查看 `QBit` 支持的所有距离函数。

**全精度搜索 (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)
