# Explorer - Laravel Scout Elasticsearch Driver ## Introduction Explorer is a next-generation Elasticsearch driver for Laravel Scout that unlocks the full power of Elasticsearch's query DSL within Laravel applications. While vanilla Laravel Scout limits developers to basic fuzzy term searches and simple ID lookups, Explorer provides complete access to Elasticsearch's advanced querying capabilities through an elegant, fluent PHP interface. The package seamlessly integrates with Laravel's existing Scout ecosystem while dramatically expanding search functionality with support for complex boolean queries, nested documents, aggregations, custom scoring, and fine-grained field mapping. The driver is built on a clean architecture separating domain logic (query syntax), application interfaces, and infrastructure implementations. It supports zero-downtime index updates through alias management, provides comprehensive debugging tools, and offers flexible configuration options for both model-based and config-based index definitions. Explorer maintains full compatibility with Laravel Scout's standard methods while adding powerful query building capabilities that enable developers to leverage Elasticsearch's complete feature set without writing raw query arrays. --- ## APIs and Functions ### Basic Search with Query Builder Perform full-text searches with the fluent query builder interface extending Laravel Scout's standard `search()` method. ```php use App\Models\Post; use JeroenG\Explorer\Domain\Syntax\Matching; use JeroenG\Explorer\Domain\Syntax\Term; // Simple search $posts = Post::search('laravel framework')->get(); // Advanced search with must/filter clauses $results = Post::search('elasticsearch') ->must(new Matching('title', 'tutorial', fuzziness: 'AUTO')) ->filter(new Term('published', true)) ->filter(new Term('category', 'technology')) ->take(10) ->get(); // Access raw results foreach ($results as $post) { echo $post->title . "\n"; } ``` ### Boolean Compound Queries Build complex boolean queries with must (AND), should (OR), and filter (AND without scoring) clauses to create sophisticated search logic. ```php use App\Models\Product; use JeroenG\Explorer\Domain\Syntax\Terms; use JeroenG\Explorer\Domain\Syntax\Range; use JeroenG\Explorer\Domain\Syntax\Matching; $products = Product::search('laptop') ->must(new Matching('name', 'gaming laptop')) ->should(new Terms('tags', ['nvidia', 'rtx', 'rgb'])) ->filter(new Range('price', ['gte' => 500, 'lte' => 2000])) ->filter(new Term('in_stock', true)) ->get(); // Multiple should clauses with minimum match $flexibleSearch = Product::search('') ->should(new Matching('brand', 'apple')) ->should(new Matching('brand', 'dell')) ->should(new Matching('brand', 'hp')) ->minimumShouldMatch(1) ->get(); ``` ### Nested Document Querying Query documents with nested object relationships using Elasticsearch's nested field type for complex data structures. ```php use App\Models\Article; use JeroenG\Explorer\Domain\Syntax\Nested; use JeroenG\Explorer\Domain\Syntax\Matching; use JeroenG\Explorer\Domain\Syntax\Term; // Model mapping with nested field class Article extends Model implements Explored { use Searchable; public function mappableAs(): array { return [ 'title' => 'text', 'comments' => [ 'type' => 'nested', 'properties' => [ 'author' => ['type' => 'text'], 'rating' => ['type' => 'integer'], 'text' => ['type' => 'text'], ], ], ]; } } // Search articles with nested comment filtering $articles = Article::search('programming') ->must(new Nested('comments', new Matching('comments.author', 'John Doe'))) ->filter(new Nested('comments', new Range('comments.rating', ['gte' => 4]))) ->get(); ``` ### Terms and Term Queries Execute exact-match queries on keyword fields for structured data like IDs, statuses, categories, and tags. ```php use App\Models\Order; use JeroenG\Explorer\Domain\Syntax\Term; use JeroenG\Explorer\Domain\Syntax\Terms; // Single exact match $orders = Order::search('*') ->filter(new Term('status', 'completed')) ->filter(new Term('payment_method', 'credit_card')) ->get(); // Multiple values (OR logic) $orders = Order::search('*') ->filter(new Terms('status', ['pending', 'processing', 'shipped'])) ->filter(new Terms('shipping_method', ['express', 'priority'])) ->get(); // Terms with boost for relevance scoring $results = Order::search('order') ->must(new Terms('tags', ['urgent', 'priority'], boost: 2.0)) ->get(); ``` ### Range Queries Filter results using numeric or date ranges with gte (>=), gt (>), lte (<=), and lt (<) operators. ```php use App\Models\Event; use JeroenG\Explorer\Domain\Syntax\Range; use Carbon\Carbon; // Date range filtering $upcomingEvents = Event::search('conference') ->filter(new Range('start_date', [ 'gte' => Carbon::now()->toIso8601String(), 'lte' => Carbon::now()->addMonths(3)->toIso8601String(), ])) ->get(); // Numeric range with open bounds $products = Product::search('laptop') ->filter(new Range('price', ['gte' => 1000])) ->filter(new Range('rating', ['gt' => 4])) ->get(); // Combined date and numeric ranges $sales = Sale::search('*') ->filter(new Range('amount', ['gte' => 100, 'lt' => 1000])) ->filter(new Range('created_at', ['gte' => 'now-7d/d'])) ->get(); ``` ### Aggregations Analyze search results using Elasticsearch aggregations for faceted search, analytics, and data summaries. ```php use App\Models\Product; use JeroenG\Explorer\Domain\Aggregations\TermsAggregation; use JeroenG\Explorer\Domain\Aggregations\MaxAggregation; use JeroenG\Explorer\Domain\Syntax\Term; // Basic terms aggregation $results = Product::search('*') ->filter(new Term('category', 'electronics')) ->aggregation('brands', new TermsAggregation('brand', 10)) ->aggregation('price_max', new MaxAggregation('price')) ->get(); // Access aggregation results $aggregations = $results->aggregations(); foreach ($aggregations as $agg) { echo "Aggregation: {$agg->name()}\n"; if ($agg->name() === 'brands') { foreach ($agg->values() as $bucket) { echo " {$bucket['key']}: {$bucket['doc_count']}\n"; } } } // Nested aggregation for complex data use JeroenG\Explorer\Domain\Aggregations\NestedAggregation; $articles = Article::search('technology') ->aggregation('comment_authors', new NestedAggregation('comments', new TermsAggregation('comments.author', 20) ) ) ->get(); ``` ### Field Selection and Source Filtering Optimize query performance and response size by selecting specific fields to return from Elasticsearch. ```php use App\Models\User; use JeroenG\Explorer\Domain\Query\QueryProperties\SourceFilter; // Select specific fields only $users = User::search('john') ->field('id') ->field('name') ->field('email') ->get(); // Advanced source filtering with includes/excludes $products = Product::search('laptop') ->property( SourceFilter::empty() ->include('id', 'name', 'price') ->include('specifications.*') ->exclude('*_internal', '*_secret') ) ->get(); // Disable source retrieval entirely (IDs only) $ids = Product::search('gaming') ->property(SourceFilter::empty()->exclude('*')) ->get() ->pluck('id'); ``` ### Multi-Field Matching Search across multiple fields simultaneously with customizable field boosts and matching operators. ```php use App\Models\Article; use JeroenG\Explorer\Domain\Syntax\MultiMatch; // Search multiple fields with equal weight $articles = Article::search('') ->must(new MultiMatch('elasticsearch tutorial', ['title', 'content', 'summary'])) ->get(); // Boost specific fields for relevance $articles = Article::search('') ->must(new MultiMatch('laravel', [ 'title^3', // 3x boost 'tags^2', // 2x boost 'content', // 1x (default) ])) ->get(); // Multi-match with fuzziness and operator $results = Article::search('') ->must(new MultiMatch( query: 'elasticsearch performance', fields: ['title', 'content'], fuzziness: 'AUTO', operator: 'and' )) ->get(); ``` ### Query String Syntax Enable advanced user-driven queries using Elasticsearch query string syntax with operators and wildcards. ```php use App\Models\Document; use JeroenG\Explorer\Domain\Syntax\QueryString; use JeroenG\Explorer\Domain\Syntax\SimpleQueryString; // Full query string with field-specific searches $docs = Document::search('') ->must(new QueryString('title:elasticsearch AND author:john NOT status:draft')) ->get(); // Query string with specific fields and boost $results = Document::search('') ->must(new QueryString( query: 'quick brown fox', fields: ['title^2', 'content'], defaultOperator: 'AND' )) ->get(); // Simplified query string (safer for user input) $userSearch = Document::search('') ->must(new SimpleQueryString( query: 'elasticsearch +tutorial -deprecated', fields: ['title', 'content'] )) ->get(); ``` ### Wildcard and Regular Expression Queries Perform pattern-based searches using wildcards or regular expressions for flexible text matching. ```php use App\Models\Product; use JeroenG\Explorer\Domain\Syntax\Wildcard; use JeroenG\Explorer\Domain\Syntax\RegExp; // Wildcard search (* and ? patterns) $products = Product::search('*') ->filter(new Wildcard('sku', 'ELEC-*-2024')) ->filter(new Wildcard('name', 'laptop?')) ->get(); // Regular expression search $emails = User::search('*') ->filter(new RegExp('email', '.*@company\\.com')) ->get(); // Case-insensitive wildcard with boost $results = Product::search('') ->should(new Wildcard('category', 'electr*', caseInsensitive: true, boost: 1.5)) ->get(); ``` ### Exists and Missing Field Queries Filter documents based on field presence or absence to handle sparse data structures. ```php use App\Models\Profile; use JeroenG\Explorer\Domain\Syntax\Exists; use JeroenG\Explorer\Domain\Syntax\Invert; // Find documents where field exists $profilesWithPhone = Profile::search('*') ->filter(new Exists('phone_number')) ->filter(new Exists('verified_at')) ->get(); // Find documents where field is missing (using Invert) $incompleteProfiles = Profile::search('*') ->filter(new Invert(new Exists('bio'))) ->get(); // Combine existence checks $completeProfiles = Profile::search('*') ->filter(new Exists('email')) ->filter(new Exists('phone_number')) ->filter(new Exists('address')) ->get(); ``` ### Custom Scoring with Function Score Manipulate search relevance scores using custom scoring functions for business-specific ranking logic. ```php use App\Models\Product; use JeroenG\Explorer\Domain\Syntax\Compound\FunctionScore; use JeroenG\Explorer\Domain\Syntax\Compound\ScoreFunction\ScriptScoreFunction; use JeroenG\Explorer\Domain\Syntax\Matching; // Boost recent products $query = new Matching('name', 'laptop'); $scoreFunction = new FunctionScore($query); $scoreFunction->addDecayFunction('gauss', 'created_at', [ 'origin' => 'now', 'scale' => '30d', 'decay' => 0.5, ]); $products = Product::search('') ->must($scoreFunction) ->get(); // Custom script scoring $scriptScore = new ScriptScoreFunction( source: "Math.log(2 + doc['popularity'].value) * params.multiplier", params: ['multiplier' => 1.5] ); $functionScore = new FunctionScore($query); $functionScore->addScriptScoreFunction($scriptScore); $results = Product::search('')->must($functionScore)->get(); ``` ### Match Phrase and Proximity Searches Find exact phrase matches or words within specified proximity for precise text matching. ```php use App\Models\Article; use JeroenG\Explorer\Domain\Syntax\MatchPhrase; // Exact phrase matching $articles = Article::search('') ->must(new MatchPhrase('content', 'elasticsearch cluster setup')) ->get(); // Phrase with slop (allows words in between) $articles = Article::search('') ->must(new MatchPhrase( field: 'content', query: 'laravel scout elasticsearch', slop: 2 // Allow up to 2 words between terms )) ->get(); // Multiple phrase matches $results = Article::search('programming') ->should(new MatchPhrase('title', 'best practices')) ->should(new MatchPhrase('title', 'getting started')) ->minimumShouldMatch(1) ->get(); ``` ### Distance Feature Queries Boost search results based on geographic or numeric distance from a reference point. ```php use App\Models\Store; use JeroenG\Explorer\Domain\Syntax\DistanceFeature; // Boost nearby locations $stores = Store::search('coffee shop') ->must(new DistanceFeature( field: 'location', origin: '40.7128,-74.0060', // New York coordinates pivot: '5km', boost: 2.0 )) ->get(); // Date-based distance (e.g., recent events) $events = Event::search('conference') ->must(new DistanceFeature( field: 'event_date', origin: 'now', pivot: '7d', boost: 1.5 )) ->get(); ``` ### Sorting Results Control result ordering with single or multiple sort criteria including geo-distance sorting. ```php use App\Models\Product; use JeroenG\Explorer\Domain\Syntax\Sort; use JeroenG\Explorer\Domain\Syntax\SortOrder; // Simple field sorting $products = Product::search('laptop') ->orderBy('price', 'asc') ->orderBy('rating', 'desc') ->get(); // Advanced sorting with Sort object $products = Product::search('laptop') ->must(new Term('in_stock', true)) ->orderBy( Sort::create('price', SortOrder::ASC) ->missing('_last') ->unmappedType('long') ) ->get(); // Geo-distance sorting $stores = Store::search('*') ->orderBy([ '_geo_distance' => [ 'location' => ['lat' => 40.7128, 'lon' => -74.0060], 'order' => 'asc', 'unit' => 'km', ] ]) ->get(); ``` ### Pagination with Cursor Paginate search results efficiently using Laravel Scout's pagination methods with Elasticsearch scroll support. ```php use App\Models\Article; // Standard pagination $articles = Article::search('laravel') ->paginate(perPage: 15, pageName: 'page', page: 1); // Access pagination metadata foreach ($articles as $article) { echo $article->title . "\n"; } echo "Total: {$articles->total()}\n"; echo "Per page: {$articles->perPage()}\n"; // Simple pagination (no total count) $articles = Article::search('laravel') ->simplePaginate(15); // Deep pagination with search_after $firstPage = Article::search('laravel')->take(100)->get(); $searchAfter = $firstPage->last()->getScoutKey(); $nextPage = Article::search('laravel')->take(100)->searchAfter($searchAfter)->get(); ``` ### Track Total Hits Control whether Elasticsearch counts total matching documents for performance optimization. ```php use App\Models\Product; use JeroenG\Explorer\Domain\Query\QueryProperties\TrackTotalHits; // Exact count (default, slower for large result sets) $products = Product::search('laptop') ->property(new TrackTotalHits(true)) ->paginate(15); echo "Total products: {$products->total()}\n"; // Limit counting to 10000 for performance $products = Product::search('laptop') ->property(new TrackTotalHits(10000)) ->get(); // Disable counting for maximum performance $products = Product::search('laptop') ->property(new TrackTotalHits(false)) ->take(20) ->get(); ``` ### Model Configuration with Explored Interface Define Elasticsearch field mappings and indexing behavior using the Explored interface on Eloquent models. ```php use Illuminate\Database\Eloquent\Model; use JeroenG\Explorer\Application\Explored; use JeroenG\Explorer\Application\Aliased; use JeroenG\Explorer\Application\IndexSettings; use Laravel\Scout\Searchable; class Product extends Model implements Explored, Aliased, IndexSettings { use Searchable; protected $fillable = ['name', 'description', 'price', 'stock']; // Define field mappings public function mappableAs(): array { return [ 'id' => 'keyword', 'name' => ['type' => 'text', 'analyzer' => 'standard'], 'description' => ['type' => 'text', 'analyzer' => 'english'], 'price' => 'double', 'stock' => 'integer', 'tags' => ['type' => 'keyword'], 'created_at' => ['type' => 'date', 'format' => 'strict_date_time'], 'specifications' => [ 'type' => 'nested', 'properties' => [ 'key' => ['type' => 'keyword'], 'value' => ['type' => 'text'], ], ], ]; } // Custom index settings public function indexSettings(): array { return [ 'number_of_shards' => 3, 'number_of_replicas' => 2, 'refresh_interval' => '1s', ]; } // Control what data gets indexed public function toSearchableArray() { return [ 'id' => $this->id, 'name' => $this->name, 'description' => $this->description, 'price' => (float) $this->price, 'stock' => (int) $this->stock, 'tags' => $this->tags()->pluck('name')->toArray(), 'created_at' => $this->created_at->toIso8601String(), ]; } } ``` ### Custom Analyzers and Text Analysis Configure custom text analyzers, tokenizers, and filters for language-specific or domain-specific search requirements. ```php use Illuminate\Database\Eloquent\Model; use JeroenG\Explorer\Application\Explored; use JeroenG\Explorer\Application\IndexSettings; use JeroenG\Explorer\Domain\Analysis\Analysis; use JeroenG\Explorer\Domain\Analysis\Analyzer\StandardAnalyzer; use JeroenG\Explorer\Domain\Analysis\Filter\SynonymFilter; use Laravel\Scout\Searchable; class Article extends Model implements Explored, IndexSettings { use Searchable; public function mappableAs(): array { return [ 'title' => [ 'type' => 'text', 'analyzer' => 'custom_english', 'fields' => [ 'keyword' => ['type' => 'keyword'], ], ], 'content' => ['type' => 'text', 'analyzer' => 'custom_english'], ]; } public function indexSettings(): array { $analysis = new Analysis(); // Add synonym filter $synonyms = new SynonymFilter('custom_synonyms', [ 'elasticsearch, elastic, es', 'laravel, lumen', 'search, find, lookup', ]); $analysis->addFilter($synonyms); // Add custom analyzer $analyzer = new StandardAnalyzer('custom_english'); $analyzer->setFilters(['lowercase', 'custom_synonyms', 'english_stemmer']); $analyzer->setStopwords(['a', 'an', 'the']); $analysis->addAnalyzer($analyzer); return [ 'number_of_shards' => 1, 'analysis' => $analysis->build(), ]; } } ``` ### Config-Based Index Definition Define Elasticsearch indexes in configuration files instead of model classes for flexibility. ```php // config/explorer.php return [ 'connection' => [ 'host' => env('ELASTICSEARCH_HOST', 'localhost'), 'port' => env('ELASTICSEARCH_PORT', '9200'), 'scheme' => env('ELASTICSEARCH_SCHEME', 'http'), 'user' => env('ELASTICSEARCH_USER'), 'pass' => env('ELASTICSEARCH_PASS'), ], 'indexes' => [ // Model-based index \App\Models\Product::class, // Config-based index 'articles' => [ 'properties' => [ 'id' => 'keyword', 'title' => [ 'type' => 'text', 'analyzer' => 'standard', 'fields' => ['keyword' => ['type' => 'keyword']], ], 'content' => ['type' => 'text'], 'author' => ['type' => 'keyword'], 'published_at' => ['type' => 'date'], 'tags' => ['type' => 'keyword'], ], 'settings' => [ 'number_of_shards' => 2, 'number_of_replicas' => 1, ], ], ], 'prune_old_aliases' => true, ]; ``` ### Index Management Commands Manage Elasticsearch indexes using Artisan commands for creating, updating, importing, and flushing indexes. ```bash # Create or update all configured indexes php artisan elastic:update # Update specific index php artisan elastic:update products # Update with aliased index (creates new version) php artisan elastic:update articles # Import all models into index php artisan scout:import "App\Models\Product" # Import with chunking php artisan scout:import "App\Models\Product" --chunk=500 # Flush (delete all documents from) index php artisan scout:flush "App\Models\Product" # Search from command line php artisan elastic:search "App\Models\Product" "laptop" --fields=id,name,price # Delete and recreate index php artisan scout:flush "App\Models\Product" php artisan scout:import "App\Models\Product" ``` ### Zero-Downtime Index Updates with Aliases Implement zero-downtime reindexing using the Aliased interface for seamless index updates in production. ```php use Illuminate\Database\Eloquent\Model; use JeroenG\Explorer\Application\Explored; use JeroenG\Explorer\Application\Aliased; use Laravel\Scout\Searchable; class Product extends Model implements Explored, Aliased { use Searchable; public function mappableAs(): array { return [ 'id' => 'keyword', 'name' => 'text', 'price' => 'double', ]; } // Override default alias (optional) public function searchableAs(): string { return 'products_alias'; } } // Update process creates new index, imports data, swaps alias // Step 1: Creates products_20240116_143022 // Step 2: Imports all documents // Step 3: Updates alias products_alias -> products_20240116_143022 // Step 4: Deletes old index (if prune_old_aliases = true) ``` ```bash # Trigger zero-downtime update php artisan elastic:update products_alias # Manual process via facade use JeroenG\Explorer\Infrastructure\Elastic\ElasticIndexAdapter; $adapter = app(ElasticIndexAdapter::class); $config = app(IndexConfigurationRepositoryInterface::class)->findForIndex('products_alias'); // Create new timestamped index $newIndex = $adapter->createNewWriteIndex($config); // Import data to new index Product::all()->searchable(); // Swap alias atomically $adapter->pointToAlias($config); ``` ### Query Debugging Debug and inspect generated Elasticsearch queries to optimize search performance and troubleshoot issues. ```php use App\Models\Product; use JeroenG\Explorer\Infrastructure\Scout\ElasticEngine; use JeroenG\Explorer\Domain\Syntax\Matching; use JeroenG\Explorer\Domain\Syntax\Range; // Execute search $products = Product::search('laptop') ->must(new Matching('name', 'gaming')) ->filter(new Range('price', ['gte' => 500, 'lte' => 2000])) ->take(10) ->get(); // Get last executed query $debugger = ElasticEngine::debug(); // Output query array dump($debugger->query()); // [ // 'index' => 'products', // 'body' => [ // 'query' => [ // 'bool' => [ // 'must' => [ // ['multi_match' => ['query' => 'laptop', 'fields' => ['*']]], // ['match' => ['name' => ['query' => 'gaming']]], // ], // 'filter' => [ // ['range' => ['price' => ['gte' => 500, 'lte' => 2000]]], // ], // ], // ], // 'size' => 10, // ], // ] // Log queries for analysis \Log::info('Elasticsearch Query', $debugger->query()); ``` ### Bulk Operations Perform efficient bulk indexing and updates using Scout's bulk operations for large datasets. ```php use App\Models\Product; use JeroenG\Explorer\Application\Operations\Bulk\BulkUpdateOperation; // Bulk import with Scout (chunked automatically) Product::query()->where('active', true)->searchable(); // Manual bulk operations use JeroenG\Explorer\Infrastructure\Elastic\ElasticDocumentAdapter; $adapter = app(DocumentAdapterInterface::class); $products = Product::where('active', true)->get(); $operations = new BulkUpdateOperation('products'); foreach ($products as $product) { $operations->add( $product->getScoutKey(), $product->toSearchableArray() ); } $response = $adapter->bulk($operations); // Unsearchable (bulk delete from index) Product::query()->where('active', false)->unsearchable(); // Queue-based async indexing Product::find(1)->queueSearchable(); ``` ### Custom Search Command Builder Build advanced search commands with custom query properties for specialized search requirements. ```php use JeroenG\Explorer\Application\SearchCommand; use JeroenG\Explorer\Domain\Query\Query; use JeroenG\Explorer\Domain\Syntax\Compound\BoolQuery; use JeroenG\Explorer\Domain\Syntax\Matching; use JeroenG\Explorer\Domain\Query\QueryProperties\SourceFilter; use JeroenG\Explorer\Domain\Query\QueryProperties\TrackTotalHits; // Create custom search command $query = new Query(); $query->setLimit(20); $query->setOffset(0); $query->setFields(['id', 'name', 'price']); $bool = new BoolQuery(); $bool->must(new Matching('name', 'laptop')); $bool->filter(new Term('category', 'electronics')); $bool->minimumShouldMatch('75%'); $query->setQuery($bool); $query->addQueryProperty(new SourceFilter(['id', 'name', 'price'])); $query->addQueryProperty(new TrackTotalHits(10000)); // Execute via document adapter use JeroenG\Explorer\Infrastructure\Elastic\ElasticDocumentAdapter; $adapter = app(DocumentAdapterInterface::class); $searchCommand = new SearchCommand('products', $query->build()); $results = $adapter->search($searchCommand); // Access raw results $hits = $results->hits(); $total = $results->count(); ``` --- ## Summary Explorer transforms Laravel Scout into a production-ready, full-featured Elasticsearch integration that maintains Laravel's elegant syntax while unlocking advanced search capabilities. The package provides comprehensive support for complex boolean queries, nested document searches, aggregations, custom scoring functions, and fine-grained index management. Developers can leverage Explorer's fluent query builder to construct sophisticated searches using Elasticsearch's complete query DSL without writing raw JSON arrays, while maintaining type safety and IDE autocomplete support through well-defined interfaces and classes. The architecture emphasizes flexibility and maintainability through multiple configuration approaches: model-based definitions using the Explored interface for rapid development, config-based definitions for shared indexes across models, and programmatic query building for dynamic search requirements. Explorer handles production concerns including zero-downtime index updates via aliased indexes, efficient bulk operations for large datasets, comprehensive debugging tools for query optimization, and seamless integration with Laravel's queue system for asynchronous indexing. Whether building a simple site search, complex faceted navigation, or sophisticated recommendation engine, Explorer provides the tools needed to harness Elasticsearch's full power within Laravel's ecosystem.