mozex/laravel-scout-bulk-actions
Laravel Scout's commands handle one model at a time. This package auto-discovers every model using the Searchable trait and lets you import, flush, or refresh them all with a single command, plus queued bulk imports for faster indexing.
Install and publish config:
composer require mozex/laravel-scout-bulk-actions
php artisan vendor:publish --tag="scout-bulk-actions-config"
Requires PHP 8.2+, Laravel 11/12/13, and laravel/scout ^10.18 (Scout 11 supported since 1.7.0). The service provider is auto-discovered — no manual registration.
Where to look first: config/scout-bulk-actions.php. Everything the package does hinges on one key:
return [
'model_directories' => [
app_path('Models'),
],
];
If your models live anywhere else (app/Domain/*/Models, Modules/*/Models, a legacy app/ root), the commands will silently do nothing useful — see Gotchas.
Sanity check discovery before doing anything destructive:
php artisan list scout # confirm the four commands are registered
php artisan scout:import-all -v # watch the progress bar, note the model count
The progress bar is your diagnostic: if it shows fewer models than you expect, the config is wrong, not the search engine.
First real use case — you've got 8 searchable models and a fresh Meilisearch/Algolia/Typesense index that's empty. Instead of eight scout:import calls:
php artisan scout:import-all
Then, when the index schema or toSearchableArray() changes and you need a clean rebuild on a staging box:
php artisan scout:refresh --force
That's the whole package in practice. Everything below is about doing it safely and at scale.
Wire reindexing into your deploy script rather than remembering to run it by hand. Prefer --fresh over refresh on anything with live traffic:
# deploy.sh / Envoyer / Deployer task, after migrations
php artisan scout:import-all --fresh --chunk=500 --force
Why: scout:import-all --fresh flushes one model at a time, immediately before re-importing it. Each index is only incomplete for the duration of its own import, so search keeps serving results for the other seven models the whole time. scout:refresh flushes every index first, then imports — a much longer window where search returns nothing.
Reserve scout:refresh --force for staging, ephemeral review apps, or a genuine driver/schema migration where a blank slate is required.
--force is non-negotiable in CI: all four commands prompt for confirmation when APP_ENV=production, and a non-interactive runner will hang or abort otherwise.
Once you're past a few hundred thousand rows, synchronous import blocks a deploy for too long. scout:queue-import-all splits each model into ID-range chunks and dispatches one job per range, mirroring Scout's own scout:queue-import across every discovered model:
php artisan scout:queue-import-all --queue=indexing --chunk=1000 --force
Operational requirements this implies:
php artisan queue:work --queue=indexing.--tries / --timeout on the worker for your chunk size. A 1000-record chunk hitting a rate-limited driver can take minutes.Pair with --order=desc on long rebuilds:
php artisan scout:queue-import-all --order=desc --force
Default is ascending ID, so a rebuild that takes four hours puts your oldest content in search first and your newest content last. --order=desc inverts that so recent posts/products are searchable within minutes. Requires Laravel Scout 11.4+ — the package forwards Scout's --order flag and validates it as asc|desc before dispatching anything (because Scout itself exits 0 on an invalid value, which would otherwise look like success).
Symfony Finder globs are supported, so monorepo-style module trees work without listing every path:
'model_directories' => [
app_path('Models'),
base_path('Modules/*/Models'),
base_path('app/Domain/*/Models'),
],
For multi-tenant setups where each tenant has its own index namespace, indexing is still per-model, so your searchableAs() / index config must be tenant-aware at runtime. Bulk commands run in a single process with no tenant context bound — which makes them a good fit for a global "rebuild everything" job but a poor fit for per-tenant rebuilds. For those, dispatch your own job that binds the tenant and calls scout:queue-import-all with a narrowed config, or just loop Model::makeAllSearchable() yourself.
You don't need a real search engine to catch a broken model_directories config. Point Scout at the collection driver in your test environment:
// phpunit.xml
<env name="SCOUT_DRIVER" value="collection"/>
php artisan scout:import-all --force
This exercises the full discovery → reflection → import path cheaply. A green run means your glob patterns resolved and every intended model passed the Searchable check. Add it as a smoke test after a refactor that moves models around.
There's no documented public API or events — the commands are the API. If you need programmatic control (e.g. reindex only models tagged in a config array), shell out and check the exit code rather than calling Artisan::call and ignoring the result:
$exit = Artisan::call('scout:import-all', ['--force' => true]);
if ($exit !== 0) {
throw new RuntimeException('Scout reindex failed: ' . Artisan::output());
}
The command returns non-zero on the first model that throws, which is exactly the signal you want in a deploy gate.
The Searchable trait must be used directly on each model. If you have a BaseModel that use Searchable and children that inherit from it, the children will not be discovered — reflection checks the trait on the concrete class. This is the single most common "why is my index empty" cause, explicitly called out in the package's own Boost skill. Add use Searchable; to every model, even if it's redundant.
Silent skips, not errors. A model is only collected if it is (1) found by the Finder scan, (2) autoloadable, (3) a concrete non-abstract Eloquent model, and (4) directly uses Searchable. Failing any check just means it isn't in the progress bar. Always eyeball the model count on the first run after adding a new domain/module. If the count is short, check your glob pattern before you check your search driver.
Abstract base classes and interfaces are correctly ignored — but that also means a Searchable interface (a common "marker trait" pattern) does nothing here. Trait only.
config:cache will bite you. If your deploy caches config, editing model_directories on the server has no effect until you re-cache. After changing it in production: php artisan config:clear && php artisan config:cache.
scout:flush-all is an unfiltered, unconfirmed-by-default index wipe. No --dry-run, no "flush only these models", no backup. It removes every record for every discovered model. There is no per-model flag on it either — only scout:refresh accepts a model argument:
php artisan scout:refresh "App\Models\Post" --force
If you need to flush a single model, that's the command (it'll flush then re-import), or reach for native scout:flush "App\Models\Post" instead.
Failure halts the entire run. One model throwing (bad toSearchableArray(), driver auth error, a nullable column in a cast) stops everything and returns a failure exit code. That's good for deploy gating, bad for partial progress. Since imports are idempotent, the recovery is simply re-running — but be aware that under scout:refresh, models after the failure point have been flushed and never re-imported. Prefer import-all --fresh if a mid-run failure leaving you with empty indexes is unacceptable.
Queued runs report success on dispatch, not on indexing. scout:queue-import-all exits 0 as soon as jobs are pushed. Real failures land in failed_jobs later. If indexing correctness matters for a release, add a post-deploy check (a scout:import-all dry comparison, or a scheduled job that asserts document counts per index) rather than trusting the exit code.
Chunk sizing is a driver concern, not a Scout concern. --chunk falls back to config('scout.chunk.searchable') (500 by default). Pushing it very high to "go faster" reliably trips Algolia's ~10 MB batch payload limit and Meilisearch's request size cap. 200–500 is a sane range; go down, not up, if you're seeing 4xx responses in worker logs.
No throttling or retry. The bulk commands fire operations as fast as the driver accepts them, with no backoff. Against a rate-limited Algolia plan this will eventually produce errors that halt the run. If you hit that, drop --chunk and run during an off-peak window, or use the queued variant with a rate-limited queue connection so your workers self-pace.
--order needs a version check before it silently no-ops. It requires Laravel Scout 11.4+. The package validates the value itself, but not the Scout version — check composer show laravel/scout if desc appears to have no effect.
Soft deletes and stale documents. A flush clears the whole index and a re-import only indexes rows reachable through searchable(), so trashed records correctly vanish after refresh/--fresh. But if you rely on Searchable's model events alone with SoftDeletes, make sure your deleted handler actually calls unsearchable() — bulk actions won't fix an event-hook gap.
Pin the version and keep an escape hatch. 14 stars, zero dependents, single maintainer. The surface area is small enough that the fallback is trivial — a one-off command that reflects over your model list and calls Model::makeAllSearchable() / removeAllFromSearch(). Write it down. Don't let a deploy pipeline depend on an unpinned patch release of a pre-2.0 package.
Laravel Boost users: since 1.6.0 the package ships a Boost skill with the commands, glob setup, and the direct-trait pitfall. Run php artisan boost:update after upgrades so your AI assistant gets current context instead of guessing.
How can I help you explore Laravel packages today?