ThinkPHP 必须集成 Elasticsearch 实现全文检索,因其自带的 like 和 MySQL FULLTEXT 无法支持中文分词、相关性排序、高亮、模糊匹配及千万级数据高效查询;应使用官方 elasticsearch-php 客户端,通过模型事件同步数据,预建带中文分词器的索引,并封装搜索逻辑为 Service。

ThinkPHP 本身不内置全文检索能力,where + like 或 fulltext(依赖 MySQL MyISAM/InnoDB 的 FULLTEXT 索引)只能应付极轻量场景,一到中文分词、相关性排序、高亮、模糊匹配就崩。真要搞全文检索,得把 Elasticsearch 接进来——不是“能不能”,而是“必须用”。
为什么不能只靠 ThinkPHP 自带查询
ThinkPHP 的 Db::table()->where('title', 'like', '%关键词%') 是字符串模糊匹配,不支持:
- 中文分词(“笔记本电脑”不会自动拆成“笔记本”“电脑”)
- 词干提取(“running”和“ran”无法归一)
- 字段权重控制(标题比正文更重要?没法设)
- 拼写纠错(输错“elasticsearch”也能返回结果)
- 千万级数据下响应慢(
like '%x%'必全表扫描)
MySQL 的 MATCH ... AGAINST 虽然支持中文(需 ngram),但配置麻烦、更新延迟高、扩展性差,上线后基本没法调优。
用 elasticsearch-php 客户端直连 ES(推荐)
别用过时的 thinkphp-elasticsearch 扩展包,它封装太深、版本锁死、文档缺失。直接上官方客户端 elasticsearch/elasticsearch,可控性强,升级方便。
立即学习“PHP免费学习笔记(深入)”;
- 安装:
composer require elasticsearch/elasticsearch - 初始化客户端(建议放
app/common/EsClient.php):
$client = ClientBuilder::create()
->setHosts(['http://127.0.0.1:9200'])
->setRetries(2)
->build();
注意:setHosts 必须带协议(http:// 或 https://),漏了会报 cURL error 3;生产环境务必加认证(->setBasicAuthentication('user', 'pass'))。
同步数据到 ES 的关键时机与方式
ES 数据不能靠“查时再同步”,必须在业务写入时触发。ThinkPHP 最稳妥的是监听模型事件:
- 在
app/model/Article.php中定义:
protected static function init()
{
self::afterInsert(function ($article) {
$client->index([
'index' => 'article_index',
'id' => $article->id,
'body' => [
'title' => $article->title,
'content' => $article->content,
'created_at' => $article->created_at
]
]);
});
<pre class="brush:php;toolbar:false;">self::afterUpdate(function ($article) {
$client->update([
'index' => 'article_index',
'id' => $article->id,
'body' => ['doc' => ['title' => $article->title, 'content' => $article->content]]
]);
});}
⚠️ 避坑点:
- 不要在事务中直接调 ES 写入——ES 不参与 MySQL 事务,失败会导致数据不一致;加 try/catch + 本地日志记录失败 ID,后续用脚本重推
- 首次全量导入别用循环
index(),改用bulk()批量接口,速度提升 10 倍以上 - ES 索引要提前建好,定义好中文分词器(如
ik_max_word),否则搜索“人工智能”会当一个词切分
搜索逻辑怎么写进 ThinkPHP 控制器
别把复杂 DSL 全塞控制器里,抽成 Service 方法。例如:
// app/service/SearchService.php
public function search($keyword, $page = 1, $size = 10)
{
$params = [
'index' => 'article_index',
'body' => [
'query' => [
'multi_match' => [
'query' => $keyword,
'fields' => ['title^3', 'content']
]
],
'highlight' => [
'fields' => ['title' => new \stdClass(), 'content' => new \stdClass()]
]
],
'from' => ($page - 1) * $size,
'size' => $size
];
<pre class="brush:php;toolbar:false;">return $this->client->search($params);}
调用时注意:
-
multi_match的^3表示 title 字段权重是 content 的 3 倍,数值按实际效果调 - 返回结果里的
highlight是数组,需手动合并到原始数据中,content高亮可能截断,得用fragment_size控制长度 - ES 默认只返回 10000 条结果(
index.max_result_window),深度分页要用search_after,别硬改 from/size
ES 和 ThinkPHP 是两套体系,中间没银弹。字段映射对不上、分词器没生效、高亮标签被 HTML 转义……这些问题不会报错,只会让你搜不到东西。上线前一定拿真实语料跑几轮 case,重点看“同义词”“错别字”“长尾词”的召回率。



















