专注于分布式系统架构AI辅助开发工具(Claude
Code中文周刊)

第02章:搜索引擎核心原理:Tokenization的艺术

智谱 GLM,支持多语言、多任务推理。从写作到代码生成,从搜索到知识问答,AI 生产力的中国解法。

第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章:权重系统设计

赞(0)
未经允许不得转载:Toy Tech Blog » 第02章:搜索引擎核心原理:Tokenization的艺术
免费、开放、可编程的智能路由方案,让你的服务随时随地在线。

评论 抢沙发

十年稳如初 — LocVPS,用时间证明实力

10+ 年老牌云主机服务商,全球机房覆盖,性能稳定、价格厚道。

老品牌,更懂稳定的价值你的第一台云服务器,从 LocVPS 开始