全文检索(FTS)在 OceanBase 4.3 里被拆成了多层:底层有 space/ngram/beng/ik/ngram2 等具体分词器,中间有停用词、词典缓存,上层索引构建只需要一个统一 facade。本文解读的 storage/fts/ob_fts_plugin_helper.cpp 就是这个 facade 与资源管理层:它负责 parser 名字解析、进程级全局数据初始化、单次分词请求的参数装配,以及把分词结果输出成 JSON 倒排文档。全文检索链路中的分词动作,最终都会收敛到这个文件里的 ObFTParseHelper::segment。
1. 核心数据结构
文件里三个类职责清晰,呈”名字 → 全局资源 → 会话助手”的层次:
- ObFTParser:把索引表元信息里的
plugin_name(如default_parser.1)解析成parser_name_ + parser_version_二元组,并提供反向序列化。 - ObFTParsePluginData:进程级单例,统一管理
ObStopWordChecker与ObFTDictHub,生命周期由 observer 启动/退出显式控制。 - ObFTParseHelper:会话级对象,按 parser 名字与 JSON 配置初始化后,可反复调用
segment()处理文档;通过check_is_the_same()判断能否复用。
2. Parser 名字解析
索引表 schema 里存储的 parser 名是字符串 name.version,ObFTParser::parse_from_str 负责反解,并用 is_valid() 校验:
int ObFTParser::parse_from_str(const char *plugin_name, const int64_t buf_len)
{
int ret = OB_SUCCESS;
if (OB_ISNULL(plugin_name)) {
ret = OB_INVALID_ARGUMENT;
} else if (OB_UNLIKELY(buf_len >= share::OB_PLUGIN_NAME_LENGTH)) {
ret = OB_INVALID_ARGUMENT;
} else {
char name[share::OB_PLUGIN_NAME_LENGTH];
char *saveptr = nullptr;
char *token = nullptr;
char *end_ptr = nullptr;
MEMCPY(name, plugin_name, buf_len);
name[buf_len] = '';
if (OB_ISNULL(token = STRTOK_R(name, ".", &saveptr))) {
ret = OB_ERR_UNEXPECTED;
} else if (OB_FAIL(parser_name_.set_name(token))) {
} else if (OB_ISNULL(token = STRTOK_R(nullptr, ".", &saveptr))) {
ret = OB_ERR_UNEXPECTED;
} else if (OB_FAIL(ob_strtoll(token, end_ptr, parser_version_))) {
} else if (OB_NOT_NULL(token = STRTOK_R(nullptr, ".", &saveptr))) {
ret = OB_ERR_UNEXPECTED; // 不允许第三段
} else if (OB_UNLIKELY(!is_valid())) {
ret = OB_INVALID_ARGUMENT;
}
}
return ret;
}
3. 进程级全局资源
ObFTParsePluginData 是 observer 进程内唯一实例,内部包含停用词检查器与词典中心:
int ObFTParsePluginData::init()
{
int ret = OB_SUCCESS;
lib::ObMemAttr mem_attr;
mem_attr.label_ = FTPARSE_PLUGIN_DATA_MEMORY_LABEL;
if (OB_FAIL(handler_allocator_.init(lib::ObMallocAllocator::get_instance(),
OB_MALLOC_NORMAL_BLOCK_SIZE,
mem_attr))) {
} else if (OB_FAIL(init_and_set_stopword_list())) {
} else if (OB_FAIL(init_dict_hub())) {
} else {
is_inited_ = true;
}
if (OB_UNLIKELY(!is_inited_)) {
destroy(); // 任一环节失败都回滚
}
return ret;
}
这里用 ObFIFOAllocator 给停用词对象和词典对象提供长期内存,失败路径统一 destroy(),避免半初始化单例。上层通过 get_dict_hub() 把词典引用传递给具体 parser(如 IK 分词器),实现一份词典多租户共享。
4. Helper 初始化与复用判断
ObFTParseHelper::init 把 parser 名字、JSON 配置、插件框架三者串起来:
int ObFTParseHelper::init(common::ObIAllocator *allocator,
const common::ObString &plugin_name,
const common::ObString &plugin_properties)
{
int ret = OB_SUCCESS;
// ... 参数校验 ...
if (OB_FAIL(parser_name_.parse_from_str(plugin_name.ptr(), plugin_name.length()))) {
} else if (OB_FAIL(parser_property_.parse_for_parser_helper(parser_name_, plugin_properties))) {
} else if (OB_FAIL(ObPluginHelper::find_ftparser(parser_name_.get_parser_name().str(),
parser_desc_, plugin_param_))) {
} else if (OB_ISNULL(parser_desc_)) {
} else if (OB_FAIL(set_add_word_flag(*parser_desc_))) {
} else {
allocator_ = allocator;
is_inited_ = true;
}
if (OB_FAIL(ret) && OB_UNLIKELY(!is_inited_)) {
reset();
}
return ret;
}
关键步骤是 ObPluginHelper::find_ftparser:内置 parser 直接命中本地实现,自定义 parser 则走插件系统加载动态库。set_add_word_flag 把 parser 声明的归一化策略(如大小写折叠、按词分组)保存到 add_word_flag_,后续倒排写入时统一执行。
上层为了避免每次 DML 都重建 helper,会调用 check_is_the_same():把新的 plugin_name + plugin_properties 再解析一遍,与当前对象比对,完全一致才复用。
5. 分词流水线
公开入口 ObFTParseHelper::segment 先校验字符集,再构造 ObAddWord 收集器,最后调用静态 segment():
int ObFTParseHelper::segment(const ObFTParserProperty &property,
const int64_t parser_version,
const ObIFTParserDesc *parser_desc,
ObPluginParam *plugin_param,
const ObCharsetInfo *cs,
const char *ft,
const int64_t ft_len,
common::ObIAllocator &allocator,
ObAddWord &add_word)
{
int ret = OB_SUCCESS;
// ... 参数校验 ...
ObFTParserParam param;
ObITokenIterator *iter = nullptr;
param.allocator_ = &allocator;
param.cs_ = cs;
param.fulltext_ = ft;
param.ft_length_ = ft_len;
param.parser_version_ = parser_version;
param.plugin_param_ = plugin_param;
param.ngram_token_size_ = property.ngram_token_size_;
param.ik_param_.mode_ = property.ik_mode_smart_ ? ObFTIKParam::Mode::SMART
: ObFTIKParam::Mode::MAX_WORD;
param.min_ngram_size_ = property.min_ngram_token_size_;
param.max_ngram_size_ = property.max_ngram_token_size_;
if (OB_FAIL(parser_desc->segment(¶m, iter))) {
} else if (OB_ISNULL(iter)) {
} else {
const char *word = nullptr;
int64_t word_len = 0;
int64_t char_cnt = 0;
int64_t word_freq = 0;
while (OB_SUCC(ret)) {
if (OB_FAIL(iter->get_next_token(word, word_len, char_cnt, word_freq))) {
if (OB_ITER_END != ret) {
LOG_WARN("fail to get next token", K(ret), KPC(iter));
}
} else if (OB_FAIL(add_word.process_word(word, word_len, char_cnt, word_freq))) {
}
}
if (OB_ITER_END == ret) {
ret = OB_SUCCESS;
}
}
if (OB_NOT_NULL(iter)) {
parser_desc->free_token_iter(¶m, iter);
iter = nullptr;
}
return ret;
}
流程可概括为:装配 ObFTParserParam → 调用 parser 的 segment() 拿到迭代器 → 循环取 token 并交给 ObAddWord 做归一化与去重 → 用 OB_ITER_END 判定结束 → 释放迭代器。ObAddWord 内部会参考 add_word_flag_ 决定是否大小写归一、是否按词分组,并维护 ObFTWordMap 词频表。
6. JSON 倒排文档输出
分词完成后,helper 提供两种 JSON 输出:详细版带词频,简化版只有词列表:
// 详细版:{ "doc_len": N, "tokens": [{word: freq}, ...] }
int ObFTParseHelper::make_detail_json(const ObFTWordMap &words,
const int64_t doc_length,
common::ObIJsonBase *&json_root);
// 简化版:[word1, word2, ...]
int ObFTParseHelper::make_token_array_json(const ObFTWordMap &words,
common::ObIJsonBase *&json_root);
两个函数都使用 allocator_ 分配 JSON 节点,失败路径逐个删除已分配对象,避免内存泄漏。详细版中的 doc_len 来自 add_word.get_add_word_count(),即文档分出的词项总数,供后续相关性评分使用。
7. 小结
ob_fts_plugin_helper.cpp 本身不包含具体分词算法,却是 FTS 链路中承上启下的一层:
- 名字解析让 schema 字符串与代码对象可双向转换;
- 全局单例把停用词、词典等重资源提升到进程级共享;
- Helper facade让上层索引构建只关心 “初始化一次、反复分词”;
- JSON 输出统一了倒排文档的数据格式,供下游索引写入。
理解这一层,再看 ob_ik_ft_parser.cpp 等具体 parser 实现时,就能清楚它们被谁调用、参数从何而来、迭代器由谁释放。