OceanBase源码解读:全文检索文档词项扫描迭代器

在 OceanBase 4.3 的全文检索(Full-Text Search, FTS)体系中,倒排索引的构建与查询都需要一种能力:按文档维度读取”词项明细”——即某篇文档包含了哪些词、每个词出现了多少次、出现在什么位置。本文解读的 ObFTDocWordScanIterator 正是承担这一职责的核心组件,它位于 storage/fts/ 目录,衔接上层的 FT 索引构建流程与底层的通用存储扫描接口。

一、架构位置与设计动机

OceanBase 的全文索引采用经典的”倒排表”设计:物理上维护一张隐藏的 doc-word 辅助表,主键为 (doc_id, word),列中记录词频、位置等统计信息。当需要为某篇文档构建或更新倒排索引时,系统必须按 doc_id 前缀扫描这张辅助表,取出该文档的全部词项。

直接复用通用的 ObTableScanIterator 是最经济的选择——它已具备 MVCC 一致性读、缓存、下推等全套能力。ObFTDocWordScanIterator 的作用就是做一层薄封装:把”按 doc_id 扫描词项”这个业务语义,翻译成存储层能理解的 scan_param_ 与 key_range,并对外暴露简洁的迭代器接口。

二、关键数据结构

类定义精简,核心成员如下:

class ObFTDocWordScanIterator final {
  common::ObArenaAllocator allocator_;      // 生命周期级内存(table_param等)
  common::ObArenaAllocator scan_allocator_; // 扫描级临时内存
  share::schema::ObTableParam table_param_; // 表结构参数
  storage::ObTableScanParam scan_param_;    // 扫描参数(key_range、snapshot等)
  common::ObNewRowIterator *doc_word_iter_; // 底层存储扫描迭代器
  bool is_inited_;
};

双分配器设计值得注意:allocator_ 负责贯穿对象生命周期的结构(如 table_param_);scan_allocator_ 只承载单次扫描的临时对象。当迭代器需要切换下一个 doc_id 时,reuse() 方法仅重置 scan_allocator_ 并保留一页内存,避免频繁向操作系统申请。

三、核心流程

1. 初始化:init → init_scan_param

init() 接收 table_id / ls_id / tablet_id / snapshot / schema_version 五元组,完成扫描参数的组装:

int ObFTDocWordScanIterator::init(
    const uint64_t table_id,
    const share::ObLSID &ls_id,
    const common::ObTabletID &tablet_id,
    const transaction::ObTxReadSnapshot *snapshot,
    const int64_t schema_version)
{
  // 防重复初始化
  if (OB_UNLIKELY(is_inited_)) { ret = OB_INIT_TWICE; }
  // 构建扫描参数(含schema、snapshot、query_flag等)
  else if (OB_FAIL(init_scan_param(...))) { ... }
  else { is_inited_ = true; }
  // 失败自动reset,防止半初始化泄漏
  if (OB_UNLIKELY(!is_inited_)) { reset(); }
}

init_scan_param 内部设置了大量扫描标志:Forward 顺序、MySQL 模式、无 index_back、无聚合下推等。snapshot.assign(*snapshot) 保证事务一致性读,tx_id 取自快照核心,供存储层做 MVCC 可见性判断。

2. 表参数构建:build_table_param

从多版本 Schema 服务拉取表结构,并强制校验列数必须为 4:

else if (OB_UNLIKELY(4 != column_ids.count())) {
  ret = OB_ERR_UNEXPECTED;
  LOG_WARN("unexpected error, column count isn't 4 for fts doc word", ...);
}

这 4 列对应 doc-word 辅助表的固定 schema,通常语义为:doc_id、word、word_freq、word_pos。

3. 按 doc_id 扫描:do_scan

这是对外主入口。首次调用走 table_scan 新建底层迭代器;后续切换 doc_id 时走 table_rescan 复用已有迭代器,减少对象创建开销:

int ObFTDocWordScanIterator::do_scan(
    const uint64_t table_id,
    const common::ObDocId &doc_id)
{
  const bool need_rescan = nullptr != doc_word_iter_;
  // ... 参数校验 ...
  if (need_rescan) { reuse(); }

  // 构造 [doc_id, MIN] ~ [doc_id, MAX] 闭区间
  if (FAILEDx(build_key_range(table_id, doc_id, scan_param_.key_ranges_))) { ... }
  else if (need_rescan && OB_FAIL(do_table_rescan())) { ... }
  else if (!need_rescan && OB_FAIL(do_table_scan())) { ... }
}

build_key_range 以 doc_id 为前缀,第二列取 [MIN_OBJ, MAX_OBJ],形成该文档全部词项的闭区间范围。底层存储引擎收到这个 range 后,即可精准定位到该文档的所有词项行。

4. 行消费:get_next_row

将底层通用 ObTableScanIterator 转型后透传,迭代结束返回 OB_ITER_END:

int ObFTDocWordScanIterator::get_next_row(blocksstable::ObDatumRow *&datum_row)
{
  ObTableScanIterator *tsc_iter = nullptr;
  FALSE_IT(tsc_iter = static_cast<ObTableScanIterator *>(doc_word_iter_));
  if (OB_FAIL(tsc_iter->get_next_row(datum_row))) {
    if (OB_ITER_END != ret) { LOG_WARN(...); }
  }
}

5. 资源管理:reset 与 reuse

reset() 负责彻底释放:先调用 revert_scan_iter 归还存储层迭代器,再重置分配器。顺序敏感——若先 reset allocator_,doc_word_iter_ 内部指向的内存将悬空。reuse() 则只做轻量清理:清 key_range、回收扫描级内存、保留迭代器壳子。

四、小结

ObFTDocWordScanIterator 是一个典型的”业务语义薄封装”:它不涉及分词、不涉及倒排结构组织,只专注于”按文档读词项”这一件事。通过复用通用 TableScan 能力,它在不到 300 行代码内完成了 MVCC 一致性读、迭代器复用、资源安全释放等全套机制。理解它,有助于把握 OceanBase FTS 体系中”存储层提供通用能力、业务层做语义适配”的分层设计哲学。

发表回复

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