erikwang2013/webman-scout
Full-text search for Webman inspired by Laravel Scout. Add driver-based indexing and search to your models through a simple, familiar API, with support for popular search engines while keeping your existing model workflows intact.
erikwang2013/webman-scout is a port of Laravel Scout to the Webman runtime, so the mental model carries over almost 1:1 — a Searchable trait on your models, a driver-based engine layer, and an index/import command. What changes is the host: there is no artisan, no service container bootstrapped the Laravel way, and no guarantee that model events or the Laravel queue facade are wired up the way Scout expects.
Minimal path to first result:
composer require erikwang2013/webman-scout and confirm the engine adapter you actually want (Elasticsearch / Meilisearch / Algolia-style driver) is present in composer.json — Scout ports typically require the engine client as a separate dependency.config/plugin/erikwang2013/scout/ (Webman plugin convention) and set the driver plus host/credentials.Searchable trait to one model and define toSearchableArray() explicitly rather than relying on toArray().config/plugin/*/app.php (or config/plugin/erikwang2013/scout/app.php enable => true) and restart the process: php webman restart.php webman scout:import "app\model\Post" — note the backslash-separated class string, which differs from the artisan invocation you're used to.Post::search('keyword')->get().First real use case: a single low-traffic read path (site search, admin list filtering) on one model. Get that green end-to-end before touching index settings, because most of the friction in this package is engine configuration, not the PHP API.
Model layer. Keep toSearchableArray() explicit and flat. Anything you rely on for filtering (where('status', 1), whereIn('tags', ...)) must be a scalar or array field present in that array, not a relation loaded at query time. Use searchableAs() to pin index names so that renaming a class or moving namespaces doesn't orphan your index.
Bulk sync vs. event sync. This is the central decision. If model events aren't dispatched in your Webman bootstrapping (common — the Eloquent capsule needs an event dispatcher set explicitly), writes silently won't reach the index. Two safe patterns:
save() followed by a direct engine query, and only then trust it.$model->searchable() / $model->unsearchable() from your service layer or controller after persistence. More verbose, zero ambiguity, and easier to reason about inside transactions.For anything with bulk writes, prefer explicit sync and run scout:import on a schedule or as a post-deploy step.
Import and reindex workflow. Treat the index as a derived artifact:
php webman scout:flush "app\model\Post"
php webman scout:import "app\model\Post"
Chunk large imports (scout:import typically batches, but confirm the chunk size against your engine's bulk limits) and run them off-peak. For zero-downtime changes, point searchableAs() at a versioned index name (posts_v3), import fully, then flip the constant — far safer than an in-place reindex.
Query patterns. Stick to the portable subset: search(), where(), whereIn(), take(), paginate(), raw(). Hydration goes back through your model's query builder (whereKey), so results respect your connection but not your global scopes unless the package re-applies them — verify before assuming soft-deleted rows are excluded. Use ->raw() when you need engine-native scores or highlights, and ->keys() when you only need IDs to fan out into your own eager-loading query.
Queues. Don't assume Scout::queueUsing() works under Webman. If you want async indexing, prefer pushing searchable()/unsearchable() calls onto a Webman queue plugin job yourself, or drop a Redis pub/sub listener in front of the model writes. This is usually cleaner than trying to make the Laravel queue facade behave.
Integration tips. Register a thin service class (e.g. PostSearchService) that wraps search() + filters + pagination, so controllers never touch engine specifics. That single seam is also where you put the try/catch that degrades to a database LIKE query when the engine is down — search is rarely worth a 500.
v2.2.0...v2.2.1 yourself, especially around the engine client version constraint in composer.json and any changes to the Searchable trait or config keys. Pin the version ("2.2.1") rather than ^2.2 until you've read the diff, and re-run your import on staging before rolling to production.null can end up text and then reject numeric filters later. Fix is always: delete the index, re-import. Budget for this — it's the number-one time sink with this class of package._id as a string; hydration goes back via whereKey(). With an integer primary key you can get "found N results, hydrated 0 models." Cast explicitly in toSearchableArray() or in the hydration path.toSearchableArray() defaults to toArray(). Under Webman that can pull in relations you didn't intend to index and balloon document size. Always override it.config:cache, no artisan. Config changes need a php webman restart; long-running worker processes will happily serve stale engine credentials until you do.$model->searchable() work. Manual searchable() succeeding while save() does nothing is the classic signature of missing event wiring.ik_max_word/ik_smart or a CJK analyzer on text fields. Default standard analyzer tokenizes per-character and gives poor relevance.shouldBeSearchable() is your filter for partial indexing. Cheap way to keep drafts and soft-deleted rows out of the index without touching your query layer.Searchable trait (extending the packaged one) when you need to change indexing behavior globally, e.g. adding a tenant ID to every document and forcing it into every query.search() returns nothing, bypass the package. Hit the engine directly (curl the index's _search endpoint, or the Meilisearch /indexes/{uid}/search) and confirm the documents exist and are shaped as expected. That instantly splits "indexing problem" from "query-building problem."How can I help you explore Laravel packages today?