Skip to main content
在本指南中,您将:
  • 简要了解向量搜索
  • 了解近似最近邻 (ANN) 和 分层可导航小世界 (HNSW)
  • 了解 Quantised Bit (QBit)
  • 使用 QBit 基于 DBPedia 数据集执行向量搜索

向量搜索入门

在数学和物理学中,向量被正式定义为同时具有大小和方向的对象。 它通常表现为线段或空间中的箭头,可用于表示速度、力和加速度等量。 在计算机科学中,向量是有限的数字序列。 换句话说,它是一种用于存储数值的数据结构。 在机器学习中,向量与我们在计算机科学中讨论的是同一种数据结构,但其中存储的数值具有特殊含义。 当我们取一段文本或一张图像,并将其提炼为它所表达的关键概念时,这个过程称为编码。 生成的输出是机器以数值形式对这些关键概念的表示。 这就是 嵌入向量,存储在向量中。 换个说法,当这种上下文含义被嵌入到向量中时,我们就可以将其称为 嵌入向量。 如今,向量搜索已经无处不在。 它支撑着音乐推荐、用于大语言模型的检索增强生成 (RAG) (通过拉取外部知识来改进回答) ,甚至连使用 Google 搜索在某种程度上也依赖向量搜索。 尽管专用向量数据库有其优势,用户通常仍更偏好具备即席向量能力的常规数据库,而不是完全专用的向量存储。 ClickHouse 支持暴力向量搜索,以及近似最近邻 (ANN) 搜索方法,其中包括 HNSW——这是当前快速向量检索的标准。

理解嵌入向量

我们通过一个简单示例来了解向量搜索的工作原理。 先来看单词的嵌入向量 (向量表示) : 创建下表,并填入一些示例嵌入向量:
您可以搜索与给定嵌入向量最相近的词语:
该查询嵌入向量与 “apple” 最接近 (距离最小) ;如果将这两个嵌入向量并排比较,这一点就很好理解:

近似最近邻 (ANN)

对于大型数据集,暴力搜索的速度会变得过慢。 这时就需要使用近似最近邻方法了。

量化

量化涉及将数据向下转换为更小的数值类型。 数值类型越小,数据占用就越少;数据越少,距离计算就越快。 ClickHouse 的向量化查询执行引擎可以在每次操作中将更多值装入处理器寄存器,从而直接提升吞吐量。 你有两种选择:
  1. 将量化后的副本与原始列一并保留 - 这会使存储翻倍,但更安全,因为我们始终可以回退到完整精度
  2. 完全替换原始值 (通过在插入时向下转换) - 这样可以节省空间和 I/O,但这是不可逆的

分层可导航小世界 (HNSW)

HNSW 由多层节点 (向量) 组成。每个节点会被随机分配到一层或多层中,而且出现在更高层的概率会呈指数下降。 执行搜索时,我们从顶层的某个节点出发,以贪心方式朝最近邻移动。一旦找不到更近的节点,就会下探到下一层、更稠密的一层。 由于这种分层设计,HNSW 的搜索复杂度相对于节点数量可达到对数级。
HNSW 局限性主要瓶颈在于内存。ClickHouse 使用 HNSW 的 usearch 实现,它是一种驻留内存的数据结构,不支持拆分。 因此,数据集越大,所需的 RAM 也会相应增加。

方法对比

QBit 深入剖析

Quantised Bit (QBit)

QBit 是一种新的数据结构,它利用浮点数按位表示的特性来存储 BFloat16Float32Float64 值。 QBit 并不是将每个数字作为一个整体存储,而是将这些值拆分为位平面:所有第 1 位、所有第 2 位、所有第 3 位,依此类推。 这种方法解决了传统量化的主要局限。既不需要存储重复数据,也无需承担数值失去意义的风险。由于 QBit 直接基于已存储的数据工作,而不是维护内存中的索引,因此它也避开了 HNSW 的 RAM 瓶颈。
优势最重要的是,无需预先做出取舍。 精度和性能可以在查询时动态调整,让用户能够以最小成本探索准确性与速度之间的平衡。
局限性尽管 QBit 能加速向量搜索,但其计算复杂度仍然是 O(n)。换句话说,如果你的数据集足够小,HNSW 索引可以轻松装入 RAM,那它仍然是最快的选择。

数据类型

下面介绍如何创建 QBit 类型的列:
当数据插入 QBit 列时,会经过转置:所有第 1 位排在一起、所有第 2 位排在一起,依此类推。我们将这些称为 每个组都存储在单独的 FixedString(N) 列中:也就是长度固定为 N 字节的字符串,在内存中连续存放,彼此之间没有分隔符。随后,所有这些组会被打包到一个 Tuple 中,构成 QBit 的底层结构。 示例: 如果从一个包含 8×Float64 元素的向量开始,每个组将包含 8 位。由于一个 Float64 有 64 位,最终会得到 64 个组 (每一位对应一个组) 。因此,QBit(Float64, 8) 的内部布局看起来就像一个由 64×FixedString(1) 列组成的 Tuple。
如果原始向量长度不能被 8 整除,则会用不可见元素对结构进行填充,使其按 8 对齐。这样可确保与 FixedString 兼容,因为它只能严格处理完整字节。

