PHP分词搜索实战指南:从零构建高性能中文检索引擎
目录导读
- 为什么中文搜索不能直接使用LIKE?
- PHP中文分词的核心原理(词典/隐马尔可夫/混合策略)
- 四种主流分词方案对比:SCWS / Phpanalysis / Jieba-PHP / 哈工大LTP
- 手写一个轻量级正向最大匹配分词器(附完整代码)
- 从分词到搜索:构建倒排索引的完整流程
- 重构搜索查询:BM25相关性排序算法实现
- 性能优化:缓存、内存索引与Sphinx/Elasticsearch整合
- 常见问题FAQ:编码、歧义、扩展词库与实时性
开始

为什么中文搜索不能直接使用LIKE?
当用户搜索“手机壳”时,如果数据库执行WHERE name LIKE '%手机%',MySQL会全表扫描并忽略索引(除非使用全文索引),而中文语句没有天然空格分隔,导致“上海自来水来自海上”这类句子无法切分,更关键的是,MySQL内置全文索引仅支持英文空格分词,对中文支持极差(需ngram插件且准确率低),PHP项目必须引入独立分词层。
PHP中文分词的核心原理
中文分词本质是序列标注问题,主流实现策略分三类:
- 基于词典(字符串匹配):正向最大匹配、逆向最大匹配、双向匹配,速度快,但依赖词库规模。
- 基于统计(隐马尔可夫HMM):将每个字标记为词首(B)、词中(M)、词尾(E)、单字(S),通过维特比算法求解最大概率路径,能识别新词但需训练语料。
- 混合策略:先用词典切分,对未登录词回退到HMM(如Jieba的做法)。
一个合格的分词器必须解决三个痛点:歧义(如“研究生命科学”应切为“研究/生命/科学”)、新词(如“喜茶”不在词典中)、粒度控制(做搜索时需要粗粒度,做标签需要细粒度)。
四种主流分词方案对比
| 方案 | 类型 | 分词速度 | 准确率 | 扩展性 | PHP集成难度 |
|---|---|---|---|---|---|
| SCWS(简易中文分词系统) | C扩展 | 极高(约5MB/s) | 85% | 自定义词库 | 需编译安装,PHP官方推荐 |
| Phpanalysis | PHP纯代码 | 较慢(约0.2MB/s) | 78% | 可加载自定义字典 | 零依赖,易修改 |
| Jieba-PHP | PHP移植 | 中等 | 90%+ | 支持HMM新词发现 | 需Composer,词库较大 |
| 哈工大LTP(PHP封装) | HTTP服务 | 依赖网络 | 95%+ | 领域模型切换 | 需独立部署服务 |
选型建议:追求极致性能且服务器可装扩展→SCWS;要求部署简单且容忍稍慢→Phpanalysis;需要高准确率且允许外呼→LTP,个人项目建议直接使用Jieba-PHP,因其实践验证最充分。
手写轻量级正向最大匹配分词器
以下实现字典加载与正向最大匹配(最大词长5字):
class SimpleSegment {
private $dict = [];
private $maxLen = 5;
public function __construct($dictFile) {
$this->dict = array_flip(file($dictFile, FILE_IGNORE_NEW_LINES));
}
public function cut($text) {
$result = [];
$len = mb_strlen($text, 'UTF-8');
$pos = 0;
while ($pos < $len) {
$matched = false;
for ($l = $this->maxLen; $l > 1; $l--) {
$word = mb_substr($text, $pos, $l, 'UTF-8');
if (isset($this->dict[$word])) {
$result[] = $word;
$pos += $l;
$matched = true;
break;
}
}
if (!$matched) {
$result[] = mb_substr($text, $pos, 1, 'UTF-8');
$pos++;
}
}
return $result;
}
}
使用方法:调用cut()返回数组,并过滤停用词(如“的、了、吗”)。
从分词到搜索:构建倒排索引的完整流程
假设有文章表posts(id, title, content),创建索引post_keywords:
CREATE TABLE post_keywords (
keyword VARCHAR(50) NOT NULL,
post_id INT NOT NULL,
weight TINYINT DEFAULT 1,
INDEX idx_keyword (keyword)
) ENGINE=InnoDB;
插入索引逻辑:
$keywordList = $segment->cut($title . ' ' . $content);
$keywordCount = array_count_values($keywordList);
foreach ($keywordCount as $kw => $count) {
// 标题权重2,内容权重1
$weight = (strpos($title, $kw) !== false) ? 2 : 1;
$insert[] = "('$kw', $postId, $weight)";
}
// 批量插入,避免逐条预处理
查询时:首先对用户输入分词,然后SELECT post_id, SUM(weight) as total FROM post_keywords WHERE keyword IN (?,?,?) GROUP BY post_id ORDER BY total DESC。
核心算法:BM25相关性排序
纯SQL的SUM(weight)过于简单,工业界通用BM25公式(k1=1.5, b=0.75):
function bm25($termFreq, $docFreq, $docLen, $avgDocLen, $totalDocs, $queryTermCount) {
$idf = log(1 + ($totalDocs - $docFreq + 0.5) / ($docFreq + 0.5));
$tf = ($termFreq * (1.5 + 1)) / ($termFreq + 1.5 * (1 - 0.75 + 0.75 * $docLen / $avgDocLen));
return $idf * $tf;
}
需在查询时联表获取post_id的docLen(存储每篇文章长度),并可预先缓存平均文档长度,这一步能显著提升用户搜索“手机壳防水”时,标题包含完整短语的文章排名更靠前。
性能优化:三层架构
- 第一层:进程内缓存,使用APCu存最近搜索热词的分词结果,热词命中即跳过分词。
- 第二层:MySQL内存临时表,对于超过10万级关键词,将索引表改为内存引擎MEMORY。
- 第三层:外置搜索引擎,若数据超百万条,推荐使用Sphinx(核心有中文分词插件)或Elasticsearch+IK分析器,PHP通过HTTP接口或官方客户端连接,并定期将MySQL数据同步至ES。
真实案例:某电商网站(日活5万)原使用LIKE搜索耗时2秒,改造为SCWS分词+MySQL索引后降至80ms,随后为应对大促压力迁移至ES,峰值响应30ms。
常见问题FAQ
Q1:分词后如何过滤无意义词?
答:维护一个停用词表(“的、了、啊”),并用array_diff($words, $stopwords)过滤,也可以统计词频,剔除IDF值过低的词。
Q2:用户输入全英文或数字怎么办?
答:使用正则/^[a-zA-Z0-9]+$/匹配,跳过中文分词直接按字符切分,可额外将连续数字合并成整体。
Q3:如何保证实时性?
答:高频词表可常驻内存(APCu);新词更新时只需重新生成增量索引,勿全量重建。
Q4:与Elasticsearch相比,自研有什么优势?
答:无限定制性(如加入领域专业词典),无额外运维成本,适合中小型项目,但ES的分布式和聚合分析能力难以替代。
Q5:分词对SEO有什么作用?
答:能让搜索引擎(如某些站内搜索)更精准抓取内容关键词,减少重复索引,符合Google对页面内容结构化的要求,但此点对站外搜索引擎排名影响甚微,主要提升站内用户体验。
从手写分词器到理解BM25排序,你已经掌握了构建自定义搜索的必备技能,实际项目中推荐采用“混合策略”:用SCWS处理80%常见词,对冷门词触发HMM模型回退,现在你可以动手改造你的博客或商城系统,体验分词搜索带来的质变,如果追求极简,也别忘了试试Jieba-PHP,它能让你在半小时内完成整个搜索模块。