OceanBase源码解读:FTS插件助手 ob_fts_plugin_helper

全文检索(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(&param, 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(&param, 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 实现时,就能清楚它们被谁调用、参数从何而来、迭代器由谁释放。

发表回复

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