Search by

erikwang2013 / webman-scout

erikwang2013

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.

Package info

github.com/erikwang2013/webman-scout

pkg:composer/erikwang2013/webman-scout

Statistics

Installs: 1 799

Dependents: 0

Suggesters: 0

Stars: 3

Open Issues: 0

v2.1.5 2026-09-25 16:22 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)

↑

webman-scout

Sniffy — the webman-scout mascot: a scout dog holding a magnifier over an index card

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

webman-scout architecture: host frameworks → bootstrap → model layer → builder / engine manager → engines, with cross-cutting commands, jobs, events, contracts and adapters

Three layers do the actual work, and everything else is pluggable around them:

  1. Bootstrap seam. helpers.php defines app(), event(), config() and scout_config() only when the host does not already provide them, then binds EngineManager on the Illuminate container. ScoutConfig resolves the config root once — SCOUT_CONFIG_KEY → Webman plugin path → scout → Yii params → array registered with Scout::configure() (plain PHP) — so the same package code runs on every host. Eloquent has a getenv('KEY') ?: 'default' gotcha in app.php comments; follow it.
  2. Model layer. use Searchable boots three things: a global scope, a ModelObserver, and collection macros. The observer decides whether to write and delegates how to write; SearchableScope provides chunked searchable() / unsearchable() macros for imports and emits progress events.
  3. Query + engine layer. Every read path funnels through one Builder, which hands off to EngineManager::driver(); every engine implements the same Engine contract (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

webman-scout feature map: search API, advanced query, multi-engine, indexing, commands, multi-framework

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 Searchable swap; 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_remove jobs. Missing webman/redis-queue is logged and downgraded to synchronous instead of silently dropping writes.

Lifecycle

webman-scout lifecycle: write path, read path and the four index states

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 Searchable trait
  • 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/log if you want the host logger and cache instead of the built-in error_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)

  1. Copy the contents of src/config/plugin/erikwang2013/webman-scout/app.php into your application config, e.g. config/scout.php, returning the same associative array (keys: driver, prefix, opensearch, meilisearch, …).
  2. Set environment variable SCOUT_CONFIG_KEY=scout (no trailing dot) so lookups use config('scout.driver'), etc., instead of the Webman plugin path.
  3. Ensure your bootstrap registers Illuminate’s config repository and container so config() and app() resolve EngineManager and 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)

  1. Enable the plugin in your Webman project (per Webman plugins) so that config/plugin/erikwang2013/webman-scout/ is published. If your stack runs the package Install step, it copies plugin config and queue consumers; otherwise copy from vendor/erikwang2013/webman-scout/src/config/plugin/erikwang2013/webman-scout/ into your project.
  2. Config lives at config/plugin/erikwang2013/webman-scout/app.php. You normally do not set SCOUT_CONFIG_KEY so scout_config() resolves this path automatically.
  3. 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).
  4. Models usually extend support\Model (Eloquent-based) and use Searchable.
  5. Queues (optional): install/configure webman/redis-queue, set 'queue' => true in Scout config, and run consumers under app/queue/redis/search/ (scout_make, scout_remove). If Redis Queue is missing or queue is false, indexing runs synchronously in the request/process.

Laravel (7.x – 12.x)

  1. Config file: add config/scout.php that returns the same structure as this package’s src/config/plugin/erikwang2013/webman-scout/app.php (keys: driver, prefix, opensearch, meilisearch, queue, …).

    • Avoid loading both laravel/scout and this package under the same config/scout.php unless you know how to separate them; this package is a standalone Scout-style implementation.
  2. Environment: in .env set SCOUT_CONFIG_KEY=scout so scout_config('driver') reads config('scout.driver').

  3. Container: helpers.php registers EngineManager (and Meilisearch client when installed) on the active app() container. If you bootstrap before Composer’s files autoload, register the same bindings in AppServiceProvider::register():

    $this->app->singleton(\Erikwang2013\WebmanScout\EngineManager::class, function ($app) {
        return new \Erikwang2013\WebmanScout\EngineManager($app);
    });
  4. Artisan commands: commands are Symfony Command classes with names like scout: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.php use ->withCommands([...]) with the same class list (see Laravel 11 structure).

  5. Models extend Illuminate\Database\Eloquent\Model (or your base model) and use Searchable.

  6. Queues: async indexing in this package is wired to Webman\RedisQueue when that class exists. On stock Laravel, keep 'queue' => false in Scout config so changes are applied synchronously, or implement your own pipeline (e.g. dispatch a Laravel job from model observers) using syncMakeSearchable / engine update() as a reference.

