seekdb 全文索引调优
面向 seekdb(OceanBase 内核)全文索引的性能调优工作流:先量、再定位、只做可证明的改动、用命中数回归锁住正确性。
何时使用
- 需要降低
MATCH ... AGAINST查询延迟、全文索引构建耗时、或TOKENIZE()调用耗时 - 需要在 seekdb 源码树上复现 / 提升
tools/benchmark/fts_large_bench的分数 - 需要判断某个"性能优化"是否真的有效、是否改变了检索语义
不适用:单机 OLTP 调参、SQL 改写优化、与全文索引无关的存储引擎调优。
核心原则
- 只认同机对比。跨机器的绝对耗时没有意义(同一份代码在不同机器上可以差 8 倍)。任何收益必须来自"同一台机器、同一参数、改动前后"的对比。
- 先确认噪声水平。全文索引构建指标在同一机器上波动可达 ±30%~50%,优化幅度小于噪声的改动等于没做。
- 语义零变化。只消除"与输入无关的重复工作",绝不改变分词规则、命中集合、评分排序。
- 命中数是回归红线。四项
query_*_hits任何一项变化,本次改动作废。
工作流
第 1 步:建立可比基线
cd <seekdb 仓库根目录>
bash scripts/run_bench.sh before # 固定参数跑一遍,落盘报告
参数由脚本固定为评测口径:ROWS=20000 BATCH=500 ROUNDS=3000 QUERY_ROUNDS=200 SAMPLES=3 WARMUP=30 SKIP_LOAD=0。
配置不一致时评分脚本会直接拒评。
第 2 步:判断瓶颈落在哪一类
| 现象 | 优先怀疑 |
| --- | --- |
| tokenize_*_avg_ms 明显高于 select1_avg_ms(纯 SQL 往返地板) | 分词路径的每次调用固定开销(配置解析、分词器/词典重建) |
| build_*_sec 换算到"每段文本"后与 tokenize 单次同量级 | 同一个固定开销被文档数放大 |
| query_*_avg_ms 与命中数近似线性(约 0.1~0.2 µs/命中) | 逐命中成本,已经很紧,优化空间小 |
| query_limit_avg_ms 与全量命中的查询耗时接近 | LIMIT 没有被下推,整表扫描后只取前 N 行 |
第 3 步:按清单定位代码
见 references/bottleneck-checklist.md,按文件:行号逐项核对。清单里的每一条都是"与文本内容无关的重复工作",属于低风险目标。
第 4 步:一次只改一处,每次都要能编译
改完立刻重编、重跑:
bash scripts/run_bench.sh after
bash scripts/run_bench.sh score # 自动算分 + 校验四项命中数
第 5 步:写清楚结论
提交材料至少包含:瓶颈定位依据(文件:行号 + 代码事实)、改动内容、同机前后对比、正确性论证(为什么分词/命中结果不变)。
已知的高价值目标(按性价比)
| 目标 | 位置 | 性质 |
| --- | --- | --- |
| 停用词检查的每个 token 分配 + 恒等字符集转换 | src/storage/fts/ob_fts_stop_word.cpp | 恒等变换消除 |
| casedown_word 对无可折叠字符仍走 tolower | 同上 | 恒等变换消除 |
| tokenize 参数(parser 名 / properties)每次重新解析 | src/sql/engine/expr/ob_expr_tokenize.cpp | 结果缓存(缓存输入解析,非分词结果) |
| 每篇文档重建整套 IK 词典 / 分词器 | src/storage/fts/ob_ik_ft_parser.cpp | 只读对象复用 |
| 停用词词典构建但匹配逻辑已被注释 | 同上 | 死代码 |
反例:看起来该做、实际不能做
- 跳过 BM25 打分:
need_calc_relevance恒为 true 看着像浪费,但执行端的过滤依赖同一个表达式,置 false 会让过滤失效、命中数暴涨。 - 约束全局词典表的锁:单线程基准下这把锁几乎不花时间,改了实测 0 收益。
- 缓存分词结果:这是把测试文本硬编码进结果,属于作弊;可缓存的只有"与文本无关的配置解析"。
参考
references/bottleneck-checklist.md— 逐条瓶颈与证据scripts/run_bench.sh— 前后对比 + 官方算分 + 命中数校验scripts/score_sim.py— 不跑基准就能推演"某项指标改动值多少分"
Scan to join WeChat group