必须重写shouldbesearchable()排除软删记录并清空重建索引,否则仅用es的must_not会导致脏数据残留、性能下降和链路断裂;路由绑定需配合withtrashed()与手动校验,确保全链路一致性。

ES 查询里用 must_not 剔除软删记录
直接在 Elasticsearch DSL 里加 must_not 并不能自动屏蔽软删除数据——因为 Laravel Scout 同步时没过滤,ES 里本就存着 deleted_at 为非空的文档。你查 must_not 只是“查时不返回”,但这些脏数据仍占索引空间、拖慢写入、干扰聚合统计。
- 必须先确保模型同步时跳过软删项:重写
shouldBeSearchable(),显式返回!$this->trashed() - 已有脏数据得清空重建索引,
scout:import不会自动覆盖旧文档 -
must_not适合临时排除,比如搜索时加“不显示已下架商品”,但不是软删的兜底方案 - 别用
filter+exists组合去查deleted_at是否存在——软删后该字段有值,exists会命中,得用must_not+exists或直接term匹配null(ES 里 null 不可查,所以必须存为字符串或布尔标记)
bool 查询中 filter 和 must_not 混用的陷阱
ES 的 bool 查询里,filter 和 must_not 不是并列关系,而是作用于不同逻辑层:filter 缩小结果集但不影响相关性评分;must_not 是硬性排除,且一旦和 should 或 must 共存,容易因布尔逻辑短路导致意外漏匹配。
- 想同时满足“状态=active”且“非测试账号”,用
must+must_not是安全的;但若混入should(比如“标题含关键词 OR 描述含关键词”),must_not会作用于整个should分支,可能把本该命中的文档全干掉 - 优先用
filter实现“状态=active AND deleted_at IS NULL”,再用must_not处理业务级排除(如“排除黑名单用户ID”),避免嵌套过深 - Laravel Scout 默认生成的查询不带
filter,要手动构造 DSL 才能用上——别依赖where()链式调用,它转成的是must,不是filter
Scout 搜索结果 ID 跳转时 404 的真实原因
ES 返回了 ID=123 的文档,但 Laravel 路由里 Model::findOrFail(123) 报 404,问题不在 ES 查询,而在模型绑定阶段没处理软删除状态。
- 别改前端或 ES 层去“隐藏 ID”,这是链路断裂——ES 只管搜得准不准,不负责业务可见性
- 路由绑定必须显式加载软删记录:
Route::bind('post', fn($id) => Post::withTrashed()->findOrFail($id)) - 控制器里立刻校验:
if ($post->trashed()) { abort(404); },否则中间件或全局策略容易漏判 - 如果用了 Scout 的
searchableAs()自定义索引名,确保同步脚本和搜索查询用的是同一个索引,否则withTrashed()查不到对应数据
刚软删就搜得到:同步延迟怎么压到毫秒级
调用 $model->delete() 后,ES 里还能搜到该记录,是因为 Scout 默认走队列异步更新。哪怕用 syncWithSearch() 强制同步,也得等事务提交完成才能触发事件监听。
- 关键点在于数据库事务边界:
DB::transaction()内 delete 后立即$model->searchable(),ES 更新才不会被回滚 - 批量软删时别用
whereIn()->update(['deleted_at' => now()]),这绕过了 Eloquent 事件,Scout 完全收不到通知 - 真要零延迟,得关掉队列、用
withoutSyncingToSearch()手动控制时机,但会阻塞 HTTP 请求——权衡点就在这里:用户体验 vs 接口响应时间 - 监控
scout:flush和scout:import的执行耗时,ES 写入慢常因 bulk size 设置过大(默认 100)或 network round-trip 过高











