第02章:搜索引擎核心原理:Tokenization的艺术
搜索质量取决于分词策略,这是搜索引擎的DNA
📝 TL;DR (核心要点速览)
– 核心概念:Tokenization是将文本转换为可搜索单元的艺术
– 四种策略:Word、Prefix、N-Grams、Singular Tokenizer
– 技术实现:接口设计 + 多策略组合 + 权重分配
– 性能平衡:搜索准确性与资源开销的权衡
1. Tokenization的本质理解
1.1 从文本到搜索单元
用户输入:"advanced database search techniques"
搜索引擎需要理解:
完整匹配:advanced database search techniques
部分匹配:advanced, database, search, techniques
前缀匹配:adv, data, search, tech
容错匹配:advancd (拼写错误), data base (空格错误)
单复数处理:technique, techniques
Tokenization的核心任务:将连续文本转换为离散的、可比较的搜索单元。
1.2 为什么Tokenization如此重要
决定召回率:
– 好的分词:用户输入”search”,能找到”searching”, “searched”
– 差的分词:用户输入”search”,只能找到完全匹配的”search”
决定精确率:
– 好的分词:不会将无关内容误判为相关
– 差的分词:大量不相关的结果被返回
决定用户体验:
– 自动补全:输入”dat”时显示”database”
– 拼写容错:输入”serch”时找到”search”
2. 四种核心Tokenizer策略详解
2.1 Word Tokenizer:精确匹配的基石
设计思想:按单词边界精确切分文本
实现原理:
class WordTokenizer implements TokenizerInterface
{
public function tokenize(string $text): array
{
// 1. 转换为小写,统一匹配标准
$text = strtolower($text);
// 2. 移除标点符号,保留字母数字和空格
$text = preg_replace('/[^a-z0-9\s]/', '', $text);
// 3. 按空格分割成单词
$words = explode(' ', $text);
// 4. 过滤空字符串和过短词
return array_filter($words, function($word) {
return strlen($word) >= 2; // 最少2个字符
});
}
public function getWeight(): int
{
return 10; // 最高权重,完全匹配
}
}
示例转换:
输入:"Advanced Database Search Techniques!"
输出:["advanced", "database", "search", "techniques"]
权重:10 (精确匹配)
适用场景:
– 需要精确匹配的关键词
– 专业术语搜索
– 标签、分类等结构化数据
2.2 Prefix Tokenizer:自动补全的奥秘
设计思想:生成单词的所有可能前缀,支持前缀匹配
实现原理:
class PrefixTokenizer implements TokenizerInterface
{
private int $minPrefixLength;
public function __construct(int $minPrefixLength = 3)
{
$this->minPrefixLength = $minPrefixLength;
}
public function tokenize(string $text): array
{
$tokens = [];
$words = $this->getWords($text); // 复用WordTokenizer的逻辑
foreach ($words as $word) {
// 生成从minPrefixLength到单词长度的所有前缀
for ($i = $this->minPrefixLength; $i <= strlen($word); $i++) {
$tokens[] = substr($word, 0, $i);
}
}
return array_unique($tokens); // 去重
}
public function getWeight(): int
{
return 2; // 较低权重,前缀匹配
}
}
示例转换:
输入:"Database Search"
输出:["dat", "data", "datab", "databa", "databas", "database", "sea", "sear", "searc", "search"]
权重:2 (前缀匹配)
适用场景:
– 自动补全功能
– 用户输入不完整时的搜索
– 移动端输入优化
2.3 N-Grams Tokenizer:容错匹配的原理
设计思想:生成固定长度的字符序列,支持部分匹配和容错
实现原理:
class NGramsTokenizer implements TokenizerInterface
{
private int $gramSize;
public function __construct(int $gramSize = 3)
{
$this->gramSize = $gramSize;
}
public function tokenize(string $text): array
{
$tokens = [];
$text = strtolower($this->cleanText($text));
// 生成滑动窗口的n-grams
for ($i = 0; $i <= strlen($text) - $this->gramSize; $i++) {
$gram = substr($text, $i, $this->gramSize);
if (strlen(trim($gram)) === $this->gramSize) {
$tokens[] = $gram;
}
}
return array_unique($tokens);
}
public function getWeight(): int
{
return 1; // 最低权重,容错匹配
}
}
示例转换:
输入:"Search"
3-grams输出:["sea", "ear", "arc", "rch"]
权重:1 (容错匹配)
适用场景:
– 拼写错误容忍
– OCR识别错误处理
– 模糊搜索需求
2.4 Singular Tokenizer:单复数处理技巧
设计思想:统一单词的单复数形式,提高匹配召回率
实现原理:
class SingularTokenizer implements TokenizerInterface
{
private array $irregularPlurals = [
'person' => 'people',
'child' => 'children',
'man' => 'men',
'woman' => 'women',
// ... 更多不规则变化
];
public function tokenize(string $text): array
{
$words = $this->getWords($text);
$tokens = [];
foreach ($words as $word) {
$tokens[] = $word; // 保留原词
$tokens[] = $this->toSingular($word); // 添加单数形式
}
return array_unique($tokens);
}
private function toSingular(string $word): string
{
// 处理不规则复数
foreach ($this->irregularPlurals as $singular => $plural) {
if ($word === $plural) {
return $singular;
}
}
// 处理规则复数
if (substr($word, -3) === 'ies') {
return substr($word, 0, -3) . 'y'; // babies -> baby
}
if (substr($word, -1) === 's') {
return substr($word, 0, -1); // dogs -> dog
}
return $word;
}
public function getWeight(): int
{
return 8; // 高权重,但略低于完全匹配
}
}
示例转换:
输入:"Database Systems"
输出:["database", "databases", "system", "systems", "databas", "system"] // 包含单复数形式
权重:8 (单复数匹配)
3. Tokenizer系统架构设计
3.1 接口设计:统一的Tokenization标准
interface TokenizerInterface
{
/**
* 将文本转换为token数组
*/
public function tokenize(string $text): array;
/**
* 获取tokenizer的权重
* 权重用于搜索结果排序计算
*/
public function getWeight(): int;
/**
* 获取tokenizer名称,用于调试和日志
*/
public function getName(): string;
}
设计原则:
– 单一职责:每个Tokenizer只负责一种分词策略
– 可组合性:多个Tokenizer可以组合使用
– 权重系统:不同策略有不同的匹配权重
– 扩展性:新的策略可以轻松添加
3.2 Tokenizer工厂:策略的统一管理
class TokenizerFactory
{
private array $tokenizers = [];
public function __construct()
{
// 注册所有可用的tokenizer
$this->tokenizers = [
'word' => new WordTokenizer(),
'prefix' => new PrefixTokenizer(3),
'ngrams' => new NGramsTokenizer(3),
'singular' => new SingularTokenizer(),
];
}
public function get(string $name): TokenizerInterface
{
if (!isset($this->tokenizers[$name])) {
throw new InvalidArgumentException("Unknown tokenizer: {$name}");
}
return $this->tokenizers[$name];
}
public function getAll(): array
{
return $this->tokenizers;
}
public function createWithWeights(array $config): array
{
$result = [];
foreach ($config as $name => $weight) {
$tokenizer = $this->get($name);
// 可以动态调整权重
$tokenizer->setWeight($weight);
$result[$name] = $tokenizer;
}
return $result;
}
}
3.3 组合Tokenizer:多策略协同工作
class CompositeTokenizer implements TokenizerInterface
{
private array $tokenizers;
public function __construct(array $tokenizerConfig)
{
$factory = new TokenizerFactory();
$this->tokenizers = [];
foreach ($tokenizerConfig as $name => $enabled) {
if ($enabled) {
$this->tokenizers[$name] = $factory->get($name);
}
}
}
public function tokenize(string $text): array
{
$allTokens = [];
foreach ($this->tokenizers as $name => $tokenizer) {
$tokens = $tokenizer->tokenize($text);
// 为每个token添加来源信息
foreach ($tokens as $token) {
$allTokens[] = [
'token' => $token,
'source' => $name,
'weight' => $tokenizer->getWeight()
];
}
}
return $allTokens;
}
public function getWeight(): int
{
// 组合tokenizer的权重是所有权重的平均值
$totalWeight = array_sum(array_map(fn($t) => $t->getWeight(), $this->tokenizers));
return (int)($totalWeight / count($this->tokenizers));
}
}
4. 性能优化与内存管理
4.1 内存使用分析
Token数量增长规律:
文本长度:100个字符
- Word Tokenizer:约15个token
- Prefix Tokenizer:约60个token
- N-Grams Tokenizer:约98个token
- 组合使用:约200个token
内存消耗:每个token约50字节(字符串+元数据)
- 单个文本:约10KB
- 10万篇文档:约1GB
优化策略:
class OptimizedTokenizer
{
private int $maxTokensPerDocument = 1000;
private int $maxTokenLength = 50;
public function tokenize(string $text): array
{
$tokens = $this->basicTokenize($text);
// 1. 限制token数量,防止内存爆炸
if (count($tokens) > $this->maxTokensPerDocument) {
$tokens = array_slice($tokens, 0, $this->maxTokensPerDocument);
}
// 2. 过滤过长的token
$tokens = array_filter($tokens, function($token) {
return strlen($token) <= $this->maxTokenLength;
});
// 3. 按频率排序,保留重要token
$tokens = $this->sortByFrequency($tokens);
return array_values($tokens);
}
}
4.2 CPU性能优化
优化技术:
class HighPerformanceTokenizer
{
// 使用静态缓存避免重复计算
private static array $cache = [];
// 使用更快的字符串处理函数
public function fastTokenize(string $text): array
{
$cacheKey = md5($text);
if (isset(self::$cache[$cacheKey])) {
return self::$cache[$cacheKey];
}
// 使用str_函数代替preg_函数,性能提升30%
$text = strtolower($text);
$text = str_replace(['.', ',', '!', '?'], ' ', $text);
$tokens = array_filter(explode(' ', $text));
self::$cache[$cacheKey] = $tokens;
return $tokens;
}
}
5. 实际应用:配置最佳实践
5.1 不同场景的Tokenizer配置
电商网站搜索:
$ecommerceConfig = [
'word' => true, // 商品标题精确匹配
'prefix' => true, // 支持自动补全
'ngrams' => false, // 不需要容错,影响精确度
'singular' => true // 单复数匹配很重要
];
内容管理系统:
$cmsConfig = [
'word' => true, // 内容关键词匹配
'prefix' => false, // 不需要自动补全
'ngrams' => true, // 需要容错,用户可能打错
'singular' => true // 文章搜索需要单复数
];
用户搜索:
$userSearchConfig = [
'word' => true, // 姓名精确匹配
'prefix' => true, // 姓名自动补全
'ngrams' => false, // 姓名不需要容错
'singular' => false // 姓名单复数不重要
];
5.2 权重调优策略
权重分配原则:
class WeightOptimizer
{
public function optimizeWeights(array $searchData): array
{
// 基于实际搜索数据调整权重
$weights = [
'word' => 10, // 精确匹配最重要
'singular' => 8, // 单复数匹配次重要
'prefix' => 3, // 前缀匹配一般重要
'ngrams' => 1 // 容错匹配最不重要
];
// 根据用户行为数据调整
if ($searchData['prefix_usage_rate'] > 0.3) {
$weights['prefix'] = 5; // 前缀使用频繁,提高权重
}
return $weights;
}
}
6. 本章总结
6.1 核心收获
技术层面:
– 掌握四种核心Tokenizer的实现原理
– 理解不同策略的适用场景和权衡
– 学会设计可扩展的Tokenization系统
– 掌握性能优化的关键技术
设计思维:
– 搜索质量取决于分词策略的选择
– 没有万能的Tokenizer,需要根据场景组合使用
– 权重系统是搜索准确性的关键
– 性能和准确性需要平衡
6.2 下章预告
下一章我们将深入权重系统设计,学习如何:
- 实现三层权重架构(field×tokenizer×length)
- 设计科学的评分算法
- 用SQL实现复杂的权重计算
- 优化搜索结果的排序质量
实践作业:为你当前项目的搜索需求设计合适的Tokenizer配置,并测试不同组合的效果。
上一篇 → 第01章:为什么需要自建搜索引擎 | 下一篇 → 第03章:权重系统设计





最新评论
照片令人惊艳。万分感谢 温暖。
氛围绝佳。由衷感谢 感受。 你的博客让人一口气读完。敬意 真诚。
实用的 杂志! 越来越好!
又到年底了,真快!
研究你的文章, 我体会到美好的心情。
感谢激励。由衷感谢
好久没见过, 如此温暖又有信息量的博客。敬意。
很稀有, 这么鲜明的文字。谢谢。