PHP code example of monkeyscloud / monkeyslegion-search

1. Go to this page and download the library: Download monkeyscloud/monkeyslegion-search library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

monkeyscloud / monkeyslegion-search example snippets



declare(strict_types=1);

namespace App\Entity;

use MonkeysLegion\Entity\Attributes\{Entity, Field, Id, ObservedBy};
use MonkeysLegion\Search\Attributes\{Searchable, SearchField, SearchIndex};
use MonkeysLegion\Search\Observers\SearchObserver;
use MonkeysLegion\Search\Traits\SearchableTrait;

#[Entity(table: 'products')]
#[Searchable(index: 'products', engine: 'default', autoSync: true)]
#[ObservedBy(SearchObserver::class)]
#[SearchIndex(
    stopWords: ['the', 'a', 'an'],
    synonyms: ['phone' => ['mobile', 'cell', 'smartphone']],
)]
class Product
{
    use SearchableTrait;

    #[Id]
    #[Field(type: 'unsignedBigInt', autoIncrement: true)]
    public private(set) int $id;

    #[Field(type: 'string', length: 255)]
    #[SearchField(searchable: true, filterable: true, sortable: true, weight: 3)]
    public string $name;

    #[Field(type: 'text')]
    #[SearchField(searchable: true, weight: 1)]
    public string $description;

    #[Field(type: 'decimal', precision: 10, scale: 2)]
    #[SearchField(filterable: true, sortable: true)]
    public string $price;

    #[Field(type: 'string', length: 100)]
    #[SearchField(filterable: true, facetable: true)]
    public string $category;

    #[Field(type: 'boolean')]
    #[SearchField(filterable: true)]
    public bool $in_stock;

    #[Field(type: 'string', length: 20)]
    public string $status = 'published';

    // Control exactly what gets indexed
    public function toSearchableArray(): array
    {
        return [
            'id'          => $this->id,
            'name'        => $this->name,
            'description' => $this->description,
            'price'       => (float) $this->price,
            'category'    => $this->category,
            'in_stock'    => $this->in_stock,
        ];
    }

    // Only index published products
    public function shouldBeSearchable(): bool
    {
        return $this->status === 'published';
    }
}

use MonkeysLegion\Search\SearchManager;

final class ProductController
{
    public function __construct(
        private readonly SearchManager $search,
    ) {}

    public function index(ServerRequestInterface $request): Response
    {
        $q = $request->getQueryParams()['q'] ?? '';

        $results = $this->search->index('products')
            ->query($q)
            ->where('in_stock', '=', true)
            ->whereBetween('price', 10.0, 500.0)
            ->facet('category', 'brand')
            ->sortBy('price', SortDirection::Asc)
            ->highlight('name', 'description')
            ->page(1, perPage: 20)
            ->get();

        // $results->total      — Total matching documents
        // $results->hits       — list<SearchHit> on this page
        // $results->facets     — list<Facet> with value counts
        // $results->lastPage   — Total pages (property hook)
        // $results->hasMore    — More pages? (property hook)
        // $results->took       — Query time in ms

        return new Response(json: [
            'data'   => array_map(fn($h) => $h->document, $results->hits),
            'total'  => $results->total,
            'facets' => $results->facets,
        ]);
    }
}

// Static search — returns a Builder for the entity's index
$results = Product::search('wireless headphones')
    ->where('category', '=', 'electronics')
    ->page(1)
    ->get();

// Index a single entity
$product->searchable();

// Remove from index
$product->unsearchable();

// Bulk index multiple entities
Product::makeSearchable($products);

// Bulk remove
Product::makeUnsearchable($products);

#[Entity(table: 'products')]
#[Searchable(autoSync: true)]
#[ObservedBy(SearchObserver::class)]
class Product { use SearchableTrait; }

// Register once at boot
LifecycleDispatcher::registerSubscriber(SearchSubscriber::class);

// Enable per-entity
#[Searchable(autoSync: true, queue: true)]
class Product { ... }

// Or dispatch jobs manually
use MonkeysLegion\Search\Jobs\{IndexDocumentJob, DeleteDocumentJob, BulkIndexJob};

$dispatcher->dispatch(new IndexDocumentJob('products', '42', $document));
$dispatcher->dispatch(new DeleteDocumentJob('products', '42'));
$dispatcher->dispatch(new BulkIndexJob('products', $documents));

$results = $search->index('restaurants')
    ->query('pizza')
    ->near(latitude: 40.7128, longitude: -74.0060, geoField: 'location')
    ->withinRadius(distanceKm: 5.0)
    ->sortByDistance(latitude: 40.7128, longitude: -74.0060)
    ->get();

// Each hit 

