以下是 From Text to Token: How Tokenization Pipelines Work | ParadeDB 的翻译
从文本到词元:分词(Tokenization)流水线是如何工作的
当你在搜索框中输入一句话时,很容易以为搜索引擎看到的内容和你看到的一样。但事实并非如此。
搜索引擎(或搜索数据库)并不存储原始文本块,也不存储完整的句子,甚至不以我们通常理解的方式存储“单词”。它们会将输入的文本(无论是索引内容还是查询语句)拆解、清洗,再重新组装成一种更抽象却更实用的形式——词元(tokens)。这些词元才是你真正用于搜索的内容,也是倒排索引中实际存储的单元。
让我们放慢节奏,逐步观察这一分词流水线的运作过程,并在每个环节暂停,看看语言是如何被拆解又重构的,以及这对搜索结果有何影响。
我们将使用一句稍作修改的经典英文句子 “The full-text database jumped over the lazy café dog” 作为演示案例。这句话包含了所有使分词变得有趣的元素:大小写、标点符号、重音符号(é)、以及在分词过程中形态发生变化的单词。经过完整处理后,它看起来会完全不同,却已为高效搜索做好了准备。
注:本文展示的并非一个完整的分词流水线,而是聚焦于词法搜索系统中最常见的几种过滤器(filters)。无论是 Lucene/Elasticsearch、Tantivy/ParadeDB,还是 PostgreSQL 的全文搜索,其核心思想都是相通的。不同系统通常将这些过滤器设计为可组合、可启用/禁用、可重新排序的构建模块,以便你根据实际需求灵活配置。
一、字符归一化:大小写折叠与重音符号处理
在进行分词之前,首先需要对文本中的字符做清洗和标准化,剔除无用信息。
最常见的是两类操作:
- 转为小写(Lowercasing):例如 “Database” → “database”
- 重音折叠(Diacritic Folding):例如 “café” → “cafe”,“résumé” → “resume”
这一步确保了在分词前,文本字符已处于一致状态。这样一来,无论用户是否输入了重音符号,都能正确匹配。比如搜索 “cafe” 也能命中 “café”。
大小写折叠虽然可能导致一些歧义(比如人名 “Olive” 和食物 “olive” 被视为相同),但大多数系统接受这种权衡:宁可多匹配(假阳性),也不要漏匹配(假阴性)。
例外情况:代码搜索通常需要保留符号和大小写(如 camelCase 或 PascalCase),因此不会进行这类归一化。
在我们的示例中,“The” 的大写 T 被转为小写,“café” 中的 “é” 被折叠为 “e”。
二、分词:将文本切分为可搜索单元
经过清洗的文本接下来会被切分为可索引的最小单元——词元(tokens)。此时,句子不再被视为整体,而是被拆解为一系列独立、可搜索的片段。
常见的分词器类型有三类:
-
面向单词的分词器(Word-oriented Tokenizers)
按词边界切割,适用于大多数以单词为单位的搜索场景。例如:空格分词器(whitespace tokenizer),或能识别中文、日文等非英文字符的语言感知分词器(如 Jieba、Lindera)。 -
子词分词器(Partial Word Tokenizers)
将单词进一步拆分为子串,常用于模糊匹配或自动补全。例如:- n-gram:生成重叠的字符序列(如 “jump” → “jum”, “ump”)
- 前缀/后缀 n-gram(Edge n-gram):仅保留开头或结尾(如 “jump” → “j”, “ju”, “jum”)
-
结构化文本分词器(Structured Text Tokenizers)
专为 URL、邮箱、文件路径等结构化数据设计,保留有意义的分隔符(如 “/” 或 “@”),避免通用分词器将其错误拆分。
举例:Lucene 默认会将 “it’s” 保留为
[it's],而 Tantivy 则拆成[it, s]。哪种更好?取决于需求——前者更符合语义,后者可能无法匹配单独搜索 “it” 的查询。
在我们的例子中,使用的是基础的空格+标点分词器,但你也可以尝试切换为 trigram(3-gram)分词器,观察输出的巨大差异。
三、停用词过滤(Stopword Removal)
某些词(如 “the”, “and”, “of”, “are”)出现频率极高,却几乎不携带语义信息,被称为停用词(stopwords)。搜索系统通常会直接丢弃它们,以提升信噪比。
风险提示:并非所有场景都适用。例如乐队名 “The Who” 中的 “the” 就至关重要。因此,停用词列表通常是可配置的。
在支持 BM25 排序算法的系统(如 Elasticsearch)中,停用词可能不会被显式移除,因为 BM25 本身会对高频词自动降权。但在不支持 BM25 的系统(如 PostgreSQL 的 tsvector)中,停用词过滤就变得尤为关键。
在我们的示例中,移除 “the” 和 “over” 后,词元数量从 10 个减少到 8 个,语义更聚焦。
技术细节:即使删除了停用词,系统仍会保留原始词的位置信息,以便支持“位置查询”(如 “cat” 出现在 “dog” 前后 5 个词内)。
四、词干提取(Stemming)
人类能轻松识别 “jump”、“jumps”、“jumped”、“jumping” 是同一词根的不同形式,但计算机不能——除非我们提供规则。
词干提取(Stemming) 是一种基于规则的简化方法,将单词缩减为其“词干”。最经典的是 Porter 词干算法(1980) 及其现代变种 Snowball。
效果可能看起来有点奇怪:
- “database” → “databas”
- “lazy” → “lazi”
- “jumped” → “jump”
但没关系——词干提取不在乎“是否像真实单词”,只在乎“是否一致”。只要所有变体都归约为同一个词干,就能实现跨形态匹配。
补充:还有一种更精确但计算成本更高的方法叫 词形还原(Lemmatization),它依赖词性标注和词典,能还原出真正的词典形式(如 “jumped” → “jump”)。但对大多数搜索场景来说,词干提取的“够用就好”策略更高效。
五、最终词元
经过上述四步处理,原始句子:
“The full-text database jumped over the lazy café dog”
已转化为以下 8 个干净、统一、可用于搜索的词元:
full
text
databas
jump
lazi
cafe
dog
这个过程不仅用于索引文档,也用于处理用户查询。
例如,当用户搜索 “databases are jumping” 时,系统会同样执行:
- 小写化 → “databases are jumping”
- 分词 → [databases, are, jumping]
- 停用词过滤 → [databases, jumping]
- 词干提取 → [databas, jump]
最终,查询词元 databas 和 jump 将完美匹配我们之前索引的内容。
为什么分词如此重要?
分词或许不够炫酷——没人会在技术大会上吹嘘自己的停用词过滤器。但它是搜索系统的静默引擎。
没有它:
- 搜 “dogs” 就找不到 “dog”
- 搜 “jumping” 就匹配不到 “jumped”
所有上层功能——相关性评分、排序、语义理解——都建立在正确分词的基础之上。它不 glamorous(华丽),但极其 precise(精准)。一旦分词做对了,整个搜索系统的体验就会大幅提升。
想亲自体验现代搜索数据库如何自动化处理分词?欢迎试用 ParadeDB。
脚注
- Lucene 保留
it's更符合语义,但无法匹配单独的it;Tantivy 拆成it和s则更灵活,但s可能是无用噪音。 - 中文、日文、韩文等语言通常依赖 Jieba、Lindera 等专用分词库。
- 删除停用词时仍保留原始位置,以支持位置相关查询。
- Lucene 和 Tantivy 默认关闭停用词,启用后使用相同的英文默认列表(含 29 个常见词)。
- 向量搜索(Vector Search)是另一种思路:用语义相似性替代词形匹配。
- 词干提取可能“过度”(overstemming),例如 “university” 和 “universe” 都被归为 “univers”,但语义不同。
- 词形还原需知道词性(名词/动词等),因此更复杂、更慢。 c