erikwang2013 / webman-scout
Scout-style full-text search for Webman, Laravel, ThinkPHP, and Hyperf (Eloquent models, multiple engines).
Requires
- php: ^8.0
- illuminate/bus: ^7.0|^8.0|^9.0|^10.0|^11.0
- illuminate/contracts: ^7.0|^8.0|^9.0|^10.0|^11.0
- illuminate/database: ^7.0|^8.0|^9.0|^10.0|^11.0
- illuminate/http: ^7.0|^8.0|^9.0|^10.0|^11.0
- illuminate/pagination: ^7.0|^8.0|^9.0|^10.0|^11.0
- illuminate/queue: ^7.0|^8.0|^9.0|^10.0|^11.0
- illuminate/support: ^7.0|^8.0|^9.0|^10.0|^11.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
- 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).
- meilisearch/meilisearch-php: Required to use the Meilisearch engine (^1.0).
- typesense/typesense-php: Required to use the Typesense engine (^4.9).
Conflicts
- algolia/algoliasearch-client-php: <3.2.0|>=5.0.0
README
Navigate (top): Top · Features · Framework support · Requirements · Installation · Framework-specific setup · Configuration · Model setup · Basic usage · Advanced builder · Artisan / Webman commands · Queues · Builder reference · References · License
Left navigation (outline)
webman-scout
Driver-based full-text search for Eloquent models, inspired by Laravel Scout and shopwwi/webman-scout. This fork adds time ranges, aggregations, OpenSearch, vector / geo helpers, and clearer multi-framework configuration.
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
- Optional queue-driven indexing (Webman Redis Queue when available)
- Index settings sync, soft deletes, chunked import
Framework support
Runtime integration targets applications that expose Laravel’s config() helper and an Illuminate container (app()), with Eloquent (Illuminate\Database\Eloquent\Model) models.
| Framework | Versions | Notes |
|---|---|---|
| Webman | 1.x / 2.x | Default install: plugin config under config/plugin/erikwang2013/webman-scout/. |
| Laravel | 7.x – 11.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. |
Composer requires illuminate/* ^7.0 – ^11.0 and symfony/console ^5.4 – ^7.0 so dependency resolution matches your framework stack.
Requirements
- PHP ^8.0
- Eloquent models for the
Searchabletrait illuminate/bus,contracts,database,http,pagination,queue,support(versions aligned with your Laravel / Hyperf / ThinkPHP stack)
Installation
composer require erikwang2013/webman-scout
The Composer autoload files entry loads helpers.php, which defines app(), event(), and scout_config() when needed and registers EngineManager.
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 – 11.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, ];
-
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.
Quick checklist
| Step | Webman | Laravel | Hyperf | ThinkPHP (Eloquent/hybrid) |
|---|---|---|---|---|
| Scout config file | config/plugin/.../app.php |
config/scout.php |
config/autoload/scout.php |
config/scout.php (or equivalent) |
SCOUT_CONFIG_KEY |
Usually omit | scout |
scout |
scout (if not using plugin path) |
| Console | php webman scout:* |
php artisan scout:* (after registration) |
Hyperf command registration | Per your console setup |
| Async indexing | Redis Queue + consumers | 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, database, collection, null, … |
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 |
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') ?: '', 'ssl_verification' => (bool) (getenv('OPENSEARCH_SSL_VERIFICATION') ?: false), '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') ->limit(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(); $engine = app(\Erikwang2013\WebmanScout\EngineManager::class)->engine(); $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) |
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).
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).
References
License
MIT