ThinkPHP中应通过容器单例管理Elasticsearch客户端,避免多次new;索引需显式定义mapping并用命令行创建;ES仅作搜索辅助,先查ID再查MySQL主库;分页需处理track_total_hits或改用search_after。

ThinkPHP 里怎么用 Elasticsearch 客户端?别 new 多次
TP 应用里直接 new Elasticsearch\Client() 是最常见也最危险的做法——每次请求都新建连接,会迅速耗尽 PHP 进程的 socket 资源,尤其在并发稍高时出现 Connection refused 或超时。必须封装成单例,且生命周期绑定到请求上下文。
推荐在 app/common.php 或服务提供者中注册:
// app/provider/ElasticsearchServiceProvider.php
use Elasticsearch\ClientBuilder;
<p>return [
'elasticsearch' => [
'class' => \Elasticsearch\Client::class,
'shared' => true, // ThinkPHP 6+ 容器默认 shared,但显式写更安全
'constructor' => [
'hosts' => ['<a href="https://www.php.cn/link/31917677a66c6eddd3ab1f68b0679e2f">https://www.php.cn/link/31917677a66c6eddd3ab1f68b0679e2f</a>'],
],
'extend' => function ($client) {
// 可加全局 header,比如带认证
$client->setTransport(new \Elasticsearch\Transport($client->getConnectionPool(), [
'headers' => ['Authorization' => 'Basic ' . base64_encode('user:pass')],
]));
return $client;
}
]
];</p>
- 务必确认
shared为true,否则容器每次get()都返回新实例 - 不要在控制器或模型里手动
new Client(),哪怕只调一次——后续维护者大概率会复制粘贴 - 如果用了 Swoole 或 Hyperf 长生命周期环境,还需额外处理连接池复用,TP 默认不支持,得换
elasticsearch/elasticsearchv8+ 的AsyncClient或自己加连接池管理
索引怎么建才不踩坑?PUT /index_name 不是万能的
直接发 PUT /my_index 创建索引看似简单,但没指定 mapping 就写入数据,ES 会按字段值自动推断类型(比如把 "123" 当成 long,后续写 "abc" 就报 illegal_argument_exception)。线上环境必须显式定义 mapping。
建议把索引创建逻辑抽成命令行任务,在部署时执行:
// app/command/CreateIndex.php
protected function configure()
{
$this->setName('es:create-index')->setDescription('Create ES index with strict mapping');
}
<p>protected function execute(Input $input, Output $output)
{
$client = \think\Container::get('elasticsearch');
$client->indices()->create([
'index' => 'article',
'body' => [
'settings' => [
'number_of_shards' => 1,
'number_of_replicas' => 0,
],
'mappings' => [
'properties' => [
'id' => ['type' => 'long'],
'title' => ['type' => 'text', 'analyzer' => 'ik_smart'],
'content' => ['type' => 'text'],
'created_at' => ['type' => 'date', 'format' => 'strict_date_optional_time'],
]
]
]
]);
}</p>
- 别依赖
auto_create_index配置——它会让 ES 自动建索引,但 mapping 不可控,查不到数据时连问题在哪都难定位 - 中文分词器(如
ik)必须提前装好,且在 mapping 中明确指定analyzer,否则text字段默认用standard,搜中文基本没结果 - ES 7.x+ 已废弃
type,mapping 里别再写'_doc'或'properties'套一层 type
TP 模型怎么和 ES 索引联动?别硬套 ORM 思维
ThinkPHP 的 Db 或模型是面向关系数据库的,ES 是文档型,强行让 ArticleModel::search() 返回 Collection 对象,会导致 ID 映射错乱、关联查询失效、分页参数传错——这不是封装得好不好,是范式根本不同。
正确做法是分层:业务层调用 ES 获取 ID 列表,再用 TP 的 whereIn('id', $ids) 查 MySQL 主库取完整数据:
Elasticsearch 9.4.1 Linux 版本现已开放下载,这是官方最新发布的分布式搜索与分析引擎。Linux 版本全面支持 x86_64 与 aarch64 架构,提供 .tar.gz、.deb 及 .rpm 多种安装包格式,可灵活适配 Ubuntu、CentOS、Debian 等主流发行版。该版本延续了 9.4 系列的核心特性,包括原生 Prometheus 支持、正式版 Elast
// 搜索服务类
class ArticleSearchService
{
public function search($keyword, $page = 1, $size = 10)
{
$client = \think\Container::get('elasticsearch');
$result = $client->search([
'index' => 'article',
'body' => [
'query' => ['match' => ['title' => $keyword]],
'from' => ($page - 1) * $size,
'size' => $size,
]
]);
<pre class="brush:php;toolbar:false;"> $ids = array_column($result['hits']['hits'], '_id');
$ids = array_map('intval', $ids); // ES 返回 _id 是 string,MySQL 主键是 int
return ArticleModel::whereIn('id', $ids)->select();
}}
- ES 返回的
_id是字符串,而 TP 模型默认主键是int,不转类型会导致whereIn查不到数据 - 不要把 ES 当主库用——它不保证强一致性,更新延迟、删除不即时、事务不可靠,只适合查,不负责写
- 如果真要实时同步,用
logstash或监听 MySQL binlog(如canal),而不是在 TP 的save()里手动 push 到 ES
为什么搜索结果总少几条?检查 track_total_hits 和分页逻辑
ES 默认只统计前 10000 条命中数,超过就返回 "total": {"value": 10000, "relation": "gte"},但 TP 分页组件看到 total 是 10000 就以为只有这么多,导致最后几页空白。这不是数据丢了,是统计被截断了。
两种解法选其一:
- 在搜索请求里加
'track_total_hits' => true(ES 7.0+),强制精确统计,但大数据量下性能下降明显 - 更推荐改分页逻辑:用
search_after替代from/size,避免深度分页,尤其当用户翻到第 100 页时 - TP 的
paginate()无法直接对接search_after,得自己封装分页器,把上一页最后一条的sort值作为下一页的search_after参数传过去
容易被忽略的是:ES 的 highlight 高亮字段默认不返回原始内容,要显式加 '_source' => ['title', 'content'],否则前端拿到空数据还怪后端没传。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










