GRDB.swift 自定义 FTS5 Tokenizer 完全指南:从协议原理到同义词与拉丁字符容错实战
【免费下载链接】GRDB.swiftA toolkit for SQLite databases, with a focus on application development项目地址: https://gitcode.com/GitHub_Trending/gr/GRDB.swift
GRDB.swift 基于 SQLite 的可扩展全文检索引擎 FTS5,允许开发者自定义 tokenizer(分词器),并扩展 SQLite 内建分词器的能力。本文以 Documentation/FTS5Tokenizers.md 为骨架,结合仓库源码 GRDB/FTS/FTS5CustomTokenizer.swift、GRDB/FTS/FTS5WrapperTokenizer.swift 与完整测试用例 Tests/GRDBTests/FTS/FTS5CustomTokenizerTests.swift,系统讲解 FTS5Tokenizer / FTS5CustomTokenizer / FTS5WrapperTokenizer 三个协议的分层设计,并给出可直接落地的同义词与拉丁字符容错实现,帮助你为多语言、多拼写变体的应用构建精确匹配的全文检索方案。
Tokenizer 与全文检索的关系
Tokenizer 把文本切分为 token(词元)。例如,一个 tokenizer 可以把 "SQLite is a database engine" 切成 "SQLite"、"is"、"a"、"database"、"engine" 五个 token。
FTS5 使用 tokenizer 对索引文档和搜索模式两方都做分词。当文档与搜索模式产生完全一致的 token 时,二者匹配。因此 tokenizer 的变换策略直接决定了哪些文本能互相匹配:
- 所有 SQLite 内建 tokenizer 都会把 "SQLite" 与 "sqlite" 统一成小写 token "sqlite",这正是它们大小写不敏感的原因。概括地说,不同的 tokenizer 通过给输入文本施加不同的变换来获得不同的匹配行为。
- asciitokenizer 把所有 ASCII 字符转为小写。"SQLite is a database engine" 产生 "sqlite"、"is"、"a"、"database"、"engine";查询 "SQLITE DATABASE" 能匹配,因为其 token "sqlite" 与 "database" 都存在于文档中。
- unicode61tokenizer 会去掉拉丁字符的变音符(diacritics)。与 ascii 不同,它能让 "Jérôme" 匹配 "Jerome",因为二者都产生 "jerome"。
- portertokenizer 把英文单词还原为词根:"database engine" 产生 "databas" 与 "engin";查询 "database engines" 能匹配,因为它产生同样的 token。
但内建 tokenizer 无法让 "first" 匹配 "1st",因为它们产生的是 "first" 与 "1st" 两个不同 token;也无法让 "Grossmann" 匹配 "Großmann",因为二者产生 "grossmann" 与 "großmann"。
自定义 tokenizer 正是为解决这类问题而生:本文后面的同义词示例会把 "first" 与 "1st" 都切为同义词 token 使二者互匹配;拉丁字符示例则把 "Grossmann"、"Großmann" 统一转成 "grossmann"。官方文档列出的典型用例还包括:"fi" 匹配连字 "fi"(U+FB01)、"Encyclopaedia" 匹配 "Encyclopædia"、"Mueller" 匹配 "Müller"、"romaji" 匹配 "ローマ字"、"pinyin" 匹配 "拼音",以及屏蔽 "the" 等停用词。
Tokenizer 协议体系:三层设计
GRDB 通过三个协议让开发者使用和定义 FTS5 tokenizer,它们之间是逐层继承、逐层抽象的关系:
- FTS5Tokenizer:所有 FTS5 tokenizer 的协议,包括内建的 ascii、unicode61、porter。
- FTS5CustomTokenizer:底层自定义协议,让自定义 tokenizer 直接对接原始 FTS5 C API。
- FTS5WrapperTokenizer:高层自定义协议,用于对另一个 tokenizer 产生的 token 做后处理。
- FTS5CustomTokenizer:底层自定义协议,让自定义 tokenizer 直接对接原始 FTS5 C API。
这条继承链在源码中清晰可见:FTS5CustomTokenizer: FTS5Tokenizer(见 GRDB/FTS/FTS5CustomTokenizer.swift),FTS5WrapperTokenizer: FTS5CustomTokenizer(见 GRDB/FTS/FTS5WrapperTokenizer.swift)。绝大多数自定义场景只需要最上层的FTS5WrapperTokenizer。
使用自定义 Tokenizer 的三步流程
只要自定义类型遵循FTS5CustomTokenizer或FTS5WrapperTokenizer,它就能驱动 FTS5 引擎,使用流程固定为三步。
第一步:注册自定义 tokenizer 到数据库。推荐把注册放进Configuration.prepareDatabase,这样无论DatabaseQueue还是DatabasePool都会在每次建立连接时自动完成注册:
class MyTokenizer : FTS5CustomTokenizer { ... } var config = Configuration() config.prepareDatabase { db in db.add(tokenizer: MyTokenizer.self) } let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)db.add(tokenizer:)在源码中通过xCreateTokenizerC 回调把 tokenizer 的名字与构造器注册进 FTS5 API(见 GRDB/FTS/FTS5CustomTokenizer.swift)。注意,tokenizer 实例的生命周期由Unmanaged.passRetained托管,直到xDeleteTokenizer被调用才释放——这保证了 SQLite 在其存续期内总能安全访问你的实例。
第二步:创建使用该自定义 tokenizer 的全文表:
try db.create(virtualTable: "documents", using: FTS5()) { t in t.tokenizer = MyTokenizer.tokenizerDescriptor() t.column("content") }tokenizerDescriptor()是FTS5CustomTokenizer的默认扩展,它把 tokenizer 名字与可选参数拼接为FTS5TokenizerDescriptor(见 GRDB/FTS/FTS5CustomTokenizer.swift)。db.create(virtualTable:using:_:)在底层会把描述符渲染进CREATE VIRTUAL TABLE语句的tokenize='...'子句(见 GRDB/QueryInterface/Schema/VirtualTableModule.swift)。
第三步:以常规方式写入并查询全文表,完全不需要感知自定义 tokenizer 的存在:
try db.execute(sql: "INSERT INTO documents VALUES (?)", arguments: ["..."]) try Document(content: "...").insert(db) let pattern = FTS5Pattern(matchingAnyTokenIn:"...") let documents = try Document.matching(pattern).fetchAll(db)全文表的写入与查询细节可参考 README.md 与 Documentation/FullTextSearch.md。
FTS5Tokenizer:所有 tokenizer 的基协议
FTS5Tokenizer是所有 FTS5 tokenizer 的协议,也是 GRDB 对内建 tokenizer 的统一定义。它只要求一个与底层xTokenizeC 函数(见 SQLite 官方文档)对应的分词方法:
typealias FTS5TokenCallback = @convention(c) ( _ context: UnsafeMutableRawPointer?, _ flags: Int32, _ pToken: UnsafePointer<Int8>?, _ nToken: Int32, _ iStart: Int32, _ iEnd: Int32) -> Int32 protocol FTS5Tokenizer : class { func tokenize( context: UnsafeMutableRawPointer?, tokenization: FTS5Tokenization, pText: UnsafePointer<Int8>?, nText: Int32, tokenCallback: FTS5TokenCallback?) -> Int32 }FTS5Tokenization是描述分词原因(tokenization reason)的 OptionSet,源码中对应 SQLite 的FTS5_TOKENIZE_*常量(见 GRDB/FTS/FTS5Tokenizer.swift):
.query(FTS5_TOKENIZE_QUERY):正在为搜索模式的MATCH分词;.prefix(FTS5_TOKENIZE_PREFIX):正在为前缀查询分词;.document(FTS5_TOKENIZE_DOCUMENT):正在为插入 FTS 表的文档分词;.aux(FTS5_TOKENIZE_AUX):辅助分词。
FTS5Tokenization可组合使用,例如tokenization.contains(.query)用来判断当前是否处于查询分词阶段——这正是同义词示例用来避免"双向都产生同义词"的判断依据。
可以通过Database.makeTokenizer()实例化 tokenizer,包括内建 tokenizer:
let unicode61 = try db.makeTokenizer(.unicode61()) // FTS5TokenizermakeTokenizer在底层通过xFindTokenizer查找已注册 tokenizer 并调用其xCreate构造(见 GRDB/FTS/FTS5Tokenizer.swift)。源码还为协议提供了两个便捷方法,方便在测试与调试中直接观察分词结果:
// tokenize(document:) 模拟文档插入时的分词 try tokenizer.tokenize(document: "foo bar") // [("foo", flags), ("bar", flags)] // tokenize(query:) 模拟 MATCH 搜索模式的分词 try tokenizer.tokenize(query: "foo bar") // [("foo", flags), ("bar", flags)]在测试 Tests/GRDBTests/FTS/FTS5CustomTokenizerTests.swift 中,SynonymsTokenizer.tokenize(document:)返回的 token 与 flags 数组精确验证了同义词展开与.colocated标记行为,是调试自定义 tokenizer 的有力工具。
FTS5TokenizerDescriptor:tokenizer 的配置描述
在深入自定义协议前,需要先认识与 FTS5 配套的FTS5TokenizerDescriptor——它描述了 tokenizer 的名字与参数,并被t.tokenizer、makeTokenizer共同使用(源码见 GRDB/FTS/FTS5TokenizerDescriptor.swift)。
init(components:):用原始组件数组构建描述符,例如["porter", "unicode61", "remove_diacritics", "0"];前提是组件数组不能为空。.ascii(separators:tokenCharacters:):构建 "ascii" 描述符,可自定义分隔符集合与 token 字符集合。.porter(wrapping:):构建 "porter" 描述符,可指定被包装的基础 tokenizer(默认 unicode61)。.unicode61(diacritics:categories:separators:tokenCharacters:):构建 "unicode61" 描述符。diacritics取FTS5.Diacritics枚举,默认.removeLegacy(对应remove_diacritics=1),.keep对应0,.remove对应2(remove_diacritics=2需要 SQLite 3.27.0+);categories默认为空串,此时 SQLite 采用 "L* N* Co" 的 Unicode 类别集合;separators与tokenCharacters默认都为空集。
描述符的components直接对应 SQLitetokenize=配置字符串的各个参数。例如unicode61(removeDiacritics: false)会生成["unicode61", "remove_diacritics", "0"]。
FTS5CustomTokenizer:底层自定义协议
FTS5CustomTokenizer是底层自定义协议,用于直接对接原始 FTS5 C API 的场景:
protocol FTS5CustomTokenizer : FTS5Tokenizer { static var name: String { get } init(db: Database, arguments: [String]) throws }自定义 tokenizer 与内建 tokenizer 一样拥有名字。不要使用 "ascii"、"porter"、"unicode61" 这些已被占用的名字,否则会与内建 tokenizer 冲突:
final class MyTokenizer : FTS5CustomTokenizer { static let name = "custom" }SQLite 在需要 token 时实例化 tokenizer。init(db:arguments:)的arguments参数是字符串数组,自定义 tokenizer 可以按自己的用途解释它。在下面的例子中,参数将是["arg1", "arg2"]:
// CREATE VIRTUAL TABLE documents USING fts5( // tokenize='custom arg1 arg2', // authors, title, body // ) try db.create(virtualTable: "documents", using: FTS5()) { t in t.tokenizer = MyTokenizer.tokenizerDescriptor(arguments: ["arg1", "arg2"]) t.column("authors") t.column("title") t.column("body") }FTS5CustomTokenizer继承自FTS5Tokenizer,通过tokenize(context:tokenization:pText:nText:tokenCallback:)执行分词。该底层方法对应 SQLite 官方文档的xTokenize函数,各参数含义如下:
context:不透明指针,作为tokenCallback的第一个参数传入;tokenization:FTS5 请求分词的原因;pText:待分词文本的字节指针,可能不以 NUL 结尾;nText:待分词文本的字节数;tokenCallback:为每个找到的 token 调用的回调函数,对应 SQLite 的xToken回调:context:不透明指针;flags:告诉 FTS5 如何登记该 token 的标记;pToken:token 的字节指针,可能不以 NUL 结尾;nToken:token 的字节数;iStart:token 在输入文本中的字节偏移;iEnd:token 在输入文本中的结束字节偏移。
由于直接处理原始字节缓冲区与@convention(c)回调相当繁琐,源码测试中给出了一个完整的底层实现范例:StopWordsTokenizer与NFKCTokenizer(见 Tests/GRDBTests/FTS/FTS5CustomTokenizerTests.swift)。它们都是"包装 unicode61 但拦截其 token"的模式:先通过withUnsafeMutablePointer把自定义上下文传给内层 tokenizer,在内层回调里把原始字节还原为String做处理,再调用原始tokenCallback反馈给 SQLite。可以看到,即使是"底层"协议,社区也习惯把它当作"半包装"来用——这正是 FTS5WrapperTokenizer 存在的原因。
FTS5WrapperTokenizer:高层包装协议
FTS5WrapperTokenizer是高层自定义协议,它为低层tokenize方法提供了默认实现,让采用者不必接触原始 FTS5 C API 的字节缓冲区。它把最难的分词工作交给另一个 tokenizer("被包装的 tokenizer"),自己只负责对被包装 tokenizer 产出的 token 做后处理:
protocol FTS5WrapperTokenizer : FTS5CustomTokenizer { var wrappedTokenizer: any FTS5Tokenizer { get } func accept( token: String, flags: FTS5TokenFlags, for tokenization: FTS5Tokenization, tokenCallback: FTS5WrapperTokenCallback) throws }与所有自定义 tokenizer 一样,包装型 tokenizer 也必须有自己的名字:
final class MyTokenizer : FTS5WrapperTokenizer { static let name = "custom" }wrappedTokenizer属性是必须实现的被包装 tokenizer,应在初始化器中只实例化一次:
final class MyTokenizer : FTS5WrapperTokenizer { let wrappedTokenizer: any FTS5Tokenizer init(db: Database, arguments: [String]) throws { // Wrap the unicode61 tokenizer wrappedTokenizer = try db.makeTokenizer(.unicode61()) } }包装型 tokenizer 需要实现accept(token:flags:for:tokenCallback:)方法。例如,一个简单透传的 tokenizer 如下:
final class MyTokenizer : FTS5WrapperTokenizer { func accept( token: String, flags: FTS5TokenFlags, for tokenization: FTS5Tokenization, tokenCallback: FTS5WrapperTokenCallback) throws { // pass through try tokenCallback(token, flags) } }各参数含义:
token:被包装 tokenizer 产出的 token,可以原样忽略、改写,或倍增为多个同义词;tokenization:说明 FTS5 当前是对文档还是对搜索模式分词,某些 tokenizer 会依据该参数产出不同的 token;tokenCallback:输出自定义 token 时调用的回调函数。
实现accept方法时有两条必须遵守的规则:
tokenCallback抛出的错误不能被捕获——它们是在通知 FTS5 立即终止分词过程;flags参数应原样传给tokenCallback,除非在产出同义词时与.colocated标记做并集(union)。
这两条规则同时写入了源码协议注释(见 GRDB/FTS/FTS5WrapperTokenizer.swift)。FTS5TokenFlags目前公开了.colocated一个成员,对应 SQLite 的FTS5_TOKEN_COLOCATED(见 GRDB/FTS/FTS5WrapperTokenizer.swift)。
从源码实现看,FTS5WrapperTokenizer的默认tokenize会把自定义上下文打包进FTS5WrapperContext,用withUnsafeMutablePointer转发给被包装 tokenizer 的tokenize;在内层回调中把 token 字节还原为String后调用accept,再把accept产出的每个自定义 token 重新编码为字节并通过tokenCallback注入 SQLite(见 GRDB/FTS/FTS5WrapperTokenizer.swift)。整个过程中iStart、iEnd偏移保持不变,因此同义词在短语查询(phrase query)中也能保持相对位置正确。
动态选择被包装的 Tokenizer
被包装的 tokenizer 既可以硬编码,也可以在运行时根据参数选择。例如下面的 tokenizer 默认包装 unicode61,但允许通过参数指定其他 tokenizer(与 porter 的包装机制类似):
final class MyTokenizer : FTS5WrapperTokenizer { static let name = "custom" let wrappedTokenizer: any FTS5Tokenizer init(db: Database, arguments: [String]) throws { if arguments.isEmpty { wrappedTokenizer = try db.makeTokenizer(.unicode61()) } else { let descriptor = FTS5TokenizerDescriptor(components: arguments) wrappedTokenizer = try db.makeTokenizer(descriptor) } } }参数在虚拟表创建时提供:
// CREATE VIRTUAL TABLE documents USING fts5( // tokenize='custom', // content // ) try db.create(virtualTable: "documents", using: FTS5()) { t in // Wraps the default unicode61 t.tokenizer = MyTokenizer.tokenizerDescriptor() t.column("content") } // CREATE VIRTUAL TABLE documents USING fts5( // tokenize='custom ascii' // content // ) try db.create(virtualTable: "documents", using: FTS5()) { t in // Wraps ascii let ascii = FTS5TokenizerDescriptor.ascii() t.tokenizer = MyTokenizer.tokenizerDescriptor(arguments: ascii.components) t.column("content") }这种"参数驱动包装目标"的模式在测试中同样被使用:CustomizedUnicode61WrappingTokenizer包装了自定义的 unicode61 配置(含自定义分隔符 "X"),并用透传回调验证了abcXdef能被切为 "abc" 与 "def" 两个 token(见 Tests/GRDBTests/FTS/FTS5WrapperTokenizerTests.swift)。
示例:同义词——让 "first" 匹配 "1st"
FTS5 允许 tokenizer 产生同义词,从而让 "first" 匹配 "1st"。同义词的完整机制在 SQLite 官方文档 中有详细介绍,包含多种实现方法,请仔细阅读后选择适合你的那种。下面采用方法 (3):单个词条为 FTS 索引提供多个同义词。文档 "I won first place" 被分词后,索引中会同时出现 "i"、"won"、"first"、"1st"、"place" 这些词条。
同时需要遵从 SQLite 的官方建议:
使用方法 (2) 或 (3) 时,tokenizer 只能在对文档文本或查询文本分词时提供同义词,不能同时对两者提供。虽然不会产生错误,但会降低效率。
下面的实现(与 Tests/GRDBTests/FTS/FTS5WrapperTokenizerTests.swift 中的SynonymsTokenizer一致)只有在.document分词时才展开同义词,查询分词直接透传:
final class SynonymsTokenizer : FTS5WrapperTokenizer { static let name = "synonyms" let wrappedTokenizer: any FTS5Tokenizer let synonyms: [Set<String>] = [["first", "1st"]] init(db: Database, arguments: [String]) throws { wrappedTokenizer = try db.makeTokenizer(.unicode61()) } func synonyms(for token: String) -> Set<String>? { synonyms.first { $0.contains(token) } } func accept(token: String, flags: FTS5TokenFlags, for tokenization: FTS5Tokenization, tokenCallback: FTS5WrapperTokenCallback) throws { if tokenization.contains(.query) { // Don't look for synonyms when tokenizing queries try tokenCallback(token, flags) return } guard let synonyms = synonyms(for: token) else { // Token has no synonym try tokenCallback(token, flags) return } for (index, synonym) in synonyms.enumerated() { // Notify each synonym, and set the colocated flag for all but the first let synonymFlags = (index == 0) ? flags : flags.union(.colocated) try tokenCallback(synonym, synonymFlags) } } }要点解读:
- 只在文档分词时展开同义词:
tokenization.contains(.query)分支直接透传原始 token,避免同义词同时注入索引与查询造成冗余开销; .colocated标记:除第一个同义词外,其余同义词都必须带上.colocated(FTS5_TOKEN_COLOCATED),表示它们与被替换 token 位于同一位置(colocated),这样才能保证短语查询与命中高亮的正确性。
测试 Tests/GRDBTests/FTS/FTS5WrapperTokenizerTests.swift 验证了完整行为:插入 "first foo" 与 "1st bar" 两篇文档后,MATCH "first"与MATCH "1st"都命中 2 篇;短语查询"first foo"、"1st foo"、"first bar"、"1st bar"均命中 1 篇;前缀查询fi*与1s*也都命中 2 篇。同时,tokenize(document:)输出的 flags 序列验证了首个 token 无标记、后续同义词带.colocated的精确行为(见 Tests/GRDBTests/FTS/FTS5CustomTokenizerTests.swift)。
示例:拉丁字符容错——让 "Grossmann" 匹配 "Großmann"
使用拉丁字母的语言拥有丰富的排版、历史与地域特征:变音符(diacritics)、连字(ligatures)、无点 i(dotless I)等,例如 Großmann、fidélité(含连字 "fi" U+FB01)、Diyarbakır。这类语料做全文检索时,通常需要输入容错,让 "encyclopaedia" 能匹配 "Encyclopædia","Grossmann" 能匹配 "Großmann","Jerome" 能匹配 "Jérôme"。
德语还有一个特殊需求:"Mueller" 与 "Muller" 都应匹配 "Müller",但 "Bauer" 不应匹配 "Baur"(因为只有 "ü" 同时接受 "u" 与 "ue" 两种写法)。官方文档表示欢迎贡献一个专门讲解德语场景的章节。
自定义 FTS5 tokenizer 可以提供模糊拉丁匹配:当 "Grossmann"、"Großmann"、"GROSSMANN" 都被转换为 "grossmann" 后,三者即可互相匹配。
实现策略是:包装内建 unicode61(它擅长按空格与标点切分文本),并把每个 token 变换为小写、纯 ASCII 的形式。包装机制由FTS5WrapperTokenizer提供,字符串变换则由 Foundation 的 String.applyingTransform 提供:
final class LatinAsciiTokenizer : FTS5WrapperTokenizer { static let name = "latinascii" let wrappedTokenizer: any FTS5Tokenizer init(db: Database, arguments: [String]) throws { wrappedTokenizer = try db.makeTokenizer(.unicode61()) } func accept(token: String, flags: FTS5TokenFlags, for tokenization: FTS5Tokenization, tokenCallback: FTS5WrapperTokenCallback) throws { if let token = token.applyingTransform(StringTransform("Latin-ASCII; Lower"), reverse: false) { try tokenCallback(token, flags) } } }StringTransform("Latin-ASCII; Lower")的变换链路会把拉丁字符转写为最接近的 ASCII 字符("ß"→"ss"、"æ"→"ae"、带音字母去音)并转为小写;若变换结果不存在,则跳过该 token。使用前记得注册:
dbQueue.add(tokenizer: LatinAsciiTokenizer.self) // or dbPool.add dbQueue.inDatabase { db in try db.create(virtualTable: "documents", using: FTS5()) { t in t.tokenizer = LatinAsciiTokenizer.tokenizerDescriptor() t.column("authors") t.column("title") t.column("body") } }测试 Tests/GRDBTests/FTS/FTS5WrapperTokenizerTests.swift 给出了对照实验:同一份含 "aimé fidélité Encyclopædia Großmann Diyarbakır" 的文档,使用内建 unicode61 时查询 "aime fidelite encyclopaedia grossmann diyarbakir" 命中 0 篇;改用LatinAsciiTokenizer后命中 1 篇。该测试还同时验证了组合变音符(aime\u{0301}分解形式)也能与预组合形式(aimé)互相匹配。类似地,测试 Tests/GRDBTests/FTS/FTS5CustomTokenizerTests.swift 中的NFKCTokenizer通过precomposedStringWithCompatibilityMapping(NFKC 归一化)让 "aimefi" 匹配 "aiméfi"(U+FB01 连字),展示了同一模式在不同 Unicode 归一化策略下的应用。
实战要点与注意事项
1. 注册时机与作用域。自定义 tokenizer 必须在使用它的连接上注册。DatabaseQueue单连接场景可直接在prepareDatabase中注册;DatabasePool多连接场景必须用dbPool.add(tokenizer:)或prepareDatabase,保证每个连接都能找到 tokenizer,否则建表会失败。测试 Tests/GRDBTests/FTS/FTS5WrapperTokenizerTests.swift 展示了DatabasePool下的正确写法。
2. 名字唯一性。static let name是 tokenizer 的全局标识,不能与内建名(ascii、porter、unicode61)或其他已注册自定义名冲突。
3. 分词一致性。索引分词与查询分词必须使用同一个tokenizer 描述符,否则索引 token 与查询 token 无法对齐、必然失配。对同一张表,改变tokenizer描述符需要重建虚拟表。
4. 高效同义词策略。遵循 SQLite 官方建议:只在文档分词或查询分词之一产生同义词,不要两者都产生;同义词必须正确携带.colocated标记,以保证短语查询正确。
5. 测试驱动开发。仓库测试提供了三层验证手段:直接用 SQL 的MATCH断言命中数(端到端)、用tokenize(document:)/tokenize(query:)断言 token 序列与 flags(单元级)、用 DatabaseQueue 与 DatabasePool 双路径验证注册可靠性。自定义 tokenizer 上线前,建议仿照 Tests/GRDBTests/FTS/FTS5WrapperTokenizerTests.swift 建立同等覆盖。
6. 性能与底层开销。每个被包装 token 都会经历"字节→String→accept→字节"的往返(见 GRDB/FTS/FTS5WrapperTokenizer.swift),高吞吐场景下应避免在accept中做重量级计算;若确需极致性能,可退回FTS5CustomTokenizer直接操作字节缓冲。这一取舍在官方文档中同样被提及:"分词很困难,字节缓冲指针也不易处理"——这正是推荐优先使用FTS5WrapperTokenizer的原因。
【免费下载链接】GRDB.swiftA toolkit for SQLite databases, with a focus on application development项目地址: https://gitcode.com/GitHub_Trending/gr/GRDB.swift
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考