Tantivy
Abstract
Tantivy 是一个用 Rust 编写的全文搜索引擎库,定位类似于 Java 生态中的 Apache Lucene——它不是一个开箱即用的搜索服务,而是一个供开发者嵌入到自己程序中的底层搜索库。
背景
在 Rust 生态出现之前,构建全文搜索能力的主要路径是:
- 嵌入 Lucene(Java):功能完善,但引入 JVM 依赖;
- 接入 Elasticsearch / Solr:部署运维复杂,对轻量服务来说过重;
- SQLite FTS:功能有限,对数据量大(百万条以上)或高并发场景可能不够理想。
Tantivy 的优势
Tantivy 的出现填补了 Rust / 原生程序中"嵌入式全文搜索"的空白。它以 Lucene 的设计为蓝本,同样基于倒排索引 + Segment 模型,但充分利用 Rust 的零成本抽象和内存安全特性,在性能和资源占用上都有明显优势。
核心特性
Tantivy 的核心特性包括:
- 倒排索引 + BM25 相关性评分(与 Lucene 一致);
- 支持多字段(text、i64、f64、date、bytes 等);
- 列式存储(Fast Field)加速数值过滤与聚合;
- 内置分词器,支持自定义 Tokenizer;
- 写入吞吐高,读写并发基于 Segment 不可变设计;
- 纯 Rust 实现,无 GC、无运行时依赖,易于静态链接。
Tantivy vs. Elasticsearch
Tantivy 是库,Elasticsearch 是服务。前者适合进程内嵌入,无网络开销,部署简单;后者适合多节点分布式场景。Quickwit 和 Meilisearch 等搜索引擎均以 Tantivy 作为底层索引引擎。
Tantivy in MyScaleDB
MyScaleDB 是一个 SQL Vector 数据库,帮助开发者通过熟悉的 SQL 构建支持 AI 应用的生产级、高可扩展性系统。它基于 ClickHouse 构建,并针对 AI 应用和场景进行了优化,使开发者能够高效管理与处理海量数据。
ClickHouse 原生文本能力不足
ClickHouse 原生提供了 hasToken、startsWith、multiSearchAny 等基础文本函数,简单词项匹配够用,但难以覆盖完整全文检索需求,例如:
- 短语查询(多 term + AND/OR 等复杂约束)
- 模糊匹配(容错检索)
- BM25 相关性排序(返回相似度评分)
因此引入 Tantivy 作为 MyScaleDB 的全文索引引擎,补齐上述能力,并进一步加速已有词项匹配场景。
Skipping Index
ClickHouse 的 Skipping Index 是一种面向分析场景的辅助索引,其目标不是像传统 B+Tree 那样定位具体数据行,而是通过为每个数据块(Granule)维护统计摘要(如 MinMax、Set、Bloom Filter 等),在查询阶段快速判断某个数据块是否不可能满足查询条件,从而直接跳过该数据块的读取和解压。
由于 ClickHouse 的查询成本主要来自磁盘 IO 和列数据扫描,Skipping Index 能显著减少需要访问的数据量,提高过滤查询的执行效率,尤其适用于非主键列上的高选择性条件过滤。
Clickhouse 官网的 Skipping Index 示例
CREATE TABLE skip_table
(
my_key UInt64,
my_value UInt64
)
ENGINE MergeTree primary key my_key
SETTINGS index_granularity=8192;
INSERT INTO skip_table SELECT number, intDiv(number,4096) FROM numbers(100000000);
未使用 Skipping Index 的查询,my_value 列中的全部 1 亿条记录都会被扫描
SELECT * FROM skip_table WHERE my_value IN (125, 700)
┌─my_key─┬─my_value─┐
│ 512000 │ 125 │
│ 512001 │ 125 │
│ ... | ... |
└────────┴──────────┘
8192 rows in set. Elapsed: 0.079 sec. Processed 100.00 million rows, 800.10 MB (1.26 billion rows/s., 10.10 GB/s.
创建 set 类型的 Skipping Index(Granularity = 2)
ALTER TABLE skip_table ADD INDEX vix my_value TYPE set(100) GRANULARITY 2;
ALTER TABLE skip_table MATERIALIZE INDEX vix;
SELECT * FROM skip_table WHERE my_value IN (125, 700)
┌─my_key─┬─my_value─┐
│ 512000 │ 125 │
│ 512001 │ 125 │
│ ... | ... |
└────────┴──────────┘
8192 rows in set. Elapsed: 0.051 sec. Processed 32.77 thousand rows, 360.45 KB (643.75 thousand rows/s., 7.08 MB/s.)

相较于需要扫描全部 1 亿行、800 MB 数据的传统方式,借助 Skipping Index,ClickHouse 仅需读取和分析 32,768 行、360 KB 的数据——对应于 4 个 granule(每个 granule 含 8192 行)。
这种方式显著降低了数据扫描量和 IO 成本,大幅提升了查询效率。
因此,我们基于 Tantivy 实现了一个 Skipping Index,命名为 FTS(Full-Text Search) 索引,用于增强 MyScaleDB/ClickHouse 的文本处理能力。
基于 FFI 的 Rust/C++ 桥接
由于 ClickHouse/MyScaleDB 的主体是 C++,而 Tantivy 是 Rust 库,因此集成时不能直接把 Tantivy 当作普通 C++ 依赖使用。比较稳妥的做法是:Rust 侧保留 Tantivy 的完整实现,C++ 侧只暴露一层业务语义明确的索引接口。
整体链路可以拆成两部分:
- 构建层:通过 CMake + Corrosion 把 Rust crate 纳入 C++ 工程构建;
- 接口层:通过
cxx.rs生成类型安全的 Rust/C++ FFI 绑定,避免手写裸extern "C"带来的 ABI 和内存管理风险。
CMake 接入 Rust crate
Corrosion 的作用是让 CMake 能识别并构建 Cargo 项目。C++ 工程只需要把 Rust crate 当成一个 target 链接进来,Tantivy 依赖、cxx 代码生成和 Rust 编译都交给 Cargo 处理。
...
add_library(ch_tantivy_search_rust INTERFACE)
target_link_libraries(ch_tantivy_search_rust INTERFACE supercreate)
add_library(ch_rust::tantivy_search ALIAS ch_tantivy_search_rust)
目前,ClickHouse 已经初步集成了多种 Rust crate,比如 skim、prql 等,为集成 Rust 生态提供了很好的示例与借鉴。
更多细节可参考 CMakeLists.txt,在 C++ 端并不会直接依赖 Tantivy 的内部类型,而是通过 cxx 自动生成的桥接头文件(如 tantivy_search.h)进行交互。
Rust 侧定义 FFI 边界
cxx.rs 的核心是 #[cxx::bridge]。对于 Tantivy 这种复杂库,不建议把 IndexReader、IndexWriter、Searcher 等内部对象直接暴露给 C++,而是封装成一个 Rust handle,并只暴露数据库真正需要的操作。

#[cxx::bridge(namespace = "TANTIVY")]
mod ffi {
#[derive(Debug, Clone)]
pub struct RowIdWithScore {
pub row_id: u64,
pub score: f32,
pub seg_id: u32,
pub doc_id: u32,
pub docs: Vec<String>,
}
...
extern "Rust" {
fn ffi_create_index(index_path: &CxxString, column_names: &CxxVector<CxxString>) -> FFIBoolResult;
fn ffi_free_index_writer(index_path: &CxxString) -> FFIBoolResult;
fn ffi_load_index_reader(index_path: &CxxString) -> FFIBoolResult;
...
}
}
详细请参考 src/lib.rs, 这层接口的设计原则是:FFI 边界只传递简单、稳定、所有权清晰的数据结构。
C++ 侧封装为 IndexStore
C++ 侧不需要感知 Tantivy 的内部实现,只需要把 Rust 暴露的能力包装成 ClickHouse/MyScaleDB 原有的索引生命周期接口。下面的结构与 TantivyIndexStore 的职责对应:负责加载 reader/writer、写入文档、提交索引,以及为查询返回 bitmap 或 BM25 结果。
BoolWithMessage TantivyIndexStore::freeIndexReaderImpl(const String & full_index_path)
{
TANTIVY::FFIBoolResult free_status = TANTIVY::ffi_free_index_reader(full_index_path);
return FFI_BOOL_CONVERT(free_status);
}
...
详细请参考 TantivyIndexStore.cpp。