Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

QBit 数据类型

QBit 数据类型会重新组织向量的存储方式,以加快近似向量搜索。它不是将每个向量的元素存储在一起,而是将所有向量中相同二进制位位置的值分组存储。 这样既能以全精度存储向量,又能让你在搜索时选择细粒度的量化级别:读取更少的位可减少 I/O 并加快计算,读取更多的位则可获得更高精度。你既能享受量化在减少数据传输和计算量方面带来的速度优势,又能在需要时使用全部原始数据。

要声明一个 QBit 类型的列,请使用以下语法:

column_name QBit(element_type, dimension[, stride])
  • element_type – 每个向量元素的数据类型。允许的类型包括 Int8BFloat16Float32Float64
  • dimension – 每个向量中的元素个数
  • stride – 可选。一起存储在同一组流中的维度数量。省略时,默认值为 dimension (单组) 。提供该参数时,dimension 必须是 stride 的倍数;并且当 stride 小于 dimension 时,stride 必须是 8 的倍数。dimension 个维度会被拆分为 dimension / stride 个连续分组,每个分组的位平面存储在单独的流中。这样一来,对前 D 个维度进行搜索时 (其中 Dstride 的倍数) ,只需读取覆盖这些维度的各分组对应的流,这对 Matryoshka 嵌入向量很有用。

创建 QBit

在表的列定义中使用 QBit 类型:

CREATE TABLE test (id UInt32, vec QBit(Float32, 8)) ENGINE = Memory;
INSERT INTO test VALUES (1, [1, 2, 3, 4, 5, 6, 7, 8]), (2, [9, 10, 11, 12, 13, 14, 15, 16]);
SELECT vec FROM test ORDER BY id;
┌─vec──────────────────────┐
│ [1,2,3,4,5,6,7,8]        │
│ [9,10,11,12,13,14,15,16] │
└──────────────────────────┘

将数组转换为 QBit

当数组长度与 QBit 的维度一致时,数组会转换为 QBit。数组的元素类型不必与 QBit 的元素类型一致。任何数值类型的元素都会自动转换为 QBit 的元素类型。因此,你可以将现有的嵌入向量列直接迁移到 QBit 列中:

CREATE TABLE embeddings (id UInt32, embedding Array(Float32)) ENGINE = Memory;
INSERT INTO embeddings VALUES (1, [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]), (2, [0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1]);

CREATE TABLE vectors (id UInt32, vec QBit(Float32, 8)) ENGINE = Memory;
INSERT INTO vectors SELECT id, embedding FROM embeddings;

SELECT * FROM vectors ORDER BY id;
┌─id─┬─vec───────────────────────────────┐
│  1 │ [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8] │
│  2 │ [0.8,0.7,0.6,0.5,0.4,0.3,0.2,0.1] │
└────┴───────────────────────────────────┘

这种转换也可以显式地通过 CAST 实现,例如 CAST(embedding AS QBit(Float32, 8))

将 QBit 转换为数组

反向转换会根据按比特转置后的表示重建原始向量,因此将 QBit 转换为 Array 时会返回存储的值。这一过程与将数组转换为 QBit相反:

SELECT [1, 2, 3, 4]::QBit(Float32, 4)::Array(Float32) AS vec;
┌─vec───────┐
│ [1,2,3,4] │
└───────────┘

重建后的数组会使用 QBit 的 元素类型,然后再将其中的元素转换为所请求的数组 元素类型。因此,连同 元素类型 一并更改的转换同样可行,例如从 QBit(Float32, N) 转换为 Array(Float64)

对于 Int8Float32Float64Array -> QBit -> Array 的往返转换是无损的。对于 BFloat16,其结果与直接转换为 BFloat16 一致——唯一损失的精度仅来自 BFloat16 本身。

dimension 不是 8 的倍数时,内部表示中的尾部 padding 元素会被丢弃,因此结果始终恰好包含 dimension 个元素。

QBit 类型之间的转换

只要 dimension (向量元素的数量) 保持不变,QBit 就可以转换为另一种 QBitelement_typestride 都可以改变;如果转换为 dimension 不同的 QBit,则会引发异常,因为这会改变向量本身。

更改 element_type 会重建向量,并将每个元素转换为新类型,这与对应的 Array 转换完全一致:扩宽 (例如从 QBit(Float32, N) 转换为 QBit(Float64, N)) 是精确的,而缩窄则会像缩窄 Array 转换一样损失精度。

SELECT [1, 2, 3, 4]::QBit(Float32, 4)::QBit(Float64, 4) AS vec;
┌─vec───────┐
│ [1,2,3,4] │
└───────────┘

仅更改 stride (保持 element_type 不变) 只会在不改动数值的情况下重新组织已存储的比特平面,因此这一过程始终是无损的:

