OceanBase / seekdb 全文索引调优
把"全文索引慢"拆成可测量的三段:索引构建、分词热路径、MATCH 查询。不要凭感觉改参数,先拿到 before 数据。
何时使用
- 建索引(
ALTER TABLE ... ADD FULLTEXT INDEX)耗时异常或阻塞业务 MATCH(col) AGAINST('...')延迟高、QPS 低,或带标量过滤时明显变慢- 分词器(ik / beng / ngram / ngram2 / space)CPU 占比高
- 想验证某项优化是否真的带来收益
第一步:量化基线
在 seekdb 仓库中运行统一压测脚本(参数固定后才可横向对比):
cd tools/benchmark
LABEL=before ROWS=20000 BATCH=500 ROUNDS=3000 QUERY_ROUNDS=200 SAMPLES=3 ./fts_large_bench.sh
产出字段分三类,优化时按类别汇总而非只看单项:
| 类别 | 指标 |
|---|---|
| 构建 | build_ik_all_sec、build_ik_content_sec、build_beng_en_sec |
| 分词 | tokenize_ik_avg_ms、tokenize_beng_avg_ms |
| 查询 | query_cn_avg_ms、query_beng_avg_ms、query_mixed_avg_ms、query_limit_avg_ms |
计分/验收口径(若参加比赛):三类等权平均,相对基线提升 50% 记满分;单次波动约 2 分,重要结论至少重复 3 次取中位数。
第二步:分段定位
- 构建慢:看
build_total_sec与三个build_*的占比。若随文档数线性增长且 CPU 打满,优先并行度与排序;若 CPU 空闲,优先 I/O 与表结构。 - 分词慢:单独跑
SELECT tokenize(?, 'ik')。若单次调用 µs 级以上,多为解析器实例反复构造、逐字符分配、停用词表重复加载。 - 查询慢:对比
query_*_hits。命中数相近但延迟差异大,说明执行计划或索引选择有问题;query_limit明显更快说明瓶颈在结果物化而非候选扫描。
第三步:按症状取用优化杠杆
分词器
- 复用解析器实例(每文档 reset,而不是每次 new),避免重复初始化词典与仲裁器
- 把逐字符的"取字符 + 判类型"合并为一次调用,减少热循环函数调用
- 停用词/词典使用进程级只读缓存,按 collation 分组,避免每批次重建
- 结果容器预分配或使用分块数组,消除逐 token 堆分配
索引构建
- 提升构建并行度:分区本地构建、按 doc_id 范围切分,避免跨分区 shuffle
- 排序阶段用 encoded sort key(order-preserving 二进制键),定长键可走基数排序
- sort key 与 payload 分离,归并只读键,输出时再物化列
- 复核内存预算:外排阈值过小会导致频繁落盘
查询
- 确认走了全文索引而不是回表全扫(看执行计划与
hits规模) - 标量过滤条件下推到索引扫描,避免先生成全量候选再过滤
LIMIT场景优先 TopN,避免完整排序- 批量/预编译重复查询,减少解析开销
参数与建表
min_token_size/max_token_size与业务语种匹配(英文别用中文默认值)- 分词器选择:中文用 ik 或 ngram2,英文用 beng/space,二进制或标识符用 ngram
- 建索引前先灌数再建(bulk 后再 DDL)通常快于边写边建
第四步:验证与回归
- 同一机器、同一
ROWS/BATCH/ROUNDS/QUERY_ROUNDS/SAMPLES跑 after - 用
LABEL=after SKIP_LOAD=1复用数据,仅重跑分词与查询 - 三类指标分别对比,记录恶化项(性能优化常见"此消彼长")
- 语义回归:
t/下的全文索引用例必须全绿,分词结果与原来逐 token 一致
make -C competition test-hybrid # PowerContext 侧混合检索回归(如相关)
# seekdb 侧:tools/deploy/mysqltest.py 跑 fts_index 套件
常见坑
- 只看总耗时:构建、分词、查询混在一起,优化方向会错
- 忽略预热:首轮含缓存冷启动,至少 WARMUP=30
- 不同机器/负载下对比:必须同机同参数
- 改分词规则导致召回变化:性能提升但语义变了,等于没通过
- 大工具结果/长文本被 Memory 截断:Source 层必须保原文,截断只允许发生在投影或预算阶段
输出模板
场景:<表规模/文档长度/语种/查询形态>
基线:build=X s, tokenize=Y ms, query=Z ms(3 次中位数)
瓶颈:<分段结论 + 证据>
改动:<1-3 条,逐条对应指标>
结果:build/tokenize/query 各自变化 + 语义回归结论
<!-- OB-S:fgUXk45ITvWBSzms1saN0w -->
Scan to join WeChat group