Skip to content

Conversation

@binaryfire
Copy link
Contributor

Summary

This PR introduces Hypervel Scout, a full-text search package for Eloquent models ported from Laravel Scout and adapted for Hypervel's Swoole-based coroutine environment.

Key Features

Search Engines

  • Meilisearch - Production-ready full-text search with typo-tolerance and instant search
  • Typesense - Fast, typo-tolerant search engine with automatic schema management
  • Database - LIKE queries and native full-text search (MySQL FULLTEXT, PostgreSQL tsvector) with no external dependencies
  • Collection - In-memory filtering for testing
  • Null - Disabled search for environments where search isn't needed

Core Functionality

  • Automatic indexing - Models are indexed on save and removed on delete via model callbacks
  • Batch operations - searchable() and unsearchable() macros on query builders and collections
  • Soft delete support - Index and search soft-deleted records with withTrashed() / onlyTrashed()
  • Fluent search builder - where(), whereIn(), whereNotIn(), orderBy(), take(), pagination
  • Custom keys - Use UUIDs, ULIDs, or any unique identifier via getScoutKey() / getScoutKeyName()
  • Conditional indexing - Control what gets indexed via shouldBeSearchable()
  • Search callbacks - Customize queries with callbacks and raw result access

Hypervel-Specific Adaptations

Laravel Scout Hypervel Scout Rationale
ModelObserver class with static state registerCallback() with Context Coroutine-safe state management
Queues or synchronous execution Coroutine::defer() by default Non-blocking indexing after response
Sequential command processing WaitConcurrent parallel execution Leverages Swoole coroutines for fast imports
Static $syncingDisabledFor Context::set() / Context::get() Per-coroutine isolation
config('scout.queue') boolean queue.enabled + queue.after_commit Explicit control with transaction safety
Process-level engine caching via Manager Static $engines array Worker-level caching for Swoole

Additional Features Not in Laravel Scout

  • SearchableInterface contract - Explicit interface for type-safe searchable models
  • command_concurrency config - Control parallel coroutine count during bulk imports (default: 50)

Features Not Ported from Laravel Scout

  • Algolia engine - Meilisearch and Typesense cover the same use cases as open-source alternatives
  • scout:queue-import command - Replaced by WaitConcurrent parallel processing in standard import
  • MakeRangeSearchable job - Range-based queue import replaced by concurrent coroutine processing

Architecture

src/scout/
├── Attributes/               # PHP 8 attributes for database engine
│   ├── SearchUsingFullText.php
│   └── SearchUsingPrefix.php
├── Console/                  # Artisan commands
│   ├── DeleteAllIndexesCommand.php
│   ├── DeleteIndexCommand.php
│   ├── FlushCommand.php
│   ├── ImportCommand.php
│   ├── IndexCommand.php
│   └── SyncIndexSettingsCommand.php
├── Contracts/                # Interfaces
│   ├── PaginatesEloquentModels.php
│   ├── PaginatesEloquentModelsUsingDatabase.php
│   ├── SearchableInterface.php
│   └── UpdatesIndexSettings.php
├── Engines/                  # Search engine implementations
│   ├── CollectionEngine.php
│   ├── DatabaseEngine.php
│   ├── MeilisearchEngine.php
│   ├── NullEngine.php
│   └── TypesenseEngine.php
├── Events/                   # Import/flush events
├── Exceptions/               # Scout-specific exceptions
├── Jobs/                     # Queue jobs for async indexing
├── Builder.php               # Fluent search query builder
├── Engine.php                # Abstract engine base class
├── EngineManager.php         # Engine factory and resolution
├── Scout.php                 # Utility class for job customization
├── Searchable.php            # Model trait with event handling
├── SearchableScope.php       # Global scope for batch macros
├── ScoutServiceProvider.php  # Service provider
└── config/scout.php          # Configuration

Usage

Basic Setup

use Hypervel\Database\Eloquent\Model;
use Hypervel\Scout\Contracts\SearchableInterface;
use Hypervel\Scout\Searchable;

class Post extends Model implements SearchableInterface
{
    use Searchable;

    public function toSearchableArray(): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
        ];
    }
}

Searching

// Basic search
$posts = Post::search('query')->get();

// With filters and ordering
$posts = Post::search('query')
    ->where('status', 'published')
    ->whereIn('category_id', [1, 2, 3])
    ->orderBy('created_at', 'desc')
    ->paginate(15);

Commands