$results = $search->index('orders')
    ->query('*')
    ->aggregate('total_revenue', 'sum', 'amount')
    ->aggregate('avg_order', 'avg', 'amount')
    ->aggregate('price_ranges', 'histogram', 'price', ['interval' => 50])
    ->aggregate('orders_per_day', 'date_histogram', 'created_at', ['interval' => 'day'])
    ->aggregate('unique_customers', 'cardinality', 'customer_id')
    ->get();

foreach ($results->aggregations as $agg) {
    echo "{$agg->name}: {$agg->value}\n";           // Scalar aggregations
    foreach ($agg->buckets as $key => $count) {       // Bucket aggregations
        echo "  {$key}: {$count}\n";
    }
}

// Via SearchManager
$suggestions = $search->suggest('products', 'wirel', limit: 5);
// Returns: [Suggestion(text: 'wireless headphones'), Suggestion(text: 'wireless mouse')]

// Via Builder
$results = $search->index('products')
    ->suggest('wirel', limit: 5)
    ->get();

foreach ($results->suggestions as $s) {
    echo "{$s->text} (score: {$s->score})\n";
}

use MonkeysLegion\Search\Contracts\SearchScopeInterface;
use MonkeysLegion\Search\Query\Builder;

final class ActiveProductsScope implements SearchScopeInterface
{
    public function apply(Builder $builder): void
    {
        $builder
            ->where('status', '=', 'active')
            ->where('in_stock', '=', true);
    }
}

final class PriceRangeScope implements SearchScopeInterface
{
    public function __construct(
        private readonly float $min,
        private readonly float $max,
    ) {}

    public function apply(Builder $builder): void
    {
        $builder->whereBetween('price', $this->min, $this->max);
    }
}

// Use — compose multiple scopes
$results = $search->index('products')
    ->query('headphones')
    ->scope(new ActiveProductsScope())
    ->scope(new PriceRangeScope(20.0, 200.0))
    ->get();

use MonkeysLegion\Search\Middleware\{LoggingMiddleware, AnalyticsMiddleware};

// Register middleware
$search->pushMiddleware(new LoggingMiddleware($logger));
$search->pushMiddleware($analytics = new AnalyticsMiddleware());

// Execute through pipeline
$result = $search->search($query);

// Analytics data
$analytics->popularTerms(20);      // ['laptop' => 42, 'phone' => 38, ...]
$analytics->zeroResultTerms();     // ['nonexistent', 'typo']
$analytics->averageQueryTime();    // 12.5 (ms)

use MonkeysLegion\Search\Middleware\SearchMiddlewareInterface;

final class CachingMiddleware implements SearchMiddlewareInterface
{
    public function handle(SearchQuery $query, callable $next): SearchResult
    {
        $key = md5(serialize($query));
        return $this->cache->remember($key, 60, fn() => $next($query));
    }
}

$results = $search->multiIndex(['products', 'articles', 'categories'])
    ->query('laptop')
    ->page(1, perPage: 20)
    ->get();

// Each hit 

// Via SearchManager
$results = $search->raw('products', [
    'query' => [
        'function_score' => [
            'query' => ['match' => ['name' => 'laptop']],
            'functions' => [
                ['field_value_factor' => ['field' => 'popularity']],
            ],
        ],
    ],
], engine: 'opensearch');

// Via engine directly
$results = $search->engine('meilisearch')->raw('products', [
    'q'      => 'laptop',
    'limit'  => 5,
    'filter' => 'price > 100',
]);

// Process millions of documents without loading all into memory
foreach ($search->index('logs')->query('error')->cursor(chunkSize: 100) as $hit) {
    processLogEntry($hit->document);
}

use MonkeysLegion\Search\Index\Reindexer;

$reindexer = new Reindexer($search->engine());

// Basic reindex
$total = $reindexer->reindex(
    indexName: 'products',
    dataProvider: fn(int $offset, int $limit) => $repo->findChunk($offset, $limit),
    chunkSize: 500,
    onProgress: fn(int $indexed, int $total) => $output->writeln("{$indexed}/{$total}"),
    totalCount: $repo->count(),
);

// Zero-downtime reindex (OpenSearch/Elasticsearch)
$reindexer->reindex(
    indexName: 'products',
    dataProvider: $provider,
    useAlias: true,  // Creates products_v{timestamp} → atomic alias swap
);

// From entity class
$reindexer->reindexEntity(
    entityClass: Product::class,
    entityLoader: fn(int $offset, int $limit) => $repo->findAll($offset, $limit),
    chunkSize: 500,
);

$results = $search->index('products')
    ->query('comfortable office chair')
    ->vectorQuery(
        vector: $embeddingService->embed('comfortable office chair'),
        vectorField: 'embedding',
        hybridWeight: 0.6,  // 0.0 = pure BM25, 1.0 = pure vector
    )
    ->page(1, perPage: 10)
    ->get();

// Sync a single entity
$search->syncIndex(Product::class);

// Sync multiple entities
$search->syncAll([Product::class, Article::class, User::class]);