SELECT range(16)::Array(Float32)::QBit(Float32, 16)::QBit(Float32, 16, 8)::Array(Float32)
     = range(16)::Array(Float32) AS is_lossless;
┌─is_lossless─┐
│           1 │
└─────────────┘

QBit 子列

QBit 实现了一种子列访问模式,使您可以访问已存储向量中的各个位平面。每个比特位都可以通过 .N 语法访问,其中 N 表示该比特位的位置:

CREATE TABLE test (id UInt32, vec QBit(Float32, 8)) ENGINE = Memory;
INSERT INTO test VALUES (1, [0, 0, 0, 0, 0, 0, 0, 0]);
INSERT INTO test VALUES (1, [-0, -0, -0, -0, -0, -0, -0, -0]);
SELECT bin(vec.1) FROM test;
┌─bin(tupleElement(vec, 1))─┐
│ 00000000                  │
│ 11111111                  │
└───────────────────────────┘

可访问的子列数量取决于元素类型 (如果使用 stride,还取决于 stride 组的数量):

  • Int8:每个 stride 组 8 个子列 (1-8)
  • BFloat16:每个 stride 组 16 个子列 (1-16)
  • Float32:每个 stride 组 32 个子列 (1-32)
  • Float64:每个 stride 组 64 个子列 (1-64)

这些子列遵循按组优先的顺序:一般来说,vec.N 读取 stride 组 (N-1) / element_size 中的比特平面 (N-1) % element_size。例如,对于 QBit(BFloat16, 4096, 1024),4096 个维度被拆分为 4 个 1024 维的组,因此共有 64 个子列:vec.1vec.16 是第一个 stride 组 (维度 1–1024) 的比特平面,vec.17vec.32 属于第二个组 (维度 1025–2048) ,依此类推。

Stride

默认情况下,QBit 会将每个位平面存储为一个跨越全部 dimension 维度的单个流,因此搜索时总是需要读取整个向量的完整位平面。可选的 stride 参数会将 dimension 维度划分为 dimension / stride 个连续分组,并将每个分组的位平面分别存储到独立的流中。这样,如果搜索只涉及前 D 个维度 (其中 Dstride 的倍数) ,就只需读取覆盖这些维度的那些分组对应的流。这对于 Matryoshka 嵌入向量 很有用,因为靠前的维度可以形成一个可用的低维嵌入向量。

CREATE TABLE test (id UInt32, vec QBit(BFloat16, 4096, 1024)) ENGINE = MergeTree ORDER BY id;

这里将 4096 个维度拆分为 4 组,每组 1024 个。子列遵循按组优先的顺序:对于 BFloat16 (16 个比特平面) ,vec.1vec.16 是第一个 stride 组 (维度 1–1024) 的 16 个比特平面,vec.17vec.32 属于第二个组 (维度 1025–2048) ,依此类推。一般情况下,vec.N 读取 stride 组 (N-1) / element_size 中的比特平面 (N-1) % element_size

要执行 reduced-dimension 搜索,请将要读取的维度数作为转置距离函数的第四个参数传入 (见下文) 。参考向量必须至少包含这么多个元素 (任何额外的尾部元素都会被忽略) ,并且该值必须是 stride 的倍数。

元素访问与切片

arrayElement (以及 vec[n] 运算符) 返回第 n 个向量元素,并以元素类型的全精度重建。仅读取包含该元素的 stride 组中的位平面。arraySlice 返回所选维度上的 QBit (即投影到部分维度) ;偏移量和长度必须为常量,因为 QBit 的维度是其类型的一部分。与 stride 组边界对齐的切片会保留步长,并复用已存储的位平面流,无需复制:

CREATE TABLE test (id UInt32, vec QBit(Float32, 8)) ENGINE = Memory;
INSERT INTO test VALUES (1, [1, 2, 3, 4, 5, 6, 7, 8]);
SELECT vec[3], arraySlice(vec, 2, 3) FROM test;
┌─arrayElement(vec, 3)─┬─arraySlice(vec, 2, 3)─┐
│                    3 │ [2,3,4]               │
└──────────────────────┴───────────────────────┘

向量搜索函数

以下是用于向量相似搜索、且使用 QBit 数据类型的距离函数:

对于带 stride 的 QBit,这些函数接受一个可选的第四个参数 used_dims——即要读取的前几个维度——仅会读取覆盖这些维度的 stride 组。参考向量必须至少包含 used_dims 个元素 (任何额外的末尾元素都会被忽略,因此完整大小的查询向量可直接复用于降维搜索,而无需先对其进行切片) ,并且 used_dims 必须是 stride 的倍数。

Navigation