OceanBase源码解读:FTS内置解析器插件注册 ob_fts_builtin_parser_register

前面几篇我们依次拆解了 OceanBase 4.3 全文检索(FTS)的五种分词解析器:空白分词、N-gram、N-gram2、英文基础分词 beng、IK 中文分词,以及停用词检查器和插件助手。它们各自独立成文件,但有一个共同的”出生地”——今天要讲的 ob_fts_builtin_parser_register.cpp。这个文件只有 76 行,却是整个 FTS 分词体系的总装车间:五种内置分词器在这里统一以”内置插件”的身份注册进插件框架,进程启动时即完成装配,这就是全文索引功能开箱即用的秘密。

为什么分词器要走插件框架

OceanBase 4.3 把全文分词器统一建模为插件(plugin),内置的五种分词器并不是硬编码进内核调用链的,而是与用户自研分词器走完全相同的查找路径(find_ftparser)。这样设计有两个好处:其一,内核只依赖统一的 Desc(描述器)接口,新增分词器不需要修改内核代码;其二,DDL 侧 CREATE FULLTEXT INDEX ... WITH PARSER xxx 按名字查找解析器,内置与外置一视同仁——用户今天用内置的 ik,明天换成自研解析器,内核路径完全不变。

整套装配关系可以概括为一句话:插件框架维护”名字 → Desc 工厂”的映射表,本文件的职责就是把五个名字填进这张表。

核心流程:plugin_init 的注册瀑布

插件的生命周期钩子只有 init 和 deinit 两个。init 做两件事:先初始化全局插件数据单例 ObFTParsePluginData,再依次注册五个解析器。

static int plugin_init(ObPluginParamPtr plugin)
{
  int ret = OBP_SUCCESS;
  // 先初始化全局单例:分配器 + 停用词检查器 + 词典中心三件套
  if (OB_FAIL(ObFTParsePluginData::init_global())) {
    LOG_WARN("failed to init global ftparse plugin data", K(ret));
  } else if (OB_FAIL(ObPluginHelper::register_builtin_ftparser<ObWhiteSpaceFTParserDesc>(
                 plugin, ObFTSLiteral::PARSER_NAME_SPACE, "..."))) {
    LOG_WARN("failed to init whitespace builtin ftparser", K(ret));
  } else if (OB_FAIL(ObPluginHelper::register_builtin_ftparser<ObNgramFTParserDesc>(
                 plugin, ObFTSLiteral::PARSER_NAME_NGRAM, "..."))) {
    // ... ngram2 / beng / ik 依序注册
  }
  return ret;
}

这段代码有几个值得注意的细节:

第一,初始化顺序有讲究。 ObFTParsePluginData::init_global() 必须放在所有注册之前。这个全局单例内部装配了”分配器 + 停用词检查器 + 词典中心(dict_hub)”三件套,IK 这类依赖词典的分词器在运行期都从这里取共享资源,避免每个 parser 实例重复加载词典。而注册完成的分词器随时可能被 DDL 或查询首次触发(Desc::segment),所以全局数据必须先就位。

第二,瀑布式错误处理。 五连注册用 else if (OB_FAIL(...)) 串成一条瀑布,任何一步失败直接跳到 return,后续注册不再执行。这是 OceanBase 代码的标志性风格,错误码逐层冒泡,不做半途吞错。

第三,名字只有一处定义。 注册时传入的解析器名全部来自 ObFTSLiteral 常量表(space / ngram / beng / ngram2 / ik),保证 DDL 侧的语法校验与插件侧的注册名永远一致,不存在”两边字符串各写一份对不上”的低级事故。

第四,模板参数是 Desc 类。 register_builtin_ftparser<ObIKFTParserDesc> 的模板参数不是 Parser 本身,而是轻量的 Desc 描述器。分词器的真实实现(如 ObIKFTParser)由 Desc 在 segment 调用时通过插件工厂动态创建,每次分词任务分配一个实例、用完释放,天然无共享状态问题。

plugin_deinit:对称的收尾

卸载钩子非常简洁——只调用 ObFTParsePluginData::deinit_global() 销毁全局单例(停用词表、词典中心、分配器),不需要逐个”反注册”分词器:插件框架随插件卸载统一回收名字表。这种”注册交给框架、销毁只管自己资源”的分工,避免了双向清表的复杂度。

OBP_DECLARE_PLUGIN:插件元信息声明

OBP_DECLARE_PLUGIN(fts_parser)
{
  OBP_AUTHOR_OCEANBASE,
  OBP_MAKE_VERSION(0, 0, 1), // version 1 is compatible with current version
  OBP_LICENSE_MULAN_PUBL_V2,
  plugin_init,
  plugin_deinit
} OBP_DECLARE_PLUGIN_END;

这个宏向插件框架描述插件的元信息:插件名 fts_parser、作者、版本号、许可证和两个生命周期钩子。observer 进程启动加载内置插件时触发 plugin_init,五个分词器就此注册完毕。版本号 0.0.1 配合注释”version 1 is compatible”说明内置插件没有独立升级语义——它们随内核版本一起演进。

小结

至此,OceanBase 4.3 全文检索分词器系列的源码拆解告一段落。回看整个体系:一个插件注册文件把五种分词器装进统一的插件框架;一个全局单例共享停用词表与词典;一个 Desc 接口统一”创建实例、执行分词、释放迭代器”的生命周期。用户从 WITH PARSER ik 这句 DDL 出发,经过名字查找、Desc 工厂、分词流水线,最终落到倒排索引的词项行——所有环节都建立在”内置与外置分分词器无差别”的插件抽象之上。理解了这个注册入口,再看任何一篇分词器源码解读时,就清楚它们各自在整张地图上的坐标了。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注