Hyperf (2.x – 3.x)

  1. Config: place the Scout array under a Hyperf config file, e.g. config/autoload/scout.php, returning the same keys as the package app.php. Set SCOUT_CONFIG_KEY=scout in the environment Hyperf reads (so config('scout') is the root array).
  2. config() / app(): Hyperf provides config(); ensure the Illuminate Container is the one returned by app() if you rely on package helpers.php, or bind EngineManager in a Hyperf ConfigProvider / dependency injection config pointing at your container bridge.
  3. Models: Hyperf\Database\Model is Eloquent-compatible — use Searchable the same way as on Laravel when the database component is configured.
  4. Console: register the same command classes as Laravel with Hyperf’s command system (or invoke Symfony Application with these commands in a custom entry script).
  5. Queues: same as Laravel — without Webman\RedisQueue, prefer queue => false or custom async jobs.

ThinkPHP (6.x / 8.x)

  1. Scope: the Searchable trait expects Eloquent (Illuminate\Database\Eloquent\Model) observers and collections. It does not attach to think\Model out of the box.
  2. When it works: projects that already use illuminate/database (or another stack) with real Eloquent models, or a bridge that exposes Laravel-style config() and app() with the Illuminate container, can follow the Laravel steps: Scout config file + SCOUT_CONFIG_KEY + EngineManager binding.
  3. Pure ThinkPHP models: index data by calling app(EngineManager::class)->engine() (or the concrete engine class) update / delete / search with arrays you build yourself, or maintain a thin Eloquent model mapped to the same table for search-only usage.
  4. Config: ThinkPHP’s config('scout.driver') works if you define config/scout.php (or the version your major version uses) with the same array shape as this package’s app.php.

Yii2 (2.x)

  1. Config: put the Scout array under 'scout' in config/params.php, with the same keys as this package’s src/config/plugin/erikwang2013/webman-scout/app.php (copy it). helpers.php provides a config() polyfill that reads Yii::$app->params via dot notation, so scout_config('driver') resolves params['scout']['driver'] automatically — no SCOUT_CONFIG_KEY needed.

  2. Models: use real Eloquent models (Illuminate\Database\Eloquent\Model with use Searchable) booted via an illuminate/database Capsule; the package does not depend on Yii::$app->db.

  3. 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.

  4. Cache/Log: engines automatically route Support\Cache / Support\Log through Yii::$app->cache (any yii\caching\Cache component) and Yii::info|warning|error when Yii2 is detected.

  5. Queues: same as other frameworks — keep 'queue' => false (synchronous) or implement custom async jobs.

Yii3 (3.x)

  1. Config plugin: register the provider in config-plugin.php:

    'providers' => [
        // ...
        'erikwang2013/webman-scout' => [\Erikwang2013\WebmanScout\Yii3\ScoutConfigProvider::class],
    ],

    The provider injects default scout params (the package’s webman app.php array) and wires the config/cache/log seams to the container: scout_config() reads the merged params['scout'], cache uses the PSR-16 CacheInterface, logging uses the PSR-3 LoggerInterface. Override by setting your own 'scout' => [...] in the app params.

  2. 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,
        ],
    ],
  3. Models: same as Yii2 — Eloquent models via an illuminate/database Capsule.

  4. Queues: same as other frameworks — 'queue' => false or 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 to scout, so scout_config('driver') and config('scout.driver') read your array. If the driver is missing/empty, the null engine is used instead of silently hitting a service.
  • Logging — with no logger component, Support\Log writes to error_log() (prefix [webman-scout]) instead of throwing. Hand it a PSR-3 logger instead: Log::setLoggerResolver(fn () => $myLogger).
  • Cache — with no cache component, Support\Cache falls back to a per-process array store (Support\ArrayStore). Hand it a real cache instead: Cache::setPsr16Resolver(fn () => $myPsr16Cache).
  • Paths — helpers.php supplies a base_path() fallback, so the shipped app.php (which reads base_path('config/xunsearch')) loads on hosts that have no such helper; relative ssl_cert / ssl_key paths resolve against base_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\Dispatcher is bound on the container, so ModelsImported / ModelsFlushed progress events fire and scout:import works.

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 at app/queue/redis/search/ — their app\queue\redis\search namespace is project-specific and cannot be autoloaded from vendor/. The plugin installer does this automatically; for manual installs, copy src/Jobs/search/MakeSearchable.php and RemoveFromSearch.php into app/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

开源不易,欢迎支持 / Support this project

WeChat Pay 微信 Alipay 支付宝
WeChat Pay 微信 Alipay 支付宝

Thank you for your support! 感谢支持!

License

MIT

© erik erik@erik.xyz · https://erik.xyz