必须将海量检索逻辑拆入队列任务异步执行,因Scout的search()同步调用会导致内存爆、超时和阻塞;需改用原生Elasticsearch客户端配合multi_search API,并做好连接池、错误兜底与内存控制。

直接用 Laravel Scout 同步调用 search() 做海量检索,内存爆、超时、阻塞请求是必然结果。必须把检索逻辑拆进队列任务里异步执行,再通过回调或状态轮询获取结果。
为什么不能在控制器里直接调用 search() 处理大量关键词
Scout 的 search() 默认走同步 HTTP 请求(如 Algolia/Elasticsearch),单次响应慢、并发高时连接池打满;更关键的是,如果你循环调用 search() 处理 1000 个关键词,PHP 进程会卡死在 I/O 上,不释放内存,php-fpm worker 很快耗尽。
- 本地开发时设
SCOUT_QUEUE=false是为了调试,不是为批量场景准备的 - 即使开了队列,
search()本身不进队列——只有model->searchable()或Model::search(...)->update()这类索引操作才触发队列任务 - 海量检索 ≠ 索引更新,Scout 不提供“批量搜索进队列”的原生能力,得自己封装
如何把 Elasticsearch 检索逻辑塞进 Laravel 队列任务
核心是:别依赖 Scout 的查询接口,改用原生 Elasticsearch 客户端(如 elasticsearch/elasticsearch)发请求,并确保任务类实现 ShouldQueue。
- 在任务构造函数中只传必要参数,比如关键词数组、回调 URL、超时阈值,**不要传模型实例或 Eloquent 对象**(序列化大对象易失败)
- handle() 中用
Client::search()发起多关键词请求,推荐用multi_searchAPI 批量合并(比循环调用省连接) - 结果处理完后,用
Http::post()或事件通知下游系统,避免在任务里阻塞等待响应 - 加
public $tries = 3;和public $timeout = 120;,防止 ES 临时抖动导致任务卡死
chunkById + 队列组合处理“查完再搜”的场景
有些需求是:先从数据库捞出一批记录(比如 5 万条用户),再对每条记录的字段去 ES 检索关联内容。这时不能 get()->each(...),而要用 chunkById 控制内存,再把每批交给队列。
- 主流程用
User::where('status', 'active')->chunkById(500, function ($users) { ... }) - 闭包内对每批
$users构造一个SearchBatchJob,传入$users->pluck('id')->all()和字段名 - Job 内部用
Client::msearch()一次查多个文档,避免 N 次 round-trip - 注意:ES 查询条件里别用 PHP 变量拼接 JSON 字符串,用
Builder类或数组结构防注入
容易被忽略的连接与错误兜底点
ES 客户端默认不复用连接,高频队列任务下容易触发 Connection refused 或 Too many open files。
- 初始化
Client时传'connectionPool' => \Elasticsearch\ConnectionPool\StaticNoPingConnectionPool::class,并预热连接池 - 捕获
Elasticsearch\Common\Exceptions\NoNodesAvailableException,记录到failed_jobs并告警,别静默重试 - 队列任务里别用
dd()或dump(),日志统一走Log::channel('es'),方便和 Horizon 关联排查 - 如果 ES 返回 hits 为空但 status=200,检查 mapping 是否变更、字段是否被 analyzer 处理掉(比如 keyword 字段搜 text 内容)


















