Cassandra的宽表模型擅长处理海量列与高吞吐写入,但过去面对向量相似度搜索需要额外引入Elasticsearch或Faiss等组件。随着Cassandra 5.0引入原生Vector类型和SAI向量索引,Node.js服务端可以直接在同一张宽表中完成业务数据存储与向量检索。宽表向量方案的核心在于把实体的所有属性列、标签列、特征列与向量列放进同一行,利用分区键控制数据分布,再通过聚类列排序实现高效的批量读取。

这种设计减少了跨系统数据同步的复杂度,但也带来新的挑战:向量列必须是固定维度、索引构建会占用额外存储、ANN查询需要配合分区键过滤才能控制扫描范围。下文将从驱动接入、表结构创建、写入优化、相似度查询和调优五个方面展开。
Cassandra Vector与宽表模型的基础概念
宽表模型是Cassandra最经典的数据建模方式,一张表可以包含成百上千个普通列。与关系型数据库不同,Cassandra的宽表并不要求每行都填充相同的列,缺失列不占用存储空间。向量列vector<float, N>属于Cassandra 5.0的原生类型,用于存储定长的浮点数组,维度N在建表时指定,插入数据时维度不匹配会直接报错。
SAI(Storage-Attached Index)是Cassandra的二级索引实现,新版本对向量列提供了专门的ANN(Approximate Nearest Neighbor)索引能力。创建向量索引时需要指定相似度函数,支持cosine、dot_product和euclidean三种度量方式。宽表中的向量列通常不会被所有行填充,但一旦启用SAI向量索引,索引会记录每个非空向量的近似表示,查询时通过内存中的图结构快速定位候选集。
在Node.js中操作这类表结构,官方驱动cassandra-driver从4.7版本起已支持Vector类型映射。驱动会把JavaScript的Float32Array映射为Cassandra的vector<float, N>,也会在查询结果中返回Float32Array实例。理解这个映射关系可以避免手动拼接二进制字符串或JSON数组带来的性能损耗。
Node.js驱动接入与表结构创建
首先安装驱动:npm install cassandra-driver。连接时需要指定localDataCenter和keyspace,生产环境建议启用credentials和sslOptions。下面给出创建带向量列的宽表代码。
const { Client, Uuid } = require('cassandra-driver');
const client = new Client({
contactPoints: ['127.0.0.1'],
localDataCenter: 'datacenter1',
keyspace: 'app'
});
async function createVectorWideTable() {
const createTableQuery = "CREATE TABLE IF NOT EXISTS product_vectors (" +
"partition_key text, " +
"product_id uuid, " +
"title text, " +
"category text, " +
"price double, " +
"embedding vector<float, 128>, " +
"PRIMARY KEY ((partition_key), product_id) " +
") WITH CLUSTERING ORDER BY (product_id ASC)";
await client.execute(createTableQuery);
const createIndexQuery = "CREATE CUSTOM INDEX IF NOT EXISTS product_embedding_ann_idx " +
"ON product_vectors (embedding) " +
"USING 'StorageAttachedIndex' " +
"WITH OPTIONS = {'similarity_function': 'cosine'}";
await client.execute(createIndexQuery);
console.log('宽表与向量索引创建完成');
}
上面的建表语句把partition_key作为分区键,product_id作为聚类列。向量列embedding声明为128维浮点向量。宽表模型通常按业务域划分分区,例如将商品分类ID或租户ID作为partition_key,这样可以保证同一分类下的商品集中在一个分区,后续ANN查询可以借助分区过滤缩小扫描范围。
创建向量索引时使用StorageAttachedIndex,并设置similarity_function为余弦相似度。如果后续需要切换为点积或欧氏距离,需要删除索引后重新创建,因为索引构建的图结构与度量方式强相关。对于已经存在的数据,索引会在创建后异步构建,可以通过nodetool compactionstats或系统表观察进度。
宽表向量数据的写入与批量优化
向量写入的核心是把Float32Array作为参数绑定到INSERT语句。Cassandra驱动支持prepare缓存,预编译语句可以减少服务端解析开销。对于宽表,建议一次写入包含完整的业务列与向量列,避免后续频繁更新向量值导致索引重建。
async function insertProductVectors() {
const insertQuery = 'INSERT INTO product_vectors ' +
'(partition_key, product_id, title, category, price, embedding) ' +
'VALUES (?, ?, ?, ?, ?, ?)';
const dimension = 128;
const batch = [];
for (let i = 0; i < 100; i++) {
const embedding = new Float32Array(dimension);
// 模拟生成归一化向量
for (let j = 0; j < dimension; j++) {
embedding[j] = Math.random();
}
const norm = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
for (let j = 0; j < dimension; j++) {
embedding[j] /= norm;
}
batch.push({
query: insertQuery,
params: [
'electronics',
Uuid.random(),
`Speaker model ${i}`,
'audio',
49.99 + i,
embedding
]
});
}
await client.batch(batch, { prepare: true });
console.log('批量写入完成');
}
批量写入时需要注意,Cassandra的batch并不适合无限堆积,默认单批过大反而会触发协调节点内存压力。这里以100行为一批,每行都指向同一个分区键electronics,利用单分区批处理的原子性。如果跨分区,建议使用executeConcurrent并发写入而非batch。
宽表向量模型的写入放大主要来自SAI向量索引的构建。每个向量写入后,索引会计算向量哈希并插入到内存图结构中。对于高频写入场景,建议控制单次批量大小并监控sai_index_memory_usage指标。归一化向量可以显著提升余弦相似度的计算稳定性,示例中已经对每个向量做了L2归一化。
执行ANN相似度查询与结果解析
Cassandra的ANN查询语法为ORDER BY embedding ANN OF ? LIMIT ?,其中查询向量通过占位符传入。与传统的ORDER BY不同,ANN查询不会做全表排序,而是基于向量索引的图搜索返回近似结果。建议在WHERE子句中带上分区键,让查询只扫描相关分区。
async function searchSimilarProducts(queryVector) {
const dimension = 128;
const normalized = new Float32Array(queryVector);
const norm = Math.sqrt(normalized.reduce((sum, val) => sum + val * val, 0));
if (norm > 0) {
for (let j = 0; j < dimension; j++) {
normalized[j] /= norm;
}
}
const selectQuery = 'SELECT product_id, title, price, ' +
'similarity_cosine(embedding, ?) AS score ' +
'FROM product_vectors ' +
'WHERE partition_key = ? ' +
'ORDER BY embedding ANN OF ? ' +
'LIMIT 10';
const result = await client.execute(
selectQuery,
[normalized, 'electronics', normalized],
{ prepare: true, fetchSize: 10 }
);
const rows = result.rows.map(row => ({
productId: row.product_id.toString(),
title: row.title,
price: row.price,
score: row.score
}));
return rows;
}
代码中先把查询向量做归一化,再通过similarity_cosine函数返回相似度分数。需要注意参数顺序:第一个?对应similarity_cosine(embedding, ?)中的查询向量,第三个?对应ANN OF ?中的向量,两者必须是同一份数据。驱动会将Float32Array正确序列化为二进制,无需手动转换。
结果中的score列是浮点数,余弦相似度取值范围为-1到1。因为写入前做了归一化,查询向量也做了归一化,实际上相似度会落在-1到1之间,1表示完全相同方向。如果需要展示距离,可以用1 - score进行转换。ANN查询默认返回近似结果,可能不是全局最相似的向量,可以通过放宽LIMIT再重排的方式提高召回率。
调优建议与常见问题
宽表向量模型的性能高度依赖分区键设计。分区过大时,向量索引构建和查询都会变慢;分区过小又会导致跨分区查询开销增加。实际生产环境可以根据数据量和查询模式,将分区键设为业务维度加时间分片,例如category_202501,既控制分区大小又保持查询范围稳定。
另一个常见问题是向量维度过高导致索引内存膨胀。Cassandra的SAI向量索引使用压缩向量表示,但维度每增加一倍,内存占用仍会显著上升。128到768维是常见的平衡点,超过1024维建议先使用降维算法处理。驱动连接池方面,ANN查询的响应时间通常高于普通主键查询,建议适当调大pooling.coreConnectionsPerHost并设置查询超时,避免连接堵塞。
最后要留意JavaScript的数值精度:Float32Array在转换过程中不会丢失浮点精度,因为Cassandra内部也使用32位浮点存储向量。但如果从JSON接口接收向量数组,需要先转为Float32Array再写入,直接传普通数组可能被驱动当作List类型处理,导致类型不匹配。
Node.jsCassandra Vector宽表向量修改时间:2026-09-25 01:26:49