软删除模型在elasticsearch中默认仍被索引,需在shouldbesearchable()中显式排除软删记录、同步时清空旧索引、路由绑定使用withtrashed()并手动校验、es查询加must_not exists deleted_at条件,确保全链路一致过滤。

软删除模型在 Elasticsearch 中默认仍被索引
软删除只是给 deleted_at 字段赋值,并不真正从数据库删掉记录;Elasticsearch 的同步逻辑(比如用 laravel/scout + elastic-client)若没显式过滤,就会把已软删的模型照常推到 ES 里。搜索时自然能查出来,和业务预期冲突。
常见错误现象:Model::withTrashed()->find(1) 能查到,但前端搜关键词却返回已被“删除”的文章——用户点进去发现 404 或权限拒绝,体验断裂。
- 确保 Scout 监听的是
saved和deleted事件,而不是仅created/updated - 重写模型的
shouldBeSearchable()方法,显式排除软删状态:public function shouldBeSearchable(): bool { return $this->exists && ! $this->trashed(); } - 如果用了自定义同步命令(如
php artisan scout:import),执行前先清空旧索引,否则残留数据不会自动清理
全文搜索结果里点击跳转 404,因为路由没处理软删除
ES 返回了 ID=123 的文档,但 Laravel 路由里写的是 Model::findOrFail($id) —— 它不认 trashed(),直接抛 ModelNotFoundException,最终 404。这不是搜索的问题,是后续链路断了。
使用场景:后台内容管理页带站内搜索,运营搜“活动预告”,点进结果却发现页面打不开。
- 控制器中改用
Model::where('id', $id)->withTrashed()->firstOrFail(),再手动检查if ($model->trashed()) { abort(404); } - 或者统一加中间件,在模型绑定阶段就拦截:
Route::bind('post', function ($value) { return Post::withTrashed()->findOrFail($value); });然后在对应控制器方法里判断$post->trashed() - 别依赖
Route::model()默认行为,它不处理软删除
Scout + Elasticsearch 同步延迟导致刚软删就搜得到
调用 $model->delete() 后,Scout 默认通过队列异步更新 ES,中间有毫秒到秒级延迟。用户刚点“删除”,立刻搜关键词,旧数据还在 ES 里,结果又出现了。
性能影响:强行同步(withoutSyncingToSearch 或 searchable() 手动触发)会阻塞请求,尤其批量操作时明显卡顿。
- 单条操作可接受短暂延迟,不用干预;但管理后台等强一致性场景,建议在软删后立即调用
$model->unsearchable() - 避免在事务里调用
unsearchable(),Elasticsearch 客户端可能抛连接异常,导致整个事务回滚失败 - 检查队列驱动是否正常(如
redis是否连通、horizon进程是否存活),延迟不是代码问题而是基础设施问题
用 whereRaw 或布尔查询绕过 Scout 时漏掉软删除过滤
有些搜索逻辑复杂(比如多字段加权、模糊拼写容错),直接写 SearchBuilder 或原生 DSL,这时候很容易只关注匹配逻辑,忘了加 "must_not": {"exists": {"field": "deleted_at"}} 这类条件。
参数差异:Scout 的 query() 方法默认不包含软删除过滤,它只负责把 PHP 查询翻译成 ES DSL,不注入业务规则。
- 所有手写
raw查询(包括DB::table('posts')->whereRaw(...)回退到 MySQL 全文搜索)都得自行补WHERE deleted_at IS NULL - ES DSL 示例(必须加):
{ "query": { "bool": { "must": [{ "match": { "title": "Laravel" } }], "must_not": [{ "exists": { "field": "deleted_at" } }] } } } - 别假设 “ES 里没存 deleted_at 就安全”——万一同步逻辑改过,字段存在但值为 null,
exists判断就失效了,稳妥做法是同时检查"term": {"deleted_at": null}和exists反向
软删除和搜索兼容的关键不在某一行代码,而在于每个数据出口(ES 索引、MySQL 查询、API 响应、路由解析)是否都对 deleted_at 做了同一套解释。漏掉任意一环,就会出现“搜得到但打不开”“删了还显示”这类静默异常。











