- 精确向量搜索会计算给定点与向量空间中所有点之间的距离。这可以确保达到尽可能高的准确性,也就是说,返回的点一定是真正的最近邻。由于需要穷尽整个向量空间,精确向量搜索在实际应用中可能会过慢。
- 近似向量搜索是指一类技术 (例如图、随机森林等特殊数据结构) ,其计算速度远快于精确向量搜索。其结果精度通常已经“足够好”,能够满足实际使用需求。许多近似技术都提供参数,用于在结果精度和搜索时间之间进行权衡。
vectors 的数组类型列中,例如 Array(Float64)、Array(Float32) 或 Array(BFloat16)。
参考向量是一个常量数组,通过公用表表达式给出。
<DistanceFunction> 用于计算参考点与所有已存储点之间的距离。
这里可以使用任意一种可用的距离函数。
<N> 指定要返回多少个邻居。
精确向量搜索
示例
近似向量搜索
向量相似度索引
向量相似度索引适用于 ClickHouse 25.8 及更高版本。
如果遇到问题,欢迎在 ClickHouse 仓库中提交 issue。
创建向量相似度索引
ALTER TABLE 语句只会为今后新插入表中的数据构建索引。
若要同时为现有数据构建索引,则需要将其物化:
<distance_function> 必须是
L2Distance,即 Euclidean distance (欧几里得距离) ,表示欧几里得空间中两点之间的直线距离,cosineDistance,即 cosine distance (余弦距离) ,表示两个非零向量之间的夹角,或dotProduct,即 dot product (内积) ,表示两个向量对应元素乘积之和。在归一化数据上,它等价于cosineDistance。
L2Distance 是最佳选择;否则,建议使用 cosineDistance 来补偿标度差异。
对于距离函数
L2Distance 和 cosineDistance,值越小表示相似度越高;而对于 dotProduct,值越大表示相似度越高。
因此,使用 L2Distance 和 cosineDistance 的向量索引只能用于 SELECT [...] ORDER BY [...] ASC 查询 (ASC 是 ORDER BY 的默认值) ,而为 dotProduct 构建的向量索引只能用于 SELECT [...] ORDER BY [...] DESC 查询。<dimensions> 指定底层列中数组的元素个数。
如果 ClickHouse 在创建索引时发现数组的元素个数不一致,则会放弃该索引并返回错误。
可选的 GRANULARITY 参数 <N> 表示索引粒度的大小 (见此处) 。
与默认索引粒度为 1 的常规跳过索引不同,向量相似度索引默认使用 1 亿作为索引粒度。
这个值可确保即使对于较大的 parts,内部构建的索引数量也很少。
我们建议仅由理解其影响的高级用户修改索引粒度 (见下文) 。
向量相似度索引是通用的,也就是说它们可以支持不同的近似搜索方法。
实际使用的方法由参数 <type> 指定。
截至目前,唯一可用的方法是 HNSW (学术论文) ,这是一种流行且先进的近似向量搜索技术,基于分层近邻图。
如果使用 HNSW 作为 <type>,用户还可以选择指定更多 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表示使用默认值。
- 向量相似度索引只能构建在类型为 Array(Float32)、Array(Float64) 或 Array(BFloat16) 的列上。不允许使用可空浮点数组和低基数浮点数组,例如
Array(Nullable(Float32))和Array(LowCardinality(Float32))。 - 向量相似度索引必须构建在单个列上。
- 向量相似度索引可以构建在计算表达式上 (例如
INDEX index_name arraySort(vectors) TYPE vector_similarity([...])) ,但这类索引之后无法用于近似邻居搜索。 - 向量相似度索引要求底层列中的所有数组都必须包含
<dimension>个元素——这一点会在创建索引时进行检查。为了尽早发现不满足此要求的情况,用户可以为向量列添加一个约束,例如CONSTRAINT same_length CHECK length(vectors) = 256。 - 同样,底层列中的数组值不能为空 (
[]) ,也不能为默认值 (默认值同样是[]) 。
使用向量相似度索引
要使用向量相似度索引,compatibility 设置必须为
'' (默认值) ,或者为 '25.1' 或更高版本。SELECT [...] SETTINGS hnsw_candidate_list_size_for_search = <value>) 。
该设置项的默认值 256 在绝大多数场景下均能满足需求。
设置值越高,精度越好,但性能开销也越大。
如果查询可以使用向量相似度索引,ClickHouse 会检查 SELECT 查询中提供的 LIMIT <N> 是否在合理范围内。
更具体地说,如果 <N> 大于设置项 max_limit_for_vector_search_queries 的值 (默认值为 100) ,则会返回错误。
过大的 LIMIT 值会降低搜索速度,通常意味着存在使用方式上的错误。
要检查 SELECT 查询是否使用了向量相似度索引,可以在查询前添加 EXPLAIN indexes = 1。
以下示例中,查询
Skip 以及向量索引的名称和类型 (在本示例中为 idx 和 vector_similarity) ,则表示向量相似度索引已生效。
在本例中,向量相似度索引跳过了四个粒度中的两个,即 50% 的数据。
可跳过的粒度越多,索引的使用效率就越高。
后过滤与前过滤
用户可以选择在 SELECT 查询中通过 WHERE 子句指定额外的过滤条件。
ClickHouse 将采用后过滤或前过滤策略对这些过滤条件进行求值。
简而言之,两种策略的区别在于过滤条件的求值顺序:
- 后过滤是指先评估向量相似度索引,然后 ClickHouse 再评估
WHERE子句中指定的其他过滤条件。 - 前过滤意味着过滤器的执行顺序正好相反。
- 后过滤的一个普遍问题是,返回的行数可能少于
LIMIT <N>子句请求的数量。当向量相似度索引返回的一行或多行结果不满足附加过滤条件时,就会出现这种情况。 - 前过滤通常仍是一个尚未解决的问题。某些专门的向量数据库提供了前过滤算法,但大多数关系型数据库 (包括 ClickHouse) 都会退回到精确近邻搜索,也就是不使用索引的暴力扫描。
year 进行范围分区,并执行以下查询:
- 如果过滤条件在某个 part 内至少排除一行,ClickHouse 将回退为对该 part 内“保留下来”的范围执行前过滤,
- 如果过滤条件在某个 part 内没有排除任何行,ClickHouse 将对该 part 执行后过滤。
auto,即实现上述启发式策略) 可以设为 prefilter。
这对于额外过滤条件选择性极高的场景很有用,可用于强制启用前过滤。
例如,以下查询可能会从前过滤中受益:
SETTINGS vector_search_filter_strategy = 'prefilter') ,ClickHouse 会先找出所有价格低于 2 美元的图书,然后对这些图书执行暴力穷举向量搜索。
作为解决上述问题的另一种方法,可以将 vector_search_index_fetch_multiplier (默认值:1.0,最大值:1000.0) 配置为大于 1.0 的值 (例如 2.0) 。
从向量索引中拉取的最近邻数量会乘以该设置值,然后再对这些行应用额外的过滤条件,以返回 LIMIT 指定数量的行。
例如,我们可以再次发起查询,但将 multiplier 设置为 3.0:
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。
其大致工作方式是,ClickHouse 将最相似的向量及其距离通过虚拟列 _distance 提供出来。
要查看这一点,请使用 EXPLAIN header = 1 运行向量搜索查询:
在未启用重评分 (
vector_search_with_rescoring = 0) 且启用并行副本的情况下运行的查询,可能仍会回退到重评分。性能调优
CODEC(NONE):
system.text_log) 表明正在加载向量相似度索引。
如果这类消息在不同的向量搜索查询中反复出现,则说明缓存容量过小。
向量相似度索引缓存用于存储向量索引粒度。
如果单个向量索引粒度大于缓存容量,则不会被缓存。
因此,请务必计算向量索引的大小 (根据“估算存储和内存消耗”中的公式,或参考 system.data_skipping_indices) ,并据此设置相应的缓存容量。
与搜索原始全精度浮点值 (
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 提供的模型会生成维度为 1536 甚至 3072 的嵌入向量。
在上述代码中,ClickHouse Python driver 会将嵌入向量替换为便于人类阅读的字符串,随后再将整个 SELECT 查询作为字符串发送。
假设嵌入向量由 1536 个单精度浮点数组成,发送的字符串长度可达 20 kB。
这会导致 CPU 大量消耗在标记化、解析以及成千上万次字符串到浮点数的转换上。
此外,ClickHouse server 日志文件也需要占用大量空间,同时还会导致 system.query_log 膨胀。
请注意,大多数 LLM 模型都会将嵌入向量作为原生浮点数列表或 NumPy 数组返回。
因此,我们建议 Python 应用程序使用以下方式,以二进制形式绑定参考向量参数:
system.query_log 过度膨胀。
管理与监控
与常规跳过索引的差异
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 亿。
示例
Query
Response
使用量化编解码器进行向量搜索
Quantized 编解码器属于 Experimental 功能。请使用 SET allow_experimental_codecs = 1 启用它。
如果你遇到问题,请在 ClickHouse repository 中提交 issue。引言
- 规模。 构建图所需的时间以及存储图所需的内存——还不包括向量本身——会成为主要成本。
- 筛选。 在选择性较强的
WHERE过滤器下,图遍历会变得低效,因为它要么无法到达满足谓词条件的那一小部分行,要么必须检查数量明显偏多的候选项才能找到它们。
Float32 存储的向量,扫描的开销主要受存储 I/O 限制,因为必须从磁盘 (或对象存储) 读取整个向量列——而对于稠密嵌入列,这通常是表中最大的列,并且压缩效果较差。
Quantized 列编解码器正是为了解决这个问题。
每个向量会存储两份:一份是保持不变的原始全精度值,另一份是存放在配套 stream 中的紧凑量化编码。
向量搜索查询会先使用一种低成本、对 SIMD 友好的距离函数扫描这些编码,筛出一份最有希望的候选短名单,然后再基于全精度向量对这份短名单重新排序。
由于编码的大小只是原始向量的一小部分,短名单扫描从存储中读取的字节数会少得多——并且只会为少数入围候选访问全精度列——同时最终排序仍能保持准确性。
声明编解码器
Quantized(...) 编解码器应用于 Array(Float32) (或 Array(Float64) / Array(BFloat16)) 列。
该编解码器属于实验性功能,因此请先启用 allow_experimental_codecs:
ALTER TABLE 添加或修改。
量化方法
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 的倍数。
无感搜索
k 查询即可:
vector_search_use_quantized_codes = 1 时,优化器会自动将查询改写为两阶段执行计划:先扫描量化后的编码以生成候选短名单,然后再用全精度 vec 对候选短名单重新评分。
该 setting 默认关闭,因此如果不启用它,同一查询就会作为普通的精确扫描运行——codec 绝不会改变结果;它只是在你选择启用时提供一条更快的执行路径。
请使用所选 method 支持的距离函数:所有 method 都支持 cosineDistance,此外,int8、prefix 和 product 也支持 L2Distance。
设置
allow_experimental_codecs— 必须启用后才能声明Quantized编解码器 (默认值:0) 。vector_search_use_quantized_codes— 启用“两阶段候选短名单筛选与重评分重写” (默认值:0) 。关闭时,匹配查询会对全精度向量进行精确扫描。vector_search_index_fetch_multiplier— 相对于查询LIMIT需要纳入候选短名单的候选数量:扫描会在重评分之前保留前LIMIT × multiplier个编码。该值越大,召回率越高,但重评分开销也越大。默认值为1(不过采样) ,因此通常需要将其调高——例如调到10或更高——才能获得较好的召回率。
为大规模场景而构建
- 向量化。 扫描内核专为 SIMD 编写,并会在运行时分派到 CPU 支持的最宽指令:对符号码方法 (
rabitq、turboquant) 使用硬件popcount,对其他方法则使用宽幅融合乘加。 - 跨 CPU 核心和 parts 并行。 平面扫描天然适合并行执行,而 ClickHouse 也正是这样处理的:会同时在所有可用线程以及表的所有 parts 上计算距离,只有最终的 top-
kmerge 是串行的。 - 分布式。 在分片集群上,这项工作会分散到多台机器上——每个分片并行扫描自己的那部分数据,再由协调器合并候选短名单。
- 列式且便于过滤。 量化编码存放在独立的列中,像其他列一样经过相同的 I/O 路径压缩和读取,因此有选择性的
WHERE只会留下更少需要扫描的编码。 - 无需单独构建。 这些编码会在写入向量时生成,并通过拼接完成 merge——无需构建、调优或重建索引,因此数据一落表,就能立即执行搜索。
量化位 (QBit)
Array(BFloat16) 而不是 Array(Float32),数据大小会减少一半,查询运行时间预计也会相应缩短。
这种方法称为量化。虽然它能加快计算速度,但即使对所有向量进行穷尽扫描,也可能会降低结果的准确性。
使用传统量化时,我们在搜索和数据存储这两个阶段都会损失精度。以上述示例为例,我们存储的是 BFloat16 而不是 Float32,这意味着即使之后有需要,也无法再进行更高精度的搜索。另一种方案是同时存储两份数据:一份量化后的数据和一份全精度数据。虽然这样可行,但会带来冗余存储。设想这样一种场景:原始数据为 Float64,并且希望使用不同精度 (16 位、32 位或完整 64 位) 进行搜索。这样一来,就需要存储三份独立的数据副本。
ClickHouse 提供了 Quantized Bit (QBit) 数据类型,通过以下方式解决这些限制:
- 存储原始的全精度数据。
- 支持在查询时指定量化精度。
QBit 类型的列,请使用以下语法:
element_type– 每个向量元素的数据类型。支持的类型有Int8、BFloat16、Float32和Float64dimension– 每个向量中的元素个数stride– 可选。dimension的一个除数,它会将各维度划分为dimension / stride个连续分组,并分别存储在独立的流中,因此如果只对前几个维度进行搜索,需要读取的流就更少 (对 Matryoshka 嵌入向量很有用) 。默认值为dimension,此时该类型在字节级别上与非 stride 的QBit完全相同。详见QBit数据类型页面。
创建 QBit 表并插入数据
使用 QBit 进行向量搜索
QBit 支持的所有距离函数。
全精度搜索 (64 位) :
性能考量
QBit 的性能优势来自 I/O 操作减少:使用较低精度时,需要从存储中读取的数据更少。此外,当 QBit 包含 Float32 数据且精度参数为 16 或更低时,由于计算量减少,还能获得额外的性能收益。精度参数直接决定了准确性与速度之间的权衡:
- 更高精度 (更接近原始数据位宽) :结果更准确,但查询更慢
- 更低精度:查询更快,但结果是近似值,内存占用更低