ScyllaDB 全文检索实战:fulltext_index 与 BM25 查询的完整解析
【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb
本文围绕 ScyllaDB 的全文检索(Full-Text Search, FTS)功能展开:如何在text/varchar/ascii列上创建fulltext_index自定义索引、如何编写符合约束的BM25()查询,并结合当前仓库中的索引校验源码(index/fulltext_index.cc)与查询语句实现(cql3/statements/external_search/fulltext_indexed_table_select_statement.cc)说明每条限制背后的具体校验逻辑,以及配套的端到端测试(test/cqlpy/test_fulltext_search_with_mock.py)。读完后,你将能够正确建表、建索引、编写并排查 FTS 查询,并理解每个查询约束在源码中的拒绝点。
什么是全文检索
全文检索允许你在文本列中查找包含特定单词或短语的行。与精确匹配查询或LIKE过滤不同,FTS 使用倒排索引对文本分词,并基于 BM25 评分算法按相关性对结果排序。
典型使用场景包括:
- 在产品描述、文章或日志消息中搜索关键词;
- 按与查询词的匹配程度对结果进行相关性排序;
- 过滤出至少包含一个匹配词的行。
创建 fulltext_index
在运行 FTS 查询之前,必须先对目标列创建fulltext_index:
CREATE CUSTOM INDEX ON ks.t (v) USING 'fulltext_index';可以通过WITH OPTIONS指定 analyzer,控制文本的分词方式,默认值为standard。各支持 analyzer 的完整说明见 CQL 二级索引文档:
CREATE CUSTOM INDEX ON ks.t (v) USING 'fulltext_index' WITH OPTIONS = {'analyzer': 'english'};源码中的索引选项定义
从源码看,fulltext_index支持的选项在 index/fulltext_index.cc 中集中定义,共两个:
analyzer:指定用于分词的内置文本分析器。源码注释说明该列表对应后端搜索引擎(Tantivy)预期提供的分析器,合法取值为standard、english、german、french、spanish、italian、portuguese、russian、simple、whitespace共 10 种(见 index/fulltext_index.cc);positions:布尔值,控制是否在索引中存储 token 位置。短语查询(phrase queries)依赖位置信息;如需节省空间可设为false。
传入任何未在此表中注册的选项,check_index_options会直接抛出Unsupported option ... for fulltext index异常(见 index/fulltext_index.cc)。
创建索引的硬性要求
官方文档明确要求如下,且每一条都能在索引校验逻辑fulltext_index::validate中得到印证(见 index/fulltext_index.cc,校验顺序为check_uses_tablets→check_target→check_cdc_options→check_index_options):
- 列类型:被索引列必须是
text、varchar或ascii类型,其他类型会被拒绝。源码中check_target检查abstract_type::kind::utf8与abstract_type::kind::ascii,不满足则抛出 "Fulltext index is only supported on text, varchar, or ascii columns..."(见 index/fulltext_index.cc)。此外,索引目标必须且只能有一个单列,分区键列不能作为目标。 - 表必须使用 tablets(而非 vnodes)。对应
validate中的check_uses_tablets(schema, db)。 - CDC 要求:表必须启用 CDC,且 TTL 至少为 86400 秒(24 小时),并满足
delta = 'full'或启用 postimage。创建 fulltext 索引时 CDC 会被自动开启,无需手动配置。这个 86400 秒的门槛在基类中定义为常量VS_TTL_SECONDS = 86400,注释说明其目的是"确保 CDC 数据保留时间足够长,让索引构建能够完成"(见 index/external_index.hh)。 - fulltext 索引继承自
external_index基类(见 index/external_index.hh),该基类注释表明索引由外部存储引擎(Vector Store)支撑,这也是后文 CDC 依赖与查询走外部服务的原因。
Cell 级 TTL 的陷阱
使用标准USING TTL语法(在INSERT或UPDATE上)设置的 cell 级 TTL,会在到期时间使该值不可读,但不会生成 CDC 事件,因此全文索引不会被更新,会为该值保留一条过期(stale)索引条目。若需要索引反映过期行为,应改用 Per-row TTL 特性:它会显式删除过期行,并会生成 CDC 事件,从而驱动索引更新。
使用 BM25 进行查询
FTS 查询使用BM25()函数对行进行搜索词打分。BM25()接收两个参数:列名和查询字符串。
一条合法的全文检索查询必须同时在以下两个子句中使用BM25(),且作用于同一列、使用同一搜索词:
WHERE子句:形式必须严格为BM25(column, 'term') > 0,用于过滤出匹配搜索词的行;ORDER BY子句:ORDER BY BM25(column, 'term'),按 BM25 相关性得分排序(得分最高者优先)。
两个子句缺一不可——仅有WHERE BM25()或仅有ORDER BY BM25()的查询都会被拒绝;并且两处必须引用同一列、使用同一搜索词。此外每条 FTS 查询都要求LIMIT。完整语法参考见 CQL SELECT 文档。
基础查询
过滤包含搜索词的行并按相关性排序:
SELECT * FROM ks.t WHERE BM25(v, 'search term') > 0 ORDER BY BM25(v, 'search term') LIMIT 10;在WHERE子句中,>是唯一支持的运算符,且右值必须是字面量0。>=、=、<、<=、!=等运算符以及任何非零阈值都会被拒绝。这一约束在源码validate_bm25_where_restriction中实现:运算符不是GT时抛出only ">" is supported,右值不是常量或反序列化后不等于0.0f时抛出comparison value must be the literal 0(见 cql3/statements/external_search/fulltext_indexed_table_select_statement.cc)。
与其他过滤条件组合
目前BM25()旁边不支持附加的WHERE限制(例如分区键等值比较),会被直接拒绝。从源码结构看,prepare阶段在确认恰好存在一个 BM25 评分限制后,还会检查分区键、聚簇列、非主键三类限制是否全部为空,只要有一项非空即抛出 "Full-text search queries do not support additional WHERE restrictions"(见 fulltext_indexed_table_select_statement.cc)。组合过滤能力已规划在未来版本中支持。
使用绑定标记
查询词可以在预处理语句中通过绑定标记传入:
SELECT * FROM ks.t WHERE BM25(v, ?) > 0 ORDER BY BM25(v, ?) LIMIT 10;两个绑定标记在执行时都会被检查,必须绑定相同的值;WHERE与ORDER BY绑定不同值会导致查询被拒绝。源码中,两个搜索词若都是字面量则在 prepare 阶段即检查一致性;若涉及绑定标记,则在execute_search中通过expr::evaluate求值后逐一对比(见 fulltext_indexed_table_select_statement.cc)。测试用例test_bm25_two_bind_markers_search_term、test_bm25_named_bind_markers_search_term等验证了字面量与绑定标记混用时"搜索词必须一致"的行为,且命名绑定标记(:term)同样可用(见 test/cqlpy/test_fulltext_search_with_mock.py)。
与用户自定义函数重名时的区分
BM25不是保留字。如果某个 keyspace 中定义了名为bm25的用户自定义函数,未加限定的BM25()调用会产生歧义;此时应显式地用system.bm25(...)限定以选择内置算子。
FTS 查询约束汇总
FTS 查询强制执行以下规则:
| 约束 | 说明 |
|---|---|
| 两个子句缺一不可 | 查询必须同时包含WHERE BM25() > 0过滤和ORDER BY BM25()排序,且两者引用同一列、同一搜索词。单独任一子句都会被拒绝。 |
仅支持>与字面量0 | WHERE中唯一接受的形式是BM25(column, 'term') > 0,其他运算符(>=、=、<、<=、!=)和非零阈值均被拒绝。 |
| 过滤组合受限 | WHERE中仅接受BM25(column, 'term') > 0,任何附加限制(如分区键等值)都会被拒绝;组合过滤已规划在未来版本支持。 |
LIMIT必填 | 每条 FTS 查询必须包含不超过 1000 的LIMIT;缺少LIMIT或LIMIT大于 1000 都会被拒绝。 |
不支持PER PARTITION LIMIT | FTS 查询不能与PER PARTITION LIMIT一起使用。 |
| 不支持聚合 | FTS 查询不能包含聚合函数(如COUNT(*)、SUM())。 |
| 必须有 fulltext 索引 | 被查询列上必须存在fulltext_index,普通二级索引不满足该要求。 |
BM25()不能出现在SELECT | BM25()仅在WHERE与ORDER BY子句有效,不能作为选择器。 |
| 仅支持单一排序 | ORDER BY BM25()不能与其他ORDER BY列、第二个BM25()排序或ANN排序组合。 |
| 不支持分页 | FTS 查询不支持 paging,最多LIMIT行的全部匹配结果在单页返回。 |
| 不支持分组 | FTS 查询不能包含GROUP BY子句。 |
源码纵深:一条 FTS 查询是如何被校验与执行的
fulltext_indexed_table_select_statement(见 cql3/statements/external_search/fulltext_indexed_table_select_statement.hh)是 FTS 查询的专用 SELECT 语句类型,其prepare阶段集中实现了上表大部分约束的拒绝逻辑:
- 无
LIMIT→ "Full-text search queries require a LIMIT"; - 出现
per_partition_limit→ 拒绝; - 选择器是聚合(
selection->is_aggregate())→ 拒绝; - 缺少
ORDER BY BM25()(无ordering_info)→ 拒绝; WHERE中的 BM25 评分限制为空或超过 1 个 → 分别抛出 "require a WHERE BM25() > 0 clause" / "support only one WHERE BM25() restriction"。
在执行侧,execute_search(见 fulltext_indexed_table_select_statement.cc)还会做几件文档层面的约束对应的事:
- LIMIT 上限:
limit超过max_fts_query_limit = 1000(定义于 fulltext_indexed_table_select_statement.hh)时抛出异常; - 搜索词非空:绑定标记求值结果为 null 时拒绝;
- 同词校验:
WHERE的搜索词与ORDER BY的搜索词求值后不一致时拒绝; - 调用外部索引服务:通过
query_processor的vector_store_client().bm25(ks_name, index_name, schema, search_term, limit, ...)发起请求,取得带分数的主键列表后,再由external_score_provider将分数按行回填到结果中。
测试侧用 mock 服务验证了这条链路:FTS 查询会被翻译成发往 Vector Store 服务/bm25端点的 HTTP POST 请求,具体路径为/api/v1/indexes/<keyspace>/<index>/bm25,请求体携带查询词与 limit,返回的primary_keys与scores决定了行序(见 test/cqlpy/test_fulltext_search_with_mock.py)。另一个索引侧的测试文件 test/cqlpy/test_fulltext_index.py 则覆盖建索引时的各类校验。
小结与延伸阅读
ScyllaDB 的全文检索由三部分构成:fulltext_index自定义索引(由外部 Vector Store/Tantivy 引擎支撑,依赖 tablets 与长 TTL 的 CDC 数据流)、严格的BM25()双条款查询语法(过滤 + 排序、同列同词、LIMIT ≤ 1000)、以及源码中逐条落地的 prepare/execute 两级校验。编写 FTS 功能时建议按以下顺序排查报错:先确认列类型与 tablets/CDC 前提(建索引报错时),再对照本文约束表核对 WHERE/ORDER BY/LIMIT 形态(查询报错时)。
更多细节可继续阅读仓库中的以下文件:
- 官方功能文档:docs/features/fulltext-search.rst
- 索引语法与 analyzer 说明:docs/cql/secondary-indexes.rst
- BM25 查询语法参考:docs/cql/dml/select.rst
- 内部设计说明:docs/dev/fulltext_search.md
- 索引定义与校验:index/fulltext_index.hh、index/external_index.hh
- 查询语句实现:cql3/statements/external_search/fulltext_indexed_table_select_statement.cc
- 端到端测试:test/cqlpy/test_fulltext_search_with_mock.py、test/cqlpy/test_fulltext_index.py
【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考