erikwang2013 / webman-scout
Scout-style full-text search for Eloquent models on Webman, Laravel, Hyperf, ThinkPHP, Yii2 and Yii3 — 9 engines (OpenSearch, Elasticsearch, Meilisearch, Typesense, Algolia, XunSearch, Database, Collection, Null) with time ranges, geo distance, vector/KNN, aggregations and facets.
Requires
- php: ^8.0
- illuminate/bus: ^7.0|^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/contracts: ^7.0|^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/database: ^7.0|^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/events: ^7.0|^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/http: ^7.0|^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/pagination: ^7.0|^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/queue: ^7.0|^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/support: ^7.0|^8.0|^9.0|^10.0|^11.0|^12.0
- symfony/console: ^5.4|^6.0|^7.0
Requires (Dev)
- algolia/algoliasearch-client-php: ^3.2|^4.0
- meilisearch/meilisearch-php: ^1.0
- mockery/mockery: ^1.0
- orchestra/testbench: ^5.0|^6.0|^7.0|^8.0|^9.0|^10.0
- php-http/guzzle7-adapter: ^1.0
- phpstan/phpstan: ^1.10
- typesense/typesense-php: ^4.9.3
Suggests
- algolia/algoliasearch-client-php: Required to use the Algolia engine (^3.2).
- hightman/xunsearch: Required to use the XunSearch engine.
- illuminate/cache: Useful when the host has no cache: routes Support\Cache through the Illuminate Cache facade instead of the per-process array store.
- illuminate/log: Useful when the host has no logger: routes Support\Log through the Illuminate Log facade instead of error_log().
- meilisearch/meilisearch-php: Required to use the Meilisearch engine (^1.0).
- opensearch-project/opensearch-php: Required to use the OpenSearch engine (^2.0).
- psr/log: Required to hand the package a PSR-3 logger via Support\Log::setLoggerResolver().
- psr/simple-cache: Required to hand the package a PSR-16 cache via Support\Cache::setPsr16Resolver() (Yii3 / custom hosts).
- typesense/typesense-php: Required to use the Typesense engine (^4.9).
- yiisoft/yii2: Required to use Scout in Yii2 (config() polyfill, cache/log adapters, console bridge).
- yiisoft/yii3: Required to use Scout in Yii3 (config plugin, PSR-3/PSR-16 adapters).
Provides
None
Conflicts
- algolia/algoliasearch-client-php: <3.2.0|>=5.0.0
Replaces
None
This package is auto-updated.
Last update: 2026-09-25 16:22:45 UTC
README
Navigate (top): Top · Features · Project structure · Architecture · Feature design · Lifecycle · Framework support · Plain PHP · Requirements · Installation · Framework-specific setup · Configuration · Model setup · Basic usage · Advanced builder · Artisan / Webman commands · Queues · Builder reference · Mascot · References · License
Left navigation (outline)
- Features
- Project structure
- Architecture
- Feature design
- Lifecycle
- Framework support
- Requirements
- Installation
- Framework-specific setup
- Configuration
- Model setup
- Basic usage
- Advanced builder (OpenSearch / Elasticsearch-oriented)
- Artisan / Webman commands
- Queues
- Builder reference (extensions)
- Mascot
- References
- License
webman-scout
Sniffy · 探探 — the scout dog. It sniffs out your documents, buries them in the index, and tracks them back down on demand.
Meet it in person: php webman scout:about · more about the mascot
Driver-based full-text search for Eloquent models, inspired by Laravel Scout and shopwwi/webman-scout. One package, six frameworks (Webman · Laravel · Hyperf · ThinkPHP · Yii2 · Yii3) plus plain PHP, and nine engines (OpenSearch · Elasticsearch · Meilisearch · Typesense · Algolia · XunSearch · Database · Collection · Null), with a Scout-compatible API plus time ranges, geo distance, vector / KNN search, aggregations and facets.
Languages: this file is English (default). 简体中文
Features
- Scout-like API for easy migration from Laravel Scout / shopwwi webman-scout
- Engines: OpenSearch, Elasticsearch, Meilisearch, Typesense, Algolia, XunSearch, Database, Collection, Null
- OpenSearch-first advanced queries: aggregations, facets, KNN, geo distance
- Runs on Webman, Laravel, Hyperf, ThinkPHP, Yii2, Yii3 or plain PHP (
Scout::configure(), no container to bootstrap) - Optional queue-driven indexing (Webman Redis Queue when available), synchronous with a logged downgrade when not
- Index settings sync, soft deletes, chunked import
- Design docs with diagrams: project structure · architecture · feature design · lifecycle
Project structure
webman-scout/
├── src/
│ ├── Searchable.php # Eloquent trait: search(), searchable(), toSearchableArray(), metadata
│ ├── ModelObserver.php # saved / deleted / restored / forceDeleted → index sync
│ ├── SearchableScope.php # chunkById-based searchable()/unsearchable() macros + progress events
│ ├── Builder.php # chainable query builder: basic + advanced conditions, pagination
│ ├── EngineManager.php # driver resolution (createXxxDriver) + instance cache + extend()
│ ├── Manager.php # generic driver manager (driver(), forgetDrivers(), __call)
│ ├── Scout.php # VERSION + Scout::engine($name) + Scout::configure() for plain PHP
│ ├── ScoutConfig.php # cross-framework config root resolution (SCOUT_CONFIG_KEY / plugin / Yii / array)
│ ├── Install.php # Webman plugin installer (copies config + queue consumers)
│ ├── XunSearchClient.php # XunSearch connection wrapper (XS)
│ ├── Engines/ # 16 engine classes, all extending Engines\Engine
│ ├── Command/ # 8 Symfony commands (scout:import, scout:about, …)
│ ├── Jobs/search/ # Webman Redis Queue consumers, copied into app/queue/redis/search
│ ├── Support/ # Cache / Log adapters: Webman, Illuminate, Yii, PSR-16/PSR-3 + ArrayStore fallback
│ ├── Attributes/ # #[SearchUsingFullText], #[SearchUsingPrefix]
│ ├── Concerns/ # model resolution, pagination restore helpers
│ ├── Contracts/ Events/ Exceptions/
│ ├── Yii/ # console bridge (yii scout/*)
│ ├── Yii3/ # ScoutConfigProvider + config integration
│ └── config/plugin/erikwang2013/webman-scout/
│ ├── app.php # all Scout options (driver, prefix, queue, chunk, engine blocks)
│ ├── command.php # command registration for Webman
│ └── ini/ # XunSearch ini snippets
├── tests/ # PHPUnit suite: engines, commands, events, jobs, framework stubs
├── REVIEW_REPORT.md # per-round review: findings, fixes, open trade-offs
├── helpers.php # app() / event() / config() / base_path() / scout_config() + bindings
└── docs/
├── images/ # pet.svg + architecture.svg + features.svg + lifecycle.svg
└── zh-CN/README.md # Chinese documentation
Where to start when changing something:
| I want to … | Start at |
|---|---|
| Search / paginate a model | Searchable::search() → src/Builder.php |
| Add a query capability | src/Builder.php + that engine's search() |
| Add a search engine | EngineManager::createXxxDriver() + extends Engines\Engine |
| Change when the index is written | src/ModelObserver.php |
| Change how config is resolved | src/ScoutConfig.php |
| Configure without a framework | Scout::configure() → src/ScoutConfig::setArraySource() |
| Add a host logger / cache | Support\Log::setLoggerResolver() / Support\Cache::setPsr16Resolver() |
| Turn indexing into a background job | src/Searchable.php (queueMakeSearchable) + src/Jobs/search/ |
Architecture
Three layers do the actual work, and everything else is pluggable around them:
- Bootstrap seam.
helpers.phpdefinesapp(),event(),config()andscout_config()only when the host does not already provide them, then bindsEngineManageron the Illuminate container.ScoutConfigresolves the config root once —SCOUT_CONFIG_KEY→ Webman plugin path →scout→ Yiiparams→ array registered withScout::configure()(plain PHP) — so the same package code runs on every host. Eloquent has agetenv('KEY') ?: 'default'gotcha inapp.phpcomments; follow it. - Model layer.
use Searchableboots three things: a global scope, aModelObserver, and collection macros. The observer decides whether to write and delegates how to write;SearchableScopeprovides chunkedsearchable()/unsearchable()macros for imports and emits progress events. - Query + engine layer. Every read path funnels through one
Builder, which hands off toEngineManager::driver(); every engine implements the sameEnginecontract (update/delete/search/paginate/map/flush/createIndex), so switching engines never changes application code.
Cross-cutting concerns stay out of that flow: commands, queue jobs, events, contracts and the framework cache/log adapters are registered on demand (see the right column of the diagram).
Feature design
The six capability blocks are independent: an engine only has to implement the base contract, and the advanced builder methods degrade to NotSupportedException (or are ignored) where an engine cannot serve them. Practical consequences:
- Search API mirrors Scout, so migration is mostly a
use Searchableswap; pagination works on the engine side or falls back to the database when an engine cannot count. - Advanced queries (
whereRange,whereGeoDistance,fulltextSearch,vectorSearch,aggregate,facet) are OpenSearch/Elasticsearch-first but implemented per engine where the backend supports them. - Indexing is observer-driven and synchronous by default; enable the queue and the same calls become
scout_make/scout_removejobs. Missingwebman/redis-queueis logged and downgraded to synchronous instead of silently dropping writes.
Lifecycle
Write path (model → index)
save()/delete()/restore() scalar writes on the model
└─ ModelObserver::saved() or SearchableScope::searchable() for bulk imports
├─ syncingDisabledFor()? withoutSyncingToSearch() → skip
├─ searchIndexShouldBeUpdated() / shouldBeSearchable() → decide index / unindex
└─ searchable() / unsearchable()
├─ queue = true → Redis Queue scout_make / scout_remove (falls back to sync)
└─ queue = false → syncMakeSearchable() → EngineManager::driver()->update($models)
└─ toSearchableArray() → document upsert
Read path (query → index → model)
Model::search($query, $callback) → Builder (where / orderBy / take / advance conditions)
└─ engine->search($builder) → backend query (hits + total + aggregations/facets)
└─ mapIds() → primary keys
└─ queryScoutModelsByIds() → Eloquent hydration (or whereIntegerInRaw fallback)
└─ get() / paginate() / cursor()
The index stores documents, not rows: results are fetched by primary key from the database, so total and pagination fall back to a database count whenever an engine cannot provide one.
Index states
| State | Meaning | Reached by |
|---|---|---|
| Not indexed | No document in the index | initial state · flush() · scout:delete-index · scout:import --fresh |
| Indexed | Document present | save() · scout:import · restore() |
| Soft-deleted | __soft_deleted = 1, document kept |
delete() when soft_delete = true |
| Removed | Document deleted | forceDelete() · unsearchable() |
Framework support
Runtime integration targets applications that expose Laravel’s config() helper and an Illuminate container (app()), with Eloquent (Illuminate\Database\Eloquent\Model) models. For Yii2/Yii3 the package provides the config() polyfill itself (see below), so only Eloquent + an Illuminate container are required.
| Framework | Versions | Notes |
|---|---|---|
| Webman | 1.x / 2.x | Default install: plugin config under config/plugin/erikwang2013/webman-scout/. |
| Laravel | 7.x – 12.x | Copy the plugin app.php array into config/scout.php (or config/erikwang2013.webman-scout.php) and set SCOUT_CONFIG_KEY (see below). Requires PHP 8.0+ (Laravel 7 on PHP 8 is supported in recent 7.x releases). |
| Hyperf | 2.x – 3.x | Use Hyperf’s config + DI; Hyperf\Database\Model is Eloquent-compatible. Map Scout options into config and set SCOUT_CONFIG_KEY if not using the Webman plugin path. |
| ThinkPHP | 6.x / 8.x | Use when the app loads Illuminate config / app() (e.g. hybrid setups or illuminate/database Eloquent models). Native think\Model is not wired to the Searchable trait; call engine APIs manually or use Eloquent models for indexed entities. |
| Yii2 | 2.x | config() polyfill reads Yii::$app->params['scout']; cache/log route through Yii::$app->cache and Yii::info|warning|error; console bridge yii scout/*. |
| Yii3 | 3.x | Config plugin (ScoutConfigProvider) injects default scout params; PSR-16 cache + PSR-3 logger; register the Symfony commands under yiisoft/yii-console. |
| Plain PHP | 8.0+ | No framework and no container bootstrap needed: Scout::configure([...]) (or a path to a file returning the array) supplies the config, helpers.php supplies app() / event() / config(). Add your own Eloquent bootstrap (e.g. Illuminate\Database\Capsule\Manager). See Plain PHP. |
Composer requires illuminate/* ^7.0 – ^12.0 and symfony/console ^5.4 – ^7.0 so dependency resolution matches your framework stack. illuminate/events is a direct requirement (the model observer and the import progress events dispatch through Illuminate\Events\Dispatcher).
Requirements
- PHP ^8.0
- Eloquent models for the
Searchabletrait illuminate/bus,contracts,database,events,http,pagination,queue,support(versions aligned with your Laravel / Hyperf / ThinkPHP stack)- Optional:
illuminate/log/illuminate/cache/psr/simple-cache/psr/logif you want the host logger and cache instead of the built-inerror_log()and per-process array store
Installation
composer require erikwang2013/webman-scout
The Composer autoload files entry loads helpers.php, which defines app(), event(), config() (Yii2/Yii3 and plain PHP), base_path() and scout_config() when the host does not provide them, and binds EngineManager on the container.
Webman
After install, run the plugin installer (copies config and queue consumers):
- Config:
config/plugin/erikwang2013/webman-scout/ - Consumers:
app/queue/redis/search/
Laravel / Hyperf / ThinkPHP (non-plugin layout)
- Copy the contents of
src/config/plugin/erikwang2013/webman-scout/app.phpinto your application config, e.g.config/scout.php, returning the same associative array (keys:driver,prefix,opensearch,meilisearch, …). - Set environment variable
SCOUT_CONFIG_KEY=scout(no trailing dot) so lookups useconfig('scout.driver'), etc., instead of the Webman plugin path. - Ensure your bootstrap registers Illuminate’s
configrepository and container soconfig()andapp()resolveEngineManagerand engine clients.
If SCOUT_CONFIG_KEY is unset, the package prefers config('plugin.erikwang2013.webman-scout.app') when that array exists; otherwise it tries scout or erikwang2013.webman-scout.
Framework-specific setup
The following sections assume composer require erikwang2013/webman-scout is already done.
Webman (1.x / 2.x)
- Enable the plugin in your Webman project (per Webman plugins) so that
config/plugin/erikwang2013/webman-scout/is published. If your stack runs the packageInstallstep, it copies plugin config and queue consumers; otherwise copy fromvendor/erikwang2013/webman-scout/src/config/plugin/erikwang2013/webman-scout/into your project. - Config lives at
config/plugin/erikwang2013/webman-scout/app.php. You normally do not setSCOUT_CONFIG_KEYsoscout_config()resolves this path automatically. - Console: commands are registered via
config/plugin/erikwang2013/webman-scout/command.php(e.g.php webman scout:import "App\\Model\\Product"— adjust namespace to your app). - Models usually extend
support\Model(Eloquent-based) anduse Searchable. - Queues (optional): install/configure webman/redis-queue, set
'queue' => truein Scout config, and run consumers underapp/queue/redis/search/(scout_make,scout_remove). If Redis Queue is missing orqueueis false, indexing runs synchronously in the request/process.
Laravel (7.x – 12.x)
-
Config file: add
config/scout.phpthatreturns the same structure as this package’ssrc/config/plugin/erikwang2013/webman-scout/app.php(keys:driver,prefix,opensearch,meilisearch,queue, …).- Avoid loading both
laravel/scoutand this package under the sameconfig/scout.phpunless you know how to separate them; this package is a standalone Scout-style implementation.
- Avoid loading both
-
Environment: in
.envsetSCOUT_CONFIG_KEY=scoutsoscout_config('driver')readsconfig('scout.driver'). -
Container:
helpers.phpregistersEngineManager(and Meilisearch client when installed) on the activeapp()container. If you bootstrap before Composer’sfilesautoload, register the same bindings inAppServiceProvider::register():$this->app->singleton(\Erikwang2013\WebmanScout\EngineManager::class, function ($app) { return new \Erikwang2013\WebmanScout\EngineManager($app); });
-
Artisan commands: commands are Symfony
Commandclasses with names likescout:import. Register them with the framework:-
Laravel 10 and below — in
app/Console/Kernel.php:protected $commands = [ \Erikwang2013\WebmanScout\Command\ImportCommand::class, \Erikwang2013\WebmanScout\Command\FlushCommand::class, \Erikwang2013\WebmanScout\Command\IndexCommand::class, \Erikwang2013\WebmanScout\Command\DeleteIndexCommand::class, \Erikwang2013\WebmanScout\Command\DeleteAllIndexesCommand::class, \Erikwang2013\WebmanScout\Command\QueueImportCommand::class, \Erikwang2013\WebmanScout\Command\SyncIndexSettingsCommand::class, \Erikwang2013\WebmanScout\Command\AboutCommand::class, ];
-
Laravel 11+ — in
bootstrap/app.phpuse->withCommands([...])with the same class list (see Laravel 11 structure).
-
-
Models extend
Illuminate\Database\Eloquent\Model(or your base model) anduse Searchable. -
Queues: async indexing in this package is wired to Webman\RedisQueue when that class exists. On stock Laravel, keep
'queue' => falsein Scout config so changes are applied synchronously, or implement your own pipeline (e.g. dispatch a Laravel job from model observers) usingsyncMakeSearchable/ engineupdate()as a reference.
Hyperf (2.x – 3.x)
- Config: place the Scout array under a Hyperf config file, e.g.
config/autoload/scout.php, returning the same keys as the packageapp.php. SetSCOUT_CONFIG_KEY=scoutin the environment Hyperf reads (soconfig('scout')is the root array). config()/app(): Hyperf providesconfig(); ensure the IlluminateContaineris the one returned byapp()if you rely on packagehelpers.php, or bindEngineManagerin a HyperfConfigProvider/ dependency injection config pointing at your container bridge.- Models:
Hyperf\Database\Modelis Eloquent-compatible — useSearchablethe same way as on Laravel when the database component is configured. - Console: register the same command classes as Laravel with Hyperf’s command system (or invoke Symfony
Applicationwith these commands in a custom entry script). - Queues: same as Laravel — without
Webman\RedisQueue, preferqueue=> false or custom async jobs.
ThinkPHP (6.x / 8.x)
- Scope: the
Searchabletrait expects Eloquent (Illuminate\Database\Eloquent\Model) observers and collections. It does not attach tothink\Modelout of the box. - When it works: projects that already use
illuminate/database(or another stack) with real Eloquent models, or a bridge that exposes Laravel-styleconfig()andapp()with the Illuminate container, can follow the Laravel steps: Scout config file +SCOUT_CONFIG_KEY+EngineManagerbinding. - Pure ThinkPHP models: index data by calling
app(EngineManager::class)->engine()(or the concrete engine class)update/delete/searchwith arrays you build yourself, or maintain a thin Eloquent model mapped to the same table for search-only usage. - Config: ThinkPHP’s
config('scout.driver')works if you defineconfig/scout.php(or the version your major version uses) with the same array shape as this package’sapp.php.
Yii2 (2.x)
-
Config: put the Scout array under
'scout'inconfig/params.php, with the same keys as this package’ssrc/config/plugin/erikwang2013/webman-scout/app.php(copy it).helpers.phpprovides aconfig()polyfill that readsYii::$app->paramsvia dot notation, soscout_config('driver')resolvesparams['scout']['driver']automatically — noSCOUT_CONFIG_KEYneeded. -
Models: use real Eloquent models (
Illuminate\Database\Eloquent\Modelwithuse Searchable) booted via anilluminate/databaseCapsule; the package does not depend onYii::$app->db. -
Console: register the bridge in
config/console.php:'controllerMap' => [ 'scout' => \Erikwang2013\WebmanScout\Yii\ScoutController::class, ],
Then
yii scout/import "App\Models\Product",yii scout/flush "App\Models\Product",yii scout/index --name=posts [--key=id],yii scout/delete-index --name=posts,yii scout/queue-import,yii scout/sync-index-settings [--driver=...],yii scout/delete-all-indexes. Action params map to the Symfony options, e.g.yii scout/import "App\Models\Product" --chunk=500 --fresh=1. -
Cache/Log: engines automatically route
Support\Cache/Support\LogthroughYii::$app->cache(anyyii\caching\Cachecomponent) andYii::info|warning|errorwhen Yii2 is detected. -
Queues: same as other frameworks — keep
'queue' => false(synchronous) or implement custom async jobs.
Yii3 (3.x)
-
Config plugin: register the provider in
config-plugin.php:'providers' => [ // ... 'erikwang2013/webman-scout' => [\Erikwang2013\WebmanScout\Yii3\ScoutConfigProvider::class], ],
The provider injects default
scoutparams (the package’s webmanapp.phparray) and wires the config/cache/log seams to the container:scout_config()reads the mergedparams['scout'], cache uses the PSR-16CacheInterface, logging uses the PSR-3LoggerInterface. Override by setting your own'scout' => [...]in the app params. -
Console: register the Symfony commands in
params['yiisoft/yii-console']['commands']:'yiisoft/yii-console' => [ 'commands' => [ 'scout:import' => \Erikwang2013\WebmanScout\Command\ImportCommand::class, 'scout:queue-import' => \Erikwang2013\WebmanScout\Command\QueueImportCommand::class, 'scout:index' => \Erikwang2013\WebmanScout\Command\IndexCommand::class, 'scout:flush' => \Erikwang2013\WebmanScout\Command\FlushCommand::class, 'scout:sync-index-settings' => \Erikwang2013\WebmanScout\Command\SyncIndexSettingsCommand::class, 'scout:delete-index' => \Erikwang2013\WebmanScout\Command\DeleteIndexCommand::class, 'scout:delete-all-indexes' => \Erikwang2013\WebmanScout\Command\DeleteAllIndexesCommand::class, ], ],
-
Models: same as Yii2 — Eloquent models via an
illuminate/databaseCapsule. -
Queues: same as other frameworks —
'queue' => falseor custom async jobs.
Plain PHP (no framework)
No framework at all — CLI scripts, cron jobs, queue workers, or a custom host. Only illuminate/* (installed with this package) and your own Eloquent bootstrap are needed: helpers.php already provides app(), event() and config(), so there is no container or config repository to wire up.
require __DIR__.'/vendor/autoload.php'; use Erikwang2013\WebmanScout\Scout; use Illuminate\Database\Capsule\Manager as Capsule; // 1. Scout config: an array, or a path to a PHP file that returns one. Scout::configure([ 'driver' => 'opensearch', // or database / collection / null 'prefix' => 'app_', 'queue' => false, // no Webman Redis Queue here → stays synchronous 'opensearch' => [ 'host' => 'https://127.0.0.1:9200', 'username' => 'admin', 'password' => 'admin', ], ]); // Scout::configure(__DIR__.'/scout.php'); // same thing, from a file // Scout::configure($config, 'my-root'); // publish under a custom root key // 2. Eloquent (models, observers and the index hydration all need it). $capsule = new Capsule; $capsule->addConnection(['driver' => 'mysql', 'host' => '127.0.0.1', 'database' => 'app', 'username' => 'root', 'password' => '', 'charset' => 'utf8mb4']); $capsule->setAsGlobal(); $capsule->bootEloquent(); // 3. Search like anywhere else. $products = Product::search('phone')->where('status', 1)->paginate(15); // 4. Commands work too: `php vendor/bin/…` or Symfony's Application with the scout:* commands.
Behaviour on this path:
- Config —
Scout::configure()pins the root toscout, soscout_config('driver')andconfig('scout.driver')read your array. If the driver is missing/empty, thenullengine is used instead of silently hitting a service. - Logging — with no logger component,
Support\Logwrites toerror_log()(prefix[webman-scout]) instead of throwing. Hand it a PSR-3 logger instead:Log::setLoggerResolver(fn () => $myLogger). - Cache — with no cache component,
Support\Cachefalls back to a per-process array store (Support\ArrayStore). Hand it a real cache instead:Cache::setPsr16Resolver(fn () => $myPsr16Cache). - Paths —
helpers.phpsupplies abase_path()fallback, so the shippedapp.php(which readsbase_path('config/xunsearch')) loads on hosts that have no such helper; relativessl_cert/ssl_keypaths resolve againstbase_path()or, failing that, the current working directory. - Queue — leave
'queue' => false; the Webman Redis Queue path is skipped automatically (and logged) when the queue class is absent. - Events —
Illuminate\Events\Dispatcheris bound on the container, soModelsImported/ModelsFlushedprogress events fire andscout:importworks.
Quick checklist
| Step | Webman | Laravel | Hyperf | ThinkPHP (Eloquent/hybrid) | Yii2 | Yii3 |
|---|---|---|---|---|---|---|
| Scout config file | config/plugin/.../app.php |
config/scout.php |
config/autoload/scout.php |
config/scout.php (or equivalent) |
params['scout'] |
params['scout'] (provider-injected) |
SCOUT_CONFIG_KEY |
Usually omit | scout |
scout |
scout (if not using plugin path) |
Usually omit | Usually omit |
| Console | php webman scout:* |
php artisan scout:* (after registration) |
Hyperf command registration | Per your console setup | yii scout/* (controllerMap) |
Symfony commands in yiisoft/yii-console |
| Async indexing | Redis Queue + consumers | queue false or custom jobs |
queue false or custom jobs |
queue false or custom jobs |
queue false or custom jobs |
queue false or custom jobs |
Configuration
All Scout options are read via scout_config('key'), which respects the resolved config root above.
| Key | Purpose |
|---|---|
driver |
Default engine: opensearch, elasticsearch, meilisearch, typesense, algolia, xunsearch, database, collection, null. Add an advanced_ prefix where a variant exists (advanced_opensearch is an alias of opensearch, which already ships the advanced implementation; advanced_elasticsearch / advanced_meilisearch / advanced_typesense / advanced_xunsearch unlock aggregations, facets, highlights and vectors) |
prefix |
Index name prefix |
queue |
Enable async indexing (Webman Redis Queue when installed) |
chunk.searchable / chunk.unsearchable |
Chunk sizes for bulk import/remove |
soft_delete |
Keep soft-deleted rows in the index |
after_commit |
Defer index writes until open transactions commit (requires the host to register a database transaction manager) |
OpenSearch example
'opensearch' => [ 'host' => getenv('OPENSEARCH_HTTP_HOST') ?: 'https://127.0.0.1:6205', 'username' => getenv('OPENSEARCH_USERNAME') ?: 'admin', 'password' => getenv('OPENSEARCH_PASSWORD') ?: 'admin', 'prefix' => getenv('OPENSEARCH_INDEX_PREFIX') ?: '', // 默认校验证书;字符串形式的 'false' / '0' 也会被正确解析 'ssl_verification' => filter_var(getenv('OPENSEARCH_SSL_VERIFICATION') ?: true, FILTER_VALIDATE_BOOLEAN), 'indices' => [ 'products' => [ 'settings' => [ /* ... */ ], 'mappings' => [ 'properties' => [ 'vector' => ['type' => 'knn_vector', 'dimension' => 1536], 'location' => ['type' => 'geo_point'], ], ], ], ], ],
Model setup
use Erikwang2013\WebmanScout\Searchable; use support\Model; // Webman; use your Eloquent base otherwise class Product extends Model { use Searchable; public function searchableAs(): string { return 'products'; } public function toSearchableArray(): array { return [ 'id' => $this->id, 'title' => $this->title, 'content' => $this->content, 'price' => $this->price, 'created_at' => $this->created_at?->timestamp, 'location' => ['lat' => $this->lat, 'lon' => $this->lng], 'vector' => $this->embedding ?? [], ]; } public function searchableFields(): array { return ['title', 'content']; } }
Basic usage
// Search $products = Product::search('phone')->get(); $products = Product::search('phone', function ($builder) { $builder->where('status', 1); })->get(); $paginator = Product::search('phone')->paginate(15); Product::search('keyword') ->where('status', 1) ->whereIn('category_id', [1, 2, 3]) ->orderBy('created_at', 'desc') ->take(20) ->get(); // Indexing $product->searchableSync(); $product->searchable(); $product->unsearchable(); Product::makeAllSearchable(); Product::removeAllFromSearch(); Product::withoutSyncingToSearch(function () { Product::query()->where('id', 1)->update(['title' => 'New title']); });
Advanced builder (OpenSearch / Elasticsearch-oriented)
Product::search('') ->whereRange('created_at', ['gte' => 1609459200, 'lte' => 1640995200], true) ->whereRange('price', ['gte' => 100, 'lt' => 500]) ->get(); Product::search('') ->whereGeoDistance('location', 31.23, 121.47, 10.0) ->get(); Product::search('') ->fulltextSearch('keyword', ['title', 'content'], ['operator' => 'and']) ->get(); Product::search('') ->orderByVectorSimilarity([0.1, -0.2 /* ... */], 'vector') ->get(); $builder = Product::search('keyword') ->aggregate('price_ranges', 'range', 'price', ['ranges' => [ ['from' => 0, 'to' => 100], ['from' => 100, 'to' => 500], ]]) ->facet('category_id', ['size' => 10]); $results = $builder->get(); $aggregations = $builder->getAggregations(); $facets = $builder->getFacets(); // 引擎可以按需切换:advanced_* 驱动解锁聚合 / 分面 / 高亮 / 向量 $engine = app(\Erikwang2013\WebmanScout\EngineManager::class)->engine('advanced_elasticsearch'); $engine->updateIndexMappings('products', [ 'properties' => [ 'new_field' => ['type' => 'keyword'], ], ]); $builder = Product::search('keyword'); $builder->whereRange('created_at', $range)->get(); $builder->clearAdvancedConditions();
Artisan / Webman commands
On Webman, use php webman …. On Laravel, register the command classes (see Laravel under Framework-specific setup) and run php artisan scout:import, etc.
| Command | Description |
|---|---|
php webman scout:import [Model] |
Full import; --chunk, --fresh |
php webman scout:flush [Model] |
Clear model data from the index |
php webman scout:delete-index [Model] |
Drop index for the model |
php webman scout:index |
List / create indexes (engine-dependent) |
php webman scout:queue-import |
Queue-based import |
php webman scout:sync-index-settings |
Sync index settings |
php webman scout:delete-all-indexes |
Delete all managed indexes (dangerous) |
php webman scout:about |
Mascot + resolved config + engine availability self-check |
Use --help on each command for options.
Queues
With queue enabled, searchable() / unsearchable() dispatch to Webman Redis Queue when Webman\RedisQueue\Redis is available. Ensure consumers under app/queue/redis/search are running (e.g. scout_make, scout_remove).
The consumer classes live in the package at
src/Jobs/search/but must be copied into your project atapp/queue/redis/search/— theirapp\queue\redis\searchnamespace is project-specific and cannot be autoloaded fromvendor/. The plugin installer does this automatically; for manual installs, copysrc/Jobs/search/MakeSearchable.phpandRemoveFromSearch.phpintoapp/queue/redis/search/.
Builder reference (extensions)
| Method | Description |
|---|---|
whereRange($field, array $range, bool $inclusive = true) |
Range filter |
whereGeoDistance($field, $lat, $lng, $radius) |
Geo distance |
fulltextSearch($query, array $fields = [], array $options = []) |
Full-text |
orderByVectorSimilarity(array $vector, ?string $vectorField = null) |
Vector sort |
aggregate(...) / facet(...) |
Aggregations / facets |
addResultProcessor(callable $processor) |
Post-process hits |
getAggregations() / getFacets() |
Read facet/agg results |
clearAdvancedConditions() |
Reset advanced state |
Engines such as OpenSearch expose updateIndexMappings(string $index, array $mappings).
Mascot
Sniffy (探探) is the project mascot: a scout dog with a magnifier — it sniffs out documents, buries them in the index, and tracks them back down. The vector art lives at docs/images/pet.svg and is also the visual anchor of the architecture, feature and lifecycle diagrams.
It is wired into the package as a console command, which doubles as a self-check for environment problems:
php webman scout:about # php artisan scout:about on Laravel
__ __ ___
/ \______/ \ / \
| | | -- |
| o o | \ /
| __ | |
\ \__/ / |
\__________/
Scout · webman-scout v2.1.3 sniff out your data · 嗅出你的数据
config root 配置根 plugin.erikwang2013.webman-scout.app
driver 引擎 opensearch
prefix 索引前缀 app_
queue 队列 off(请求内同步索引)
soft delete 软删除 on(保留 __soft_deleted 文档)
after commit 事务后提交 off
chunk 分块 searchable=500 / unsearchable=500
engines 引擎可用性 (composer require 对应客户端后即可用)
algolia ✘ collection ✔ database ✔
elasticsearch ✘ meilisearch ✔ null ✔
opensearch ✔ typesense ✔ xunsearch ✘
Implementation: src/Command/AboutCommand.php (mascot art + engine availability probe).
References
- Laravel Scout
- shopwwi/webman-scout
- Review report / 审查报告 — findings, fixes and open trade-offs per round
开源不易,欢迎支持 / Support this project
| WeChat Pay 微信 | Alipay 支付宝 |
|---|---|
![]() |
![]() |
Thank you for your support! 感谢支持!
License
MIT
© erik erik@erik.xyz · https://erik.xyz

