StarRocks cosine_similarity_norm 函数:面向预归一化向量的余弦相似度计算与源码实现解析
【免费下载链接】starrocksThe world's fastest open query engine for sub-second analytics both on and off the data lakehouse. With the flexibility to support nearly any scenario, StarRocks provides best-in-class performance for multi-dimensional analytics, real-time analytics, and ad-hoc queries. A Linux Foundation project.项目地址: https://gitcode.com/GitHub_Trending/st/starrocks
cosine_similarity_norm是 StarRocks 数学函数族中的一个向量相似度函数,用于在输入向量已经完成归一化的前提下,通过计算两个向量夹角的余弦值来度量其方向上的相似程度。本篇完整覆盖该函数的语法、参数、返回值语义与可复制的 SQL 实战示例,并结合 StarRocks 后端(BE)源码,深入讲解它与cosine_similarity在实现路径上的关键差异、SIMD 优化细节以及错误处理机制。读完之后,你既能直接在查询中使用该函数完成向量相似度排序,也能理解"norm 版本更快"这一结论在源码层面的确切含义。
一、函数语义:基于夹角的相似度度量
cosine_similarity_norm通过计算两个向量夹角的余弦值来度量它们的相似性。夹角只由向量的方向决定,向量的模长(magnitude)差异会被忽略——这正是"余弦"度量的核心思想。
该函数有一个前提假设:输入向量应当是已经归一化(单位化)的。如果需要在计算相似度之前先对向量做归一化,应改用 cosine_similarity,它会在内部完成范数计算与归一化。
相似度取值范围在-1 到 1之间,夹角越小,余弦相似度越大:
| 向量关系 | 夹角 | 余弦相似度 |
|---|---|---|
| 方向相同(平行同向) | 0° | 1 |
| 相互垂直 | 90° | 0 |
| 方向相反(平行反向) | 180° | -1 |
这个区间约束有一个隐含前提:[-1, 1]只对归一化后的单位向量严格成立。如果传入的向量并未归一化,结果实际上是两个向量的点积,可能超出该区间——这一点在源码实现部分会得到印证。
二、语法与参数
语法
cosine_similarity_norm(a, b)参数说明
a和b:待比较的两个向量,必须具有相同的维度(即相同的元素个数)。- 支持的数据类型为
Array<float>。 - 两个数组的元素个数必须一致,否则返回错误。
返回值
- 返回一个
FLOAT值,范围 [-1, 1](在输入为归一化向量的前提下)。 - 如果任一输入参数为 NULL 或非法(维度不匹配等),将报告错误。
函数注册信息
在 StarRocks 的函数注册表中可以确认该函数的完整签名。gensrc/script/functions.py 中定义了三个向量相似度相关函数:
[10102, "cosine_similarity", True, False, "FLOAT", ["ARRAY_FLOAT", "ARRAY_FLOAT"], "MathFunctions::cosine_similarity<TYPE_FLOAT, false>"], [10103, "cosine_similarity_norm", True, False, "FLOAT", ["ARRAY_FLOAT", "ARRAY_FLOAT"], "MathFunctions::cosine_similarity<TYPE_FLOAT, true>"], [10104, "inner_product", True, False, "FLOAT", ["ARRAY_FLOAT", "ARRAY_FLOAT"], "MathFunctions::inner_product<TYPE_FLOAT>"],注册表揭示了两个关键信息:
cosine_similarity_norm的函数 ID 为 10103,入参为两个ARRAY_FLOAT,返回FLOAT;- 它与
cosine_similarity共用同一个 C++ 模板函数MathFunctions::cosine_similarity<TYPE_FLOAT, ...>,区别仅在于第二个模板参数——norm 版本为true。这正是理解两个函数性能差异的入口。
三、实战示例:向量相似度排序
以下示例完整继承自官方文档,可直接在 StarRocks 中执行。
1. 建表并插入向量数据
CREATE TABLE t1_similarity (id int, data array<float>) DISTRIBUTED BY HASH(id); INSERT INTO t1_similarity VALUES (1, array<float>[0.1, 0.2, 0.3]), (2, array<float>[0.2, 0.1, 0.3]), (3, array<float>[0.3, 0.2, 0.1]);2. 计算每行向量与目标数组的相似度并降序排序
SELECT id, data, cosine_similarity_norm([0.1, 0.2, 0.3], data) as dist FROM t1_similarity ORDER BY dist DESC;查询结果:
+------+---------------+------------+ | id | data | dist | +------+---------------+------------+ | 1 | [0.1,0.2,0.3] | 0.14000002 | | 2 | [0.2,0.1,0.3] | 0.13000001 | | 3 | [0.3,0.2,0.1] | 0.10000001 | +------+---------------+------------+注意这里查询参数是常量数组[0.1, 0.2, 0.3],表列data是逐行变化的向量。这种"一个查询向量 vs 一列候选向量"的用法是最典型的向量近邻检索查询形态,后端为其专门提供了常量列快速路径(见后文)。
结果解读与精度说明:结果中的0.14000002、0.13000001、0.10000001这类"毛边"是float单精度浮点运算的正常现象。可以手工验证:示例中的向量并未归一化,例如第 2 行的0.2*0.1 + 0.1*0.2 + 0.3*0.3 = 0.13,这正是两个向量的点积而非严格意义的余弦相似度。若希望得到真正的余弦值,应先将向量归一化,或直接使用cosine_similarity。
四、norm 版本的实现真相:它就是点积
这是理解cosine_similarity_norm最重要的源码细节。
在 BE 表达式执行层 be/src/exprs/math_functions.cpp 中,定义了一个向量相似度算法枚举:
enum class VectorSimilarityAlgorithm { kCosineSimilarity, kNormalizedCosineSimilarity, kInnerProduct, };函数入口MathFunctions::cosine_similarity(math_functions.cpp#L1567-L1574)通过模板布尔参数isNorm选择算法分支:
template <LogicalType TYPE, bool isNorm> StatusOr<ColumnPtr> MathFunctions::cosine_similarity(FunctionContext* context, const Columns& columns) { if constexpr (isNorm) { return vector_similarity<TYPE, VectorSimilarityAlgorithm::kNormalizedCosineSimilarity>(context, columns, "cosine_similarity"); } return vector_similarity<TYPE, VectorSimilarityAlgorithm::kCosineSimilarity>(context, columns, "cosine_similarity"); }关键在于:vector_similarity的所有热点循环(math_functions.cpp#L1260-L1377)中,范数累加与除法都包裹在if constexpr (algorithm == VectorSimilarityAlgorithm::kCosineSimilarity)之内。对于kNormalizedCosineSimilarity分支,这些代码在编译期被完全剔除,最终每行只执行一个操作——累加内积后直接输出:
} else { out[i] = sum; // kNormalizedCosineSimilarity 与 kInnerProduct 共用此路径 }从源码结构看,这带来两点结论:
- 语义层面:
cosine_similarity_norm(a, b)的返回值就是dot(a, b)。对单位向量而言,点积恰好等于夹角的余弦值,因此"假设输入已归一化"是严格成立的——函数名中的 "norm" 指的不是"函数内部会做归一化",而是"输入向量已被归一化(normalized)"。 - 性能层面:相比
cosine_similarity需要额外累加两个向量的平方和并做一次开方/除法,norm 版本每元素少做两次乘加累加和一次除法,向量化路径下的收益在向量维度很高(如 768 维、1024 维的 Embedding)时尤为明显。这与文档推荐使用场景——"向量已在写入侧完成归一化"——完全吻合。
另外可以观察到,approx_cosine_similarity(函数 ID 10106)复用了cosine_similarity<TYPE_FLOAT, false>的实现(见 functions.py#L66),从源码结构看,"approx" 前缀的函数是面向向量索引近似查询场景的入口,与本文讨论的精确计算函数共享同一套标量实现。
五、执行路径与错误处理
vector_similarity主函数(math_functions.cpp#L1379-L1565)在处理每批数据时依次完成以下校验与分派:
1. 输入合法性校验(直接对应文档"如果任一输入参数为 null 或非法,将报告错误"):
- 两列行数必须相等,否则返回
requires equal length arrays错误; - 列级或元素级不允许出现 NULL,返回
does not support null values错误; - 每行维度必须一致且非空,否则分别返回
requires equal length arrays in each row或requires non-empty arrays错误。
2. 常量列快速路径:当查询侧是常量数组(如示例中的[0.1, 0.2, 0.3])时,实现通过ConstColumn检测走vector_similarity_fixed_query分支(math_functions.cpp#L1260-L1324),直接以 size-1 的底层数据指针作为查询向量,避免对常量列做全量物化展开,这正是"常量向量 × N 行候选向量"这一典型查询形态的优化。
3. 定长向量化路径与 AVX2 优化:当所有行维度一致时,走vector_similarity_fixed_dim_float分支(math_functions.cpp#L1326-L1377),内部使用 256 位 SIMD 指令每轮并行处理 8 个float:
for (; j + 7 < dim; j += 8) { __m256 base_vec_data = _mm256_loadu_ps(base_vec + j); __m256 target_vec_data = _mm256_loadu_ps(target + j); __m256 mul_vec = _mm256_mul_ps(base_vec_data, target_vec_data); sum_vec = _mm256_add_ps(sum_vec, mul_vec); ... }在开启 AVX2 的构建中,norm 路径只需"乘 + 累加"两条向量指令即可完成 8 个维度的内积;非 norm 路径则还需额外维护两个平方和累加器。尾部不足 8 维的部分退化为标量循环,且对极小向量还使用fast_rsqrt_nr(快速逆平方根近似)替代精确开方。
4. 零向量行为差异:非 norm 的kCosineSimilarity分支中,任一向量为零向量时返回 0(避免 0/0);norm 分支由于不做除法,零向量自然得到点积 0,不会产生 NaN/Inf。单元测试cosineSimilarityNorm(math_functions_test.cpp#L1996-L2009)验证了该语义:
// cosine_similarity with isNorm=true (pre-normalized vectors) float inv_sqrt2 = 1.0f / std::sqrt(2.0f); auto base_col = build_float_array_column({{1, 0, 0}, {inv_sqrt2, inv_sqrt2, 0}}); auto target_col = build_float_array_column({{1, 0, 0}, {inv_sqrt2, 0, inv_sqrt2}}); auto result = MathFunctions::cosine_similarity<TYPE_FLOAT, true>(ctx.get(), columns); ASSERT_FLOAT_EQ(res[0], 1.0f); // 同一单位向量 -> 1 ASSERT_NEAR(res[1], 0.5f, 1e-5f); // 夹角 60° 的单位向量 -> 0.5同测试文件还覆盖了常量基向量(cosineSimilarityConstBase)、常量目标向量(cosineSimilarityConstTarget)、双常量(cosineSimilarityBothConst)以及维度不匹配报错等分支,与上文描述的执行路径一一对应(见 math_functions_test.cpp#L1933-L1994)。
六、与 cosine_similarity 的选择指南
结合文档语义与上述源码实现,可以给出清晰的选择依据:
| 维度 | cosine_similarity_norm | cosine_similarity |
|---|---|---|
| 输入假设 | 向量已在写入/预处理阶段归一化 | 任意模长向量 |
| 内部计算 | 纯点积 | 点积 ÷ (‖a‖·‖b‖) |
| 单行计算开销 | 一次内积 | 内积 + 两个范数平方和 + 开方/除法 |
| 未归一化输入下的结果 | 点积(可能超出 [-1, 1]) | 严格意义的余弦相似度 |
| 对应源码算法 | kNormalizedCosineSimilarity | kCosineSimilarity |
实践建议:如果你管理着一张向量表(例如 Embedding 表),在数据写入前完成归一化是更优策略——归一化是一次性的离线开销,而查询路径上每行都能省去范数计算。这也解释了为什么 StarRocks 同时提供两个函数:把"是否需要归一化"的决定权交给数据生产者,查询侧按数据形态选用对应函数。
七、小结
cosine_similarity_norm(a, b)接受两个同维度的Array<float>,在输入向量已归一化的前提下返回夹角余弦值,取值 [-1, 1];- 其本质实现是"预归一化向量点积":BE 源码中
kNormalizedCosineSimilarity分支剔除了范数计算,配合 AVX2 定长向量化与常量列快速路径,是三者(cosine_similarity_norm、cosine_similarity、inner_product)中查询开销最低的路径; - 输入为 NULL、维度不一致或数组为空时均会直接报错,便于在数据质量环节尽早暴露问题;
- 向量未归一化时请改用 cosine_similarity,或在写入侧先行归一化后再用本函数。
如需继续深入,可参考仓库中函数注册表 gensrc/script/functions.py、BE 实现 be/src/exprs/math_functions.cpp 与测试用例 be/test/exprs/math_functions_test.cpp,以及官方文档 cosine_similarity_norm 参考页。
【免费下载链接】starrocksThe world's fastest open query engine for sub-second analytics both on and off the data lakehouse. With the flexibility to support nearly any scenario, StarRocks provides best-in-class performance for multi-dimensional analytics, real-time analytics, and ad-hoc queries. A Linux Foundation project.项目地址: https://gitcode.com/GitHub_Trending/st/starrocks
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考