Cassandra作为一款面向海量数据的分布式NoSQL数据库,原生的查询能力基本被主键设计所限制。如果想在非主键列上做查询,通常要靠二级索引、物化视图或者自己维护查询表。而在这些方案里,SASI(SSTable Attached Secondary Index)是一个比较特殊的存在:它把索引数据直接附着在SSTable上,支持前缀匹配、范围查询甚至分词模糊搜索,性能比传统二级索引好得多。这篇文章就来聊聊如何在Node.js项目中落地SASI索引,从原理到代码实现完整过一遍。

SASI索引是什么,它比普通二级索引强在哪
先说清楚背景。Cassandra早期提供的二级索引(Secondary Index)实现是建立在本地节点上的"伪表"机制,每次查询都要广播到所有节点,在大集群或者高基数列上性能非常差,官方基本不推荐在生产中使用。SASI从3.4版本开始引入(注意SOLR支持需要DataStax企业版,开源版的SASI在3.11可用,4.0一度移除、4.1社区又通过CEP-7回归了,使用前务必确认你的版本支持情况),它最大的改变是索引文件直接跟SSTable绑定写入,省去了额外的一层间接寻址。
SASI提供三种索引模式(MODE),这个参数直接决定了索引的行为:
PREFIX:按前缀组织分词,支持LIKE 'abc%'这种前缀匹配,也支持等值和范围查询,是最常用的模式。CONTAINS:支持LIKE '%abc%'任意位置匹配,灵活但开销最大,数据量大了之后要谨慎使用。SPARSE:针对高基数列(比如时间戳类数据)优化的模式,内存占用小,但只支持等值和范围查询,不支持LIKE。
每种模式下还可以配置分析器(ANALYZER),默认是STANDARD_ANALYZER,可以设置大小写是否敏感、是否跳过常见停用词等。如果要做文本搜索,可以考虑NON_TOKENIZING_ANALYZER或TOKENIZING_ANALYZER。需要特别提醒的是,SASI内置的分析器对中文分词支持非常有限,它按空格和标点切词,中文内容会被当成一整块。如果你的业务需要中文全文检索,要么自己按需设计前缀索引策略,要么老老实实上Elasticsearch做外挂索引。
建表并创建SASI索引
下面用一个实际场景演示:用户表,需要在用户名上做前缀模糊搜索,在邮箱上做等值查询。先看CQL部分:
-- 创建自定义的SASI索引需要先定义类,通常使用内置实现
CREATE CUSTOM INDEX idx_user_name_prefix ON users (user_name)
USING 'org.apache.cassandra.index.sasi.SASIIndex'
WITH OPTIONS = {
'mode': 'PREFIX',
'analyzer_class': 'org.apache.cassandra.index.sasi.analyzer.StandardAnalyzer',
'case_sensitive': 'false'
};
CREATE CUSTOM INDEX idx_user_email ON users (user_email)
USING 'org.apache.cassandra.index.sasi.SASIIndex'
WITH OPTIONS = {
'mode': 'PREFIX',
'case_sensitive': 'false'
};
建好索引后可以验证一下,执行一条前缀查询确认生效:
SELECT * FROM users WHERE user_name LIKE 'zhan%';
这里有几个容易被忽略的点。第一,SASI索引只能建在单一列上,不支持组合索引,如果你有多列联合查询需求,要么加冗余拼接列,要么调整数据模型。第二,mode选了CONTAINS之后写入放大比较明显,每次写入的索引开销约为原数据的一到两倍,磁盘和内存都要预留。第三,SASI查询目前不支持排序,返回结果的顺序由存储顺序决定,业务侧需要自己排序。第四,删除索引用DROP INDEX idx_user_name_prefix;即可,但大表上删索引会有一定的修复延迟。
用Node.js的cassandra-driver执行SASI查询
Node.js侧推荐DataStax官方的cassandra-driver包,安装很简单:
npm install cassandra-driver
先建立连接并插入一些测试数据:
const cassandra = require('cassandra-driver');
const client = new cassandra.Client({
contactPoints: ['127.0.0.1:9042'],
localDataCenter: 'datacenter1',
keyspace: 'demo'
});
async function init() {
await client.connect();
console.log('已连接Cassandra');
// 批量插入测试数据
const insert = 'INSERT INTO users (user_id, user_name, user_email) VALUES (?, ?, ?)';
const users = [
[cassandra.types.Uuid.random(), 'zhangsan', 'zhangsan@ipipp.com'],
[cassandra.types.Uuid.random(), 'zhangwei', 'zhangwei@ipipp.com'],
[cassandra.types.Uuid.random(), 'lisi', 'lisi@ipipp.com'],
[cassandra.types.Uuid.random(), 'zhangqiang', 'zhangqiang@ipipp.com']
];
// 注意SASI索引对一致性没有特殊要求,这里用默认一致性级别
for (const u of users) {
await client.execute(insert, u, { prepare: true });
}
console.log('数据插入完成');
}
init().catch(err => {
console.error('初始化失败:', err);
process.exit(1);
});
接下来是核心的模糊查询部分。LIKE查询直接写在CQL里,参数绑定用?占位符即可:
async function searchByPrefix(prefix) {
// 前缀查询,注意这里拼接百分号,且百分号作为参数值传入是安全的
const query = "SELECT user_id, user_name, user_email FROM users WHERE user_name LIKE ?";
const result = await client.execute(query, [prefix + '%'], { prepare: true });
console.log(`匹配到 ${result.rows.length} 条记录:`);
result.rows.forEach(row => {
console.log(`${row.user_name} - ${row.user_email}`);
});
return result.rows;
}
// 查询所有zhang开头的用户
searchByPrefix('zhang');
如果索引模式是CONTAINS,把拼接方式换成'%' + keyword + '%'就能实现包含匹配。需要强调的是,查询中的通配符必须由应用层拼接后作为参数传入,驱动会正确处理转义,不要把用户输入直接拼进CQL字符串,否则有CQL注入风险。这一点和SQL注入的防护思路完全一致。
生产代码中还建议加上分页处理。SASI查询虽然没有原生分页语法,但可以结合fetchSize和游标实现:
async function searchWithPaging(prefix, pageSize) {
const query = "SELECT user_id, user_name FROM users WHERE user_name LIKE ?";
const options = { prepare: true, fetchSize: pageSize, autoPage: false };
let result = await client.execute(query, [prefix + '%'], options);
let page = 1;
while (result.rows.length > 0) {
console.log(`第 ${page} 页,${result.rows.length} 条`);
// 处理当前页数据...
if (!result.pageState) break; // 没有下一页了
options.pageState = result.pageState;
result = await client.execute(query, [prefix + '%'], options);
page++;
}
}
生产环境的使用建议与常见坑
第一,版本兼容性一定要提前确认。开源版Cassandra 4.0把SASI移除了,4.1才重新支持,如果你的团队用的是4.0,要么升级,要么考虑官方主推的SAI(Storage-Attached Indexing)。SAI在功能和性能上更现代,但模糊匹配能力不如SASI灵活,两者各有取舍。DataStax Astra云服务上的情况又不同,选型时先看清楚平台文档。
第二,CONTAINS模式要克制。任意位置匹配意味着索引里每个分词都要建反向映射,写入延迟和磁盘占用都会上升。经验上,如果表数据超过几亿行,CONTAINS查询的延迟会变得不稳定。更稳妥的做法是用PREFIX模式配合业务侧的查询习惯设计,比如搜索框里默认只支持首字匹配。
第三,监控索引内存。SASI会把部分索引结构放在堆外内存中,参数max_string_term_size默认限制单个词条最大24字节,超长文本建索引会报错,建索引前评估好字段长度。同时关注column_index_size相关配置,避免大value导致索引碎片化。
第四,驱动连接管理。Node.js侧建议全局复用一个Client实例,驱动内部自带连接池和心跳维护,反复创建连接会带来不必要的握手开销。查询失败时驱动有内置的重试策略,对于SASI这种可能涉及多节点协调的查询,建议显式设置读一致性级别为LOCAL_ONE或LOCAL_QUORUM,在性能和正确性之间取平衡。
总结一下,SASI给了Cassandra一种轻量级的模糊查询能力,尤其适合用户名搜索、前缀提示这类场景,用prefix模式加上Node.js驱动的参数绑定就能稳定工作。但如果需求演进到中文全文检索、相关性排序、高亮这些搜索引擎特性,就应该把SASI和Elasticsearch组合使用,让Cassandra专注存储、搜索引擎负责检索,各司其职才是长期可维护的架构。
CassandraSASI indexNode.js修改时间:2026-09-04 22:14:48