feat: support nested tokenizer wrappers - #1103
Conversation
| StopwordTokenizerDelete, | ||
| StopwordTokenizerTokenize}; | ||
| const auto create_code = | ||
| api->xCreateTokenizer_v2(api, "stopwords", context.get(), &tokenizer_api, |
There was a problem hiding this comment.
这里把注册的 wrapper 名从 builtin_stopwords 改成了 stopwords,需要明确一下磁盘格式兼容策略。
#1096 引入 stopwords 时写入磁盘的 spec 是 tokenize='builtin_stopwords unicode61' 这类形式(jieba 是裸 'jieba' + 内联过滤)。改名后旧 spec 里的 tokenizer 名不再注册。我查了 pinned SQLite 源码确认失败路径:tokenizer 是惰性加载的(fts5_config.c:811 → sqlite3Fts5LoadTokenizer),首次 tokenize 时才报 no such tokenizer: builtin_stopwords(fts5_main.c:3539)——即旧索引文件能打开(ValidateExistingTable 只检查表存在),但第一次查询/插入才报错,且报错信息对用户没有指引。旧的 'jieba' spec 索引能打开,但内联 stopword 过滤已删,过滤会静默丢失:含 stopword 的混合查询(如 the database)从"能搜到"变成"搜不到"。
影响范围我核实过:v0.2.0 正式版(最新 tag)的 BuiltinFTSTokenizer::Register 是空实现,磁盘 spec 就是裸 'unicode61'/'jieba',这些名字在新代码里仍能解析,正式版用户的索引不受影响;受影响的只有 #1096 之后、本 PR 之前的开发版/weekly wheel 创建的索引。
建议二选一:
- (a) wrapper 同时以旧名
"builtin_stopwords"再注册一个 alias(多一次xCreateTokenizer_v2调用即可),旧索引无缝可用;或 - (b)
ValidateExistingTable从sqlite_master读出原 spec 与full_tokenizer_name_比对,不一致时在 open 阶段抛清晰错误(提示 DROP INDEX 重建),把莫名的运行时 SQLite 报错变成可操作的指引。
无论选哪个,fts_search.md 里 "Index checkpoints created by earlier versions remain compatible" 这句承诺也请按最终决策同步更新。
There was a problem hiding this comment.
现在如果stopwords=none,就不会额外创建一个stopwords wrapper,用户看到的行为和v0.2.0一致。
文档已经修改为兼容v0.2.0,开发版本不考虑兼容。
b4f4dd0
| db.close() | ||
|
|
||
|
|
||
| def test_porter_wrapper_uses_jieba_for_mixed_text(tmp_path): |
There was a problem hiding this comment.
建议补一个 wrapper spec 的 reopen/持久化用例。本 PR 改动的核心机制正是"写入表的 tokenize spec"与"reopen 时按名注册"的对应关系,但现有 porter jieba 用例(这里和 C++ 侧)都只覆盖了单进程内的 create + search。
可以仿照上面 test_fts_stopwords_persist_across_checkpoint 的写法:CREATE INDEX ... WITH (tokenizer = 'porter jieba') → close → 重开库 → 验证中文分词(如 向量)和英文词干搜索(如 embedding 命中 embeddings)仍然正常。这样能锁住 reopen 后注册路径不再退化的行为。
There was a problem hiding this comment.
增加了测试,覆盖了所有tokenizer组合的reopen恢复。
5c33146
| auto stopwords = std::move(option->second); | ||
| config.erase(option); | ||
| LoadStopwords(stopwords); | ||
| base_tokenizer_ = Create(std::move(config), full_name); |
There was a problem hiding this comment.
将 Jieba 的内联 stopword 过滤改为 wrapper 后,Jieba 索引和查询也会进入 StopwordTokenFilter。目前每个 token 都执行 std::string(token, token_size) 再查 unordered_set,长 token 会产生逐 token 堆分配,这是 Jieba 路径新增的热路径开销。建议使用支持 heterogeneous lookup 的透明 hash/equality,直接以 std::string_view 查询;或者至少补一组前后 tokenizer benchmark,确认该抽象没有明显降低建索引/查询吞吐。
There was a problem hiding this comment.
unordered_map支持heterogeneous lookup,允许接收string或者string_view,避免stopwords判断时的拷贝构造开销。
4847798
There was a problem hiding this comment.
这个功能需要c++20,wheel打包的c++版本较低,重构后全部统一使用unordered_map<string_view>,不再用异构输入。
d3acb31
| StopwordTokenizerDelete, | ||
| StopwordTokenizerTokenize}; | ||
| const auto create_code = | ||
| api->xCreateTokenizer_v2(api, "stopwords", context.get(), &tokenizer_api, |
There was a problem hiding this comment.
现在 stopwords 被提升为可组合 wrapper,建议同时补齐其 create callback 的参数校验。StopwordTokenizerCreate 会直接读取 arguments[0],但没有检查 context/output/arguments 以及 argument_count >= 1。正常生成的 spec 都带 base tokenizer,不过损坏或手工生成的持久化 FTS 表可能让这里越界,而不是返回可诊断的 SQLITE_ERROR。建议在读取 arguments[0] 前统一校验。
There was a problem hiding this comment.
stopwords是内部wrapper,不对用户暴露,这里必定保证操作合法,无需防御检查。
| config.erase(option); | ||
| } else { | ||
| throw std::invalid_argument("Unsupported FTS tokenizer: " + name); | ||
| name.erase(0, name.find_first_not_of(' ', separator)); |
There was a problem hiding this comment.
这里对尾部空格的处理会让 tokenizer = "porter " 与 tokenizer = "porter" 表现不一致:find_first_not_of 返回 npos 后 name 被清空,但 tokenizer key 仍留在 config 中;随后 BuiltinFTSTokenizer 认为显式指定了 base,不会补默认的 unicode61,递归解析空名称并报 Unsupported FTS tokenizer。建议 trim 后若 remainder 为空就删除该 key,并补 porter 、 porter 、porter jieba 的用例。
| fts5_tokenizer_v2 base_api{}; | ||
| Fts5Tokenizer* base_tokenizer{}; | ||
| const std::unordered_set<std::string>* stopwords{}; | ||
| const std::unordered_set<std::string_view>* stopwords{}; |
There was a problem hiding this comment.
现在stopwords改用引用,并且使用花括号初始化兼容低版本c++。其他成员变量都要和sqlite对接,保留裸指针。
701c49e
There was a problem hiding this comment.
所以这个裸指针是谁在own,生命周期是什么呀
What do these changes do?
重构了现有的Tokenizer类,现在tokenizer能够支持嵌套,即FTS5中的wrapper。
目前porter和stopwords都会做成wrapper的形式,而不是在每个tokenizer中重复编写一遍过滤函数。
重构后可以各种配置选项(包括大小写敏感、特殊字符的处理等)都可以注册为一个独立的wrapper,与现有的tokenizer解耦。
Related issue number
Fixes #1101