距离计算

要使用 QBit 执行查询,请使用带有 precision 参数的 L2DistanceTransposed 函数:
第三个参数 (16) 指定精度级别,以位为单位。

I/O 优化

在计算距离之前,必须先从磁盘读取所需数据,然后进行逆转置 (即将分组的位表示还原为完整向量) 。由于 QBit 按精度级别以位转置形式存储数值,ClickHouse 只需读取重建到目标精度所需的高位位平面。 在上面的查询中,我们使用的精度级别为 16。由于 Float64 有 64 位,我们只需读取前 16 个位平面,跳过 75% 的数据 读取后,我们只根据已加载的位平面重建每个数值的高位部分,未读取的位则保留为 0。

计算优化

有人可能会问,将其转换为更小的类型 (如 Float32 或 BFloat16) 是否能消除这部分未使用的空间。确实可以,但如果对每一行都显式进行类型转换,开销会很高。 相反,我们可以只对参考向量做降精度处理,并将 QBit 数据视为包含更窄类型的值 (即“忽略”某些列的存在) ,因为它的布局通常对应于这些类型的截断版本。

BFloat16 优化

BFloat16 是将 Float32 的精度截去一半后得到的格式。它保留了相同的符号位和 8 位指数,但在 23 位尾数中只保留最高 7 位。因此,读取 QBit 列的前 16 个位平面,实际上就能还原出 BFloat16 值的布局。所以在这种情况下,我们可以 (并且确实会) 安全地将参考向量转换为 BFloat16。

Float64 的复杂性

不过,Float64 就完全不同了。它使用 11 位指数和 52 位尾数,这意味着它并不是简单地把 Float32 的位数翻倍。它的结构和指数偏置都完全不同。将 Float64 向下转换为更小的格式 (如 Float32) 时,需要进行真正的 IEEE-754 转换,也就是将每个值舍入到最接近的可表示 Float32 值。这个舍入步骤的计算开销很高。
如果你想深入了解 QBit 的性能相关因素,请参阅 “Let’s vectorize”

DBpedia 示例

让我们通过一个真实场景的示例看看 QBit 的实际表现。这里使用的是 DBpedia 数据集,其中包含 100 万篇以 Float32 嵌入向量表示的 Wikipedia 文章。

准备

首先,创建表
通过命令行插入数据:
插入数据可能需要一些时间。 不妨去喝杯咖啡休息一下!
或者,也可以按如下所示分别运行 SQL 语句来加载这 25 个 Parquet 文件:
确认 dbpedia 表中有 100 万行数据:
接下来,添加一个 QBit 列:

搜索查询

我们来查找与这些太空相关搜索词最密切相关的概念:Moon、Apollo 11、Space Shuttle、Astronaut、Rocket:
该查询会针对这五个概念中的每一个,找出语义上最相近的前 1000 个条目。 它会返回至少出现在其中三个结果中的条目,并按匹配概念的数量以及与其中任一概念的最小距离 (不包括原始概念) 进行排序。 仅使用 5 位 (1 个符号位 + 4 个指数位,尾数为零) :
性能: 结果集中有 10 行。耗时:0.271 秒。已处理 846 万行,4.54 GB (3119 万行/秒,16.75 GB/秒) 。峰值内存占用:739.82 MiB
性能: 结果集 10 行。耗时:1.157 秒。已处理 1000 万行,32.76 GB (864 万行/秒,28.32 GB/秒) 。峰值内存占用:6.05 GiB

关键洞察

结果如何?不只是好,而是好得出乎意料。乍看之下,很难相信一个浮点数在去掉整个尾数和一半指数后,竟然还能保留有意义的信息。 QBit 的关键洞察在于:即使忽略不重要的位,向量搜索依然可行。 内存占用从 6.05 GB 降至 740 MB,同时仍保持了出色的语义搜索质量!

结论

QBit 是一种将浮点数存储为位平面的列类型。 它允许你在向量搜索时选择要读取的位数,从而在不更改数据的情况下调节召回率和性能。 每种向量搜索方法都有各自的参数,用于决定召回率、准确性和性能之间的权衡。 通常,这些参数都必须提前选定。 如果选错了,就会浪费大量时间和资源,而后续再调整方向也会变得十分麻烦。 有了 QBit,就不必过早做出决定。 你可以直接在查询时调整精度与速度之间的权衡,并在使用过程中逐步找到合适的平衡点。
改编自 Raufs Dunamalijevs 的博客文章,发表于 2025 年 10 月 28 日
最后修改于 2026年7月3日