在 OceanBase 4.3 的全文检索体系里,用户建全文索引时可以顺便”调教”分词器——比如指定 IK 模式、自定义词典表、控制 token 长度上下限。这些配置以 JSON 形式跟着 WITH PARSER 一起写进 DDL,而负责接住它们的就是本文的主角:src/storage/fts/ob_fts_parser_property.cpp。这个文件有两个核心类:ObFTParserJsonProps 用一棵 JSON 树作为属性的统一内存表示,负责解析、校验、落库和 DDL 回显;ObFTParserProperty 则是面向分词执行期的”扁平视图”,把 JSON 属性摊平成几个标量字段,供 ObFTParseHelper 等运行时组件直接取用。一个管”存”,一个管”用”,分工清晰。
核心数据结构:一棵 JSON 树走天下
ObFTParserJsonProps 的全部状态就是一个 root_ 指针指向 ObJsonObject,外加一个成员级 allocator_。属性以 key-value 挂在树上,支持的配置项包括:
- min_token_size / max_token_size:token 长度上下限(space、beng 解析器用)
- ngram_token_size:n-gram 窗口大小
- min/max_ngram_token_size:ngram2 的窗口区间
- dict_table / stopword_table / quantifier_table:IK 的用户词典、停用词表、量词表
- ik_mode:smart(智能切分)或 max_word(最细粒度切分)
初始化逻辑很轻,就是在成员分配器上 new 一个空对象作为树根:
int ObFTParserJsonProps::init()
{
int ret = OB_SUCCESS;
if (is_inited_) {
ret = OB_INIT_TWICE;
} else if (OB_ISNULL(root_ = OB_NEWx(ObJsonObject, &allocator_, &allocator_))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
} else {
is_inited_ = true;
}
if (OB_FAIL(ret)) {
OB_DELETEx(ObIJsonBase, &allocator_, root_);
root_ = nullptr;
}
return ret;
}
用成员分配器而非临时分配器是有意为之——属性树要活到整个对象生命周期结束,落库序列化时还能再榨一次值。
setter 系列:”先分配后校验”的兜底写法
每个配置项对应一个 config_set_*,结构完全同构,以 min_token_size 为例:
int ObFTParserJsonProps::config_set_min_token_size(const int64_t size)
{
int ret = OB_SUCCESS;
ObJsonInt *min_token_size = nullptr;
if (!IS_INIT) {
ret = OB_NOT_INIT;
} else if (OB_ISNULL(min_token_size = OB_NEWx(ObJsonInt, &allocator_, size))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
} else if (!is_valid_min_token_size(size)) {
ret = OB_INVALID_ARGUMENT;
} else if (OB_FAIL(root_->object_add(ObString(ObFTSLiteral::CONFIG_NAME_MIN_TOKEN_SIZE),
min_token_size))) {
}
if (OB_FAIL(ret)) {
OB_DELETEx(ObJsonInt, &allocator_, min_token_size);
}
return ret;
}
注意这里的顺序:先 new 节点、再校验、最后挂树。因为分配发生在校验之前,任何一步失败都必须手动 DELETE 掉刚分配的 JSON 节点,否则就泄漏了。这是 C++ 里”分配先行”模式下必须自兜底的典型写法,OCB 风格里到处都是这种模式。
字符串进出:parse 与序列化
属性有两个出入口。入口 parse_from_valid_str:空串视为”无属性”,保持默认空对象;否则用 ObJsonParser::get_tree 建树后整体替换 root_。语法合法性在 SQL 层已经把关,这里只负责结构化。出口 to_format_json:把树 print 成紧凑 JSON 串(不缩进),空树返回空串。这个字符串会随 DDL 写进元数据表,是解析器属性落库的最终形态。
DDL 时刻的属性体检:rebuild_props_for_ddl
建索引等 DDL 执行时,rebuild_props_for_ddl 会对用户传入的属性做一次”体检 + 默认值补全”,把错误挡在执行前。它是一个分派器:先把解析器名字符串转成 ObFTParser,再按类型路由:
int ObFTParserJsonProps::rebuild_props_for_ddl(const ObString &parser_name,
const common::ObCollationType &type,
const bool log_to_user)
{
int ret = OB_SUCCESS;
ObFTParser parser;
if (!IS_INIT) {
ret = OB_NOT_INIT;
} else if (OB_FAIL(parser.parse_from_str(parser_name.ptr(), parser_name.length()))) {
} else if (parser.is_ik()) {
ret = ik_rebuild_props_for_ddl(log_to_user);
} else if (parser.is_space()) {
ret = space_rebuild_props_for_ddl(log_to_user);
} else if (parser.is_ngram()) {
ret = ngram_rebuild_props_for_ddl(log_to_user);
} else if (parser.is_beng()) {
ret = beng_rebuild_props_for_ddl(log_to_user);
} else if (parser.is_ngram2()) {
ret = ngram2_rebuild_props_for_ddl(log_to_user);
} else {
ret = plugin_rebuild_props_for_ddl(log_to_user); // 未识别类型兜底
}
return ret;
}
每个 *_rebuild_props_for_ddl 做两件事:白名单校验和默认值补全。白名单校验由 check_unsupported_config 统一实现——遍历属性树每个 key,与该解析器支持的配置数组逐一 case_compare,发现不认识的 key 立即报 OB_NOT_SUPPORTED。
以最复杂的 IK 为例,白名单只认三张词典表加 ik_mode;逐项 get,查不到(OB_SEARCH_NOT_FOUND)就补默认值:
int ObFTParserJsonProps::ik_rebuild_props_for_ddl(const bool log_to_user)
{
static const char *supported[] = {
ObFTSLiteral::CONFIG_NAME_DICT_TABLE,
ObFTSLiteral::CONFIG_NAME_QUANTIFIER_TABLE,
ObFTSLiteral::CONFIG_NAME_STOPWORD_TABLE,
ObFTSLiteral::CONFIG_NAME_IK_MODE,
};
bool has_unsupported = false;
if (OB_FAIL(check_unsupported_config(supported, ARRAYSIZEOF(supported), has_unsupported))) {
} else if (has_unsupported) {
ret = OB_NOT_SUPPORTED;
if (log_to_user) {
LOG_USER_ERROR(OB_NOT_SUPPORTED, "ik config");
}
}
// ... 逐项 config_get_dict_table / config_get_ik_mode 等,
// OB_SEARCH_NOT_FOUND 时 config_set 补默认词典表与 SMART 模式
}
有个小彩蛋:这段源码里 quantifier 表的 get+set 段落重复出现了两次,属于无害的冗余代码,不影响语义。log_to_user 参数控制是否通过 LOG_USER_ERROR 把错误直接回显给终端用户——内部重建与用户 DDL 两条路径共用这套逻辑时,反馈策略不同。
执行期视图:parse_for_parser_helper
真正分词时,运行时组件不想逐个 key 去 JSON 树里查。于是 ObFTParserProperty::parse_for_parser_helper 把属性”摊平”成标量字段,按解析器类型各取所需:
int ObFTParserProperty::parse_for_parser_helper(const ObFTParser &parser,
const ObString &json_str)
{
int ret = OB_SUCCESS;
ObFTParserJsonProps props;
if (OB_FAIL(props.init())) {
} else if (OB_FAIL(props.parse_from_valid_str(json_str))) {
} else if (parser.is_ik()) {
dict_table_ = ObString(ObFTSLiteral::CONFIG_NAME_DICT_TABLE);
stopword_table_ = ObString(ObFTSLiteral::CONFIG_NAME_STOPWORD_TABLE);
quantifier_table_ = ObString(ObFTSLiteral::CONFIG_NAME_QUANTIFIER_TABLE);
ObString ik_smart;
if (OB_FAIL(props.config_get_ik_mode(ik_smart))) {
if (OB_SEARCH_NOT_FOUND == ret) {
ik_mode_smart_ = true; // 旧版本没写 ik_mode,走默认 SMART
ret = OB_SUCCESS;
}
} else if (0 == ik_smart.case_compare(ObString(ObFTSLiteral::FT_IK_MODE_SMART))) {
ik_mode_smart_ = true;
} else {
ik_mode_smart_ = false; // max_word
}
}
// space/beng 取 min/max_token_size,ngram 取 ngram_token_size,
// ngram2 取 min/max_ngram_token_size,空串时落到构造函数默认值
}
这里有个兼容性细节:ik_mode 缺失时不是报错而是静默置 ik_mode_smart_ = true——注释明确写着”from old version, ik_mode is not set”。4.3 之前建的 IK 全文索引元数据里没有这个字段,升级后必须能继续工作,所以读取侧要容忍缺省,落到构造函数里的默认值。
DDL 回显:show_parser_properties
SHOW CREATE TABLE 要把索引属性原样展示回来,show_parser_properties 负责把 JSON 树格式化成 PARSER_PROPERTIES=(key=value,...) 文本。逐项 get,OB_SEARCH_NOT_FOUND 静默跳过(没有的属性就不显示),用 need_comma 宏控制逗号分隔,字符串值如 ik_mode 额外包双引号。文件末尾还有个 tokenize_array_to_props_json 工具函数,把形如 [{"key":value}] 的数组形式属性展平重组为对象串,中间树用栈上 ObArenaAllocator(函数退出即释放),只有最终输出串用外部分配器——临时与持久内存分离的老套路。
小结
这个文件回答了一个完整的问题:用户自定义的解析器属性,从 DDL 文本到落库元数据,再到分词执行现场,是怎样一条链路。JSON 树是贯穿全程的中间表示:SQL 层解析字符串建树 → DDL 时刻按解析器类型做白名单体检并补默认值 → 序列化进元数据 → 执行期再解析、摊平成标量交给分词器。校验前置、缺省兜底、新旧版本兼容,三个设计考量在代码里都有清晰的落点。配合前几篇的 IK 分词器、插件助手和文档扫描迭代器,4.3 全文检索模块的拼图又完整了一块。