php artisan scout:import "App\Models\Post"      # Import with parallel coroutines
php artisan scout:import "App\Models\Post" --fresh  # Flush and reimport
php artisan scout:flush "App\Models\Post"       # Remove all from index
php artisan scout:sync-index-settings           # Sync Meilisearch settings

Configuration

Key configuration options in config/scout.php:

return [
    'driver' => env('SCOUT_DRIVER', 'collection'),
    'prefix' => env('SCOUT_PREFIX', ''),

    'queue' => [
        'enabled' => env('SCOUT_QUEUE', false),
        'after_commit' => env('SCOUT_AFTER_COMMIT', false),
    ],

    'command_concurrency' => env('SCOUT_COMMAND_CONCURRENCY', 50),
    'soft_delete' => env('SCOUT_SOFT_DELETE', false),

    'meilisearch' => [
        'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
        'key' => env('MEILISEARCH_KEY'),
        'index-settings' => [
            // Per-model settings
        ],
    ],

    'typesense' => [
        'client-settings' => [...],
        'model-settings' => [
            // Per-model collection schema and search parameters
        ],
    ],
];

Engine-Specific Helpers

MeilisearchEngine includes a generateTenantToken() convenience method for multi-tenant applications. This implements Meilisearch's recommended multi-tenancy pattern where all tenants share a single index and tenant tokens enforce data isolation at query time:

$engine = app(EngineManager::class)->engine('meilisearch');
$token = $engine->generateTenantToken([
    'posts' => ['filter' => "tenant_id = {$tenantId}"]
]);
// Use token for frontend-direct search with scoped access

This is a driver-specific helper, not a core Scout feature.

Testing Considerations

  • Use collection driver for unit tests (in-memory, no external dependencies)
  • Use null driver to disable search entirely in test environments
  • withoutSyncingToSearch() is coroutine-safe for isolating test cases
  • Engine instances are statically cached - use EngineManager::forgetEngines() to reset between tests

Changes vs Laravel Scout

  1. Models must implement SearchableInterface in addition to using the Searchable trait. Required for proper type safety / PHPStan level 5
  2. Queue configuration uses queue.enabled instead of a simple queue boolean
  3. No Algolia support - use Meilisearch or Typesense instead
  4. syncWithSearchUsing() returns null by default instead of falling back to default queue connection

Related Documentation

  • Configuration: src/scout/config/scout.php

Create the initial Scout package with directory structure, composer.json
with meilisearch-php dependency, and abstract Engine class for search
engine implementations.
Define the public API contract for searchable models, including methods for
searching, indexing, and managing scout metadata.
Manages search engine instances using static caching for process-global
reuse. Engines hold no request state and are safe to share across coroutines.
Supports Meilisearch, Collection, and Null drivers with extensibility
for custom drivers.
Fluent query builder for searchable models with support for filters
(where, whereIn, whereNotIn), ordering, soft deletes, pagination,
and lazy collections.
Provides full-text search capabilities to Eloquent models using
registerCallback() for lifecycle hooks and Context API for coroutine-safe
sync disabling. Uses Coroutine::defer() for async indexing after response.
SearchableScope adds searchable/unsearchable macros to the query builder
for chunked batch operations. Events (ModelsImported, ModelsFlushed) are
dispatched when models are indexed or flushed.
Includes driver, prefix, queue, chunk sizes, concurrency, soft delete
settings, and Meilisearch configuration. Defaults to Coroutine::defer()
for async indexing.
Registers EngineManager and MeilisearchClient, merges config, registers
publishable config, and registers console commands.
No-op engine implementation for testing or temporarily disabling search
without code changes.
Database-backed engine that filters results in memory. Useful for testing
without requiring an external search service.
Full Meilisearch integration with search, pagination, index management,
and generateTenantToken() helper for secure frontend direct search.
Handles soft delete metadata filtering.
MakeSearchable and RemoveFromSearch jobs for queue-based indexing.
RemoveableScoutCollection preserves Scout keys for already-deleted models.
- Use Hyperf\Paginator classes instead of non-existent Hypervel pagination
- Use Hyperf\Tappable\tap function import (existing Hypervel pattern)
- Remove use function imports for global helpers (class_uses_recursive, collect)
- Fix console commands to use App\Models namespace convention
- Register Hypervel\Scout namespace in autoload psr-4
- Add hypervel/scout to replace section
- Add ScoutServiceProvider to hypervel providers
- Add meilisearch/meilisearch-php dependency
- Fix SearchableTestModel property types
- Add comprehensive NullEngineTest with 12 test cases
- Create ScoutTestCase with RefreshDatabase and RunTestsInCoroutine
- Add SearchableModel and SoftDeletableSearchableModel test models
- Add migrations for test tables
- Write CollectionEngineTest with full feature tests
- Fix SearchableScope to use Hyperf\Database\Model\Scope interface
- Fix Searchable trait Collection type for makeSearchableUsing
Tests update, delete, search, paginate, map, mapIds, flush,
createIndex, deleteIndex, updateIndexSettings, and soft delete
metadata handling.
Tests query building, where/whereIn/whereNotIn constraints, ordering,
soft delete handling, pagination, macros, and engine integration.
Tests engine resolution, static caching, custom drivers, default
driver handling, and cache clearing.
Tests search builder creation, searchable array, scout keys, sync
disabling, soft delete handling, and metadata management.
Tests Context isolation for sync disabling across concurrent
coroutines, including nested coroutines and withoutSyncingToSearch.
Callbacks:
- Add searchIndexShouldBeUpdated() check in saved callback
- Add wasSearchableBeforeUpdate() check before unsearchable()
- Add wasSearchableBeforeDelete() check in deleted callback
- Add forceDeleted callback for force-deleted models
- Add restored callback (forced update, no searchIndexShouldBeUpdated check)

Queue dispatch:
- Implement proper queue dispatch using MakeSearchable::dispatch()
- Use onConnection() and onQueue() from model configuration

Macros:
- Fix collection macros to use $this->first() instead of captured $self
- Remove unused $chunk parameter from collection macros (matches Laravel)

Builder:
- Add simplePaginateRaw() method for Laravel API parity
- Add Arrayable support in whereIn/whereNotIn methods

Tests:
- Add tests for simplePaginateRaw, Arrayable support
- Add tests for default return values of lifecycle methods
When queue.after_commit is enabled, Scout jobs are dispatched only
after database transactions commit, preventing indexing of data that
might be rolled back.

Changes:
- Add queue.after_commit config option (default false)
- Chain ->afterCommit() on MakeSearchable/RemoveFromSearch jobs when enabled
- Update README with queue configuration documentation
- Add QueueDispatchTest with 8 test cases for queue behavior
- Fix test config to use queue.after_commit (was at root level)
- Add TypesenseClient binding in ScoutServiceProvider (matches Meilisearch pattern)
- Update EngineManager to resolve TypesenseClient from container
- Improves testability and allows apps to override client configuration
SearchUsingPrefix attribute tests:
- Verify prefix columns use 'query%' pattern instead of '%query%'
- Verify non-prefix columns still use full wildcard
- Add PrefixSearchableModel test fixture

SearchableScope macro tests:
- Verify searchable() dispatches ModelsImported event
- Verify unsearchable() dispatches ModelsFlushed event
- Verify shouldBeSearchable() filtering is applied
- Verify custom chunk sizes work correctly
- Verify query constraints are respected
- Add ConditionalSearchableModel test fixture
The test claimed to verify that shouldBeSearchable() filtering works,
but only asserted that the event contained all models. Now it actually
verifies that only the 2 visible models (not the hidden one) appear
in search results after the searchable() macro runs.
- Add tests/bootstrap.php for .env loading
- Add .env.example with Meilisearch/Typesense settings
- Add MeilisearchIntegrationTestCase base class with parallel-safe prefixes
- Add TypesenseIntegrationTestCase base class with parallel-safe prefixes
- Update phpunit.xml.dist with bootstrap reference
- Update workflow: exclude integration group from main job, add dedicated jobs
- Add .env to .gitignore
- MeilisearchEngineIntegrationTest: 13 tests for core CRUD, search, pagination
- MeilisearchFilteringIntegrationTest: 6 tests for where/whereIn/whereNotIn
- MeilisearchSortingIntegrationTest: 4 tests for orderBy operations
- TypesenseEngineIntegrationTest: 10 tests for core operations
- TypesenseFilteringIntegrationTest: 6 tests for filtering operations
- TypesenseSearchableModel with typesenseCollectionSchema() and typesenseSearchParameters()
- Updated base test cases to use setUpInCoroutine() for HTTP client initialization
The SCOUT_COMMAND path was using Concurrent::create() to spawn
coroutines but never waited for them. Since console commands don't
run inside a coroutine runtime by default, those coroutines were
orphaned when the command exited.

Changes:
- Use WaitConcurrent instead of Concurrent for proper wait() support
- ImportCommand wraps execution in run() when not already in coroutine
- Add waitForSearchableJobs() called in finally block for cleanup
- Add coroutine context checks with clear error messages
- Restore concurrency config option for command parallelism control
- Add command and soft-delete integration tests
- Remove RunTestsInCoroutine from base test cases in tests/Support
- Add initializeMeilisearch/initializeTypesense methods for flexible client init
- Scout test cases extend base classes and add RunTestsInCoroutine
- Command tests use same test case (base Command wraps in coroutine)
- Remove unnecessary run() wrapper from ImportCommand
- Delete redundant command-specific test case files
- Rename concurrency to command_concurrency (explicit command-specific)
- Fix TypesenseEngine to enforce maxTotalResults in search() and paginate()
- Add ConfigTest for command_concurrency, chunk.searchable/unsearchable
- Add MeilisearchIndexSettingsIntegrationTest for sync-index-settings
- Add TypesenseConfigIntegrationTest for model-settings, max_total_results, import_action
- Add ConfigBasedTypesenseModel for testing config-based Typesense settings
Remove unnecessary coroutine context checks - let code fail naturally
if called outside expected context rather than adding explicit guards.
- Add scout:delete-all-indexes command for deleting all search indexes
- Add PaginatesEloquentModels contract for custom engine pagination
- Update Builder to check for pagination contracts before fallback
- Add unit tests for DeleteAllIndexesCommand
- Add integration test for delete-all-indexes with Meilisearch
- Register new command in ScoutServiceProvider and test cases
- Add tests verifying Builder delegates to PaginatesEloquentModels interface
- Add tests verifying Builder delegates to PaginatesEloquentModelsUsingDatabase interface
- Fix import ordering in Builder.php (php-cs-fixer)
ImportCommand tests:
- Model class resolution with fully qualified names
- Model not found exception handling
- App\Models namespace fallback resolution

SyncIndexSettingsCommand tests:
- Engine not supporting UpdatesIndexSettings returns failure
- No index settings configured returns success with info
- Settings sync success flow
- Driver option override
- Index name prefix resolution logic
Introduces Scout utility class with:
- Static $makeSearchableJob and $removeFromSearchJob properties
- makeSearchableUsing() and removeFromSearchUsing() setters
- engine() convenience method for engine access
- resetJobClasses() for test isolation

Updates Searchable trait to dispatch jobs via Scout::$makeSearchableJob
and Scout::$removeFromSearchJob, enabling users to customize job classes
for logging, monitoring, retry behavior, etc.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces Hypervel Scout, a comprehensive full-text search package adapted from Laravel Scout for Hypervel's Swoole-based coroutine environment. The package provides multiple search engine implementations (Meilisearch, Typesense, Database, Collection, Null) with automatic indexing, batch operations, soft delete support, and a fluent search builder API.

Key Changes:

  • Full-text search infrastructure with coroutine-safe state management using Context API
  • Five search engine implementations with different use cases
  • Test infrastructure supporting parallel test execution with unique prefixes
  • Queue-based or deferred indexing with configurable concurrency
  • Comprehensive test suite including unit, feature, and integration tests

Reviewed changes

Copilot reviewed 90 out of 91 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/bootstrap.php Test bootstrap for loading .env configuration
tests/Support/*.php Base test cases for Meilisearch and Typesense integration tests with parallel safety
tests/Scout/migrations/*.php Database migrations for test models
tests/Scout/Models/*.php Test model implementations for various scenarios
tests/Scout/Unit/*.php Unit tests for core Scout functionality
tests/Scout/Feature/*.php Feature tests for search engines and coroutine safety
tests/Scout/Integration/*.php Integration tests for Meilisearch and Typesense
src/scout/src/Searchable.php Core trait providing search capabilities with coroutine-safe state
src/scout/src/SearchableScope.php Global scope adding batch search macros to query builder
src/scout/src/Scout.php Utility class for job customization and engine access
src/scout/src/Jobs/*.php Queue job implementations for async indexing
src/scout/src/Exceptions/*.php Scout-specific exception classes
src/scout/src/Events/*.php Event classes for import/flush operations
src/scout/src/ScoutServiceProvider.php Service provider for Scout registration

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@albertcht albertcht added the feature New feature or request label Jan 8, 2026
@albertcht albertcht requested a review from Copilot January 8, 2026 11:27
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 90 out of 91 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@albertcht albertcht assigned Copilot and unassigned Copilot Jan 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants