Try Live
Add Docs
Rankings
Pricing
Docs
Install
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
Laravel Cross Eloquent Search
https://github.com/protonemedia/laravel-cross-eloquent-search
Admin
A Laravel package that enables searching across multiple Eloquent models simultaneously, with
...
Tokens:
8,501
Snippets:
76
Trust Score:
9.7
Update:
2 weeks ago
Context
Skills
Chat
Benchmark
95.6
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Laravel Cross Eloquent Search Laravel Cross Eloquent Search is a powerful Laravel package that enables searching across multiple Eloquent models in a single query. It provides a fluent API for combining results from different database tables while maintaining proper pagination, sorting, and filtering capabilities. The package supports MySQL, PostgreSQL, and SQLite databases with features like full-text search, relationship searching, and relevance-based ordering. The core functionality centers around the `Search` facade which allows developers to add multiple models with their searchable columns, apply various search options like wildcards and sounds-like matching, and retrieve combined results sorted by customizable criteria. The package handles the complexity of UNION queries across different models while preserving Eloquent's eager loading and query scoping capabilities. ## Basic Search Search through multiple Eloquent models by specifying the model class and columns to search. Returns a combined collection of results sorted by `updated_at` by default. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Basic search across two models $results = Search::add(Post::class, 'title') ->add(Video::class, 'title') ->search('laravel'); // Using the new() method for cleaner indentation $results = Search::new() ->add(Post::class, 'title') ->add(Video::class, 'title') ->search('howto'); // Returns: Illuminate\Database\Eloquent\Collection containing Post and Video models // Example output: Collection with 5 items (3 Posts, 2 Videos) matching 'laravel' ``` ## Multiple Column Search Search through multiple columns on a single model by passing an array of column names. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Search in title and body for Posts, title and subtitle for Videos $results = Search::add(Post::class, ['title', 'body']) ->add(Video::class, ['title', 'subtitle']) ->search('eloquent'); // Matches any Post where title OR body contains 'eloquent' // AND any Video where title OR subtitle contains 'eloquent' ``` ## Wildcard Configuration Control how wildcards are applied to search terms. By default, terms end with a wildcard (`term%`). Enable beginning wildcards for broader matching or disable them entirely for exact prefix matching. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Enable wildcard at the beginning: %term% $results = Search::add(Post::class, 'title') ->add(Video::class, 'title') ->beginWithWildcard() ->search('os'); // Matches: 'macOS', 'iOS', 'operating system', etc. // Disable ending wildcard: %term (only match endings) $results = Search::add(Post::class, 'title') ->beginWithWildcard() ->endWithWildcard(false) ->search('os'); // Matches: 'macOS', 'iOS' but NOT 'oscillator' // Exact match: no wildcards, uses = operator instead of LIKE $results = Search::add(Post::class, 'title') ->add(Video::class, 'title') ->exactMatch() ->search('Laravel'); // Only matches exact title "Laravel", not "Laravel Framework" ``` ## Multi-word and Phrase Search Search for exact phrases using double quotes, or disable term parsing to treat the entire input as a single phrase. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Phrase search with double quotes $results = Search::add(Post::class, 'title') ->add(Video::class, 'title') ->search('"macos big sur"'); // Searches for the exact phrase "macos big sur" // Disable term parsing (same result as using quotes) $results = Search::add(Post::class, 'title') ->add(Video::class, 'title') ->dontParseTerm() ->search('macos big sur'); // Treats entire string as single search term // Default behavior splits terms: 'apple ios' becomes ['apple%', 'ios%'] $results = Search::add(Post::class, 'title') ->search('apple ios'); // Matches posts containing 'apple' OR 'ios' ``` ## Custom Sorting Specify custom columns for ordering and control sort direction. Each model can have its own order column. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Sort by custom columns per model, descending order $results = Search::add(Post::class, 'title', 'published_at') ->add(Video::class, 'title', 'released_at') ->orderByDesc() ->search('learn'); // Alternative: set order column after adding the model $results = Search::new() ->add(Post::class, 'title') ->orderBy('published_at') ->add(Video::class, 'title') ->orderBy('released_at') ->orderByDesc() ->search('tutorial'); // Sort ascending (default) $results = Search::add(Post::class, 'title', 'created_at') ->orderByAsc() ->search('guide'); ``` ## Order by Relevance Sort results by the number of search term occurrences. More matches rank higher in results. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Sort by relevance - more term matches = higher ranking $results = Search::new() ->add(Post::class, ['title', 'body']) ->add(Video::class, ['title', 'description']) ->beginWithWildcard() ->orderByRelevance() ->search('Apple iPad'); // Example: "Apple unveils new iPad mini with iPad Pro features" ranks higher // than "Apple introduces new product" because iPad appears twice // Note: Not supported when searching through nested relationships ``` ## Order by Model Type Control the order of results by model type, ensuring certain models appear before others. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Prioritize results by model type $results = Search::new() ->add(Comment::class, ['body']) ->add(Post::class, ['title']) ->add(Video::class, ['title', 'description']) ->orderByModel([ Post::class, // Posts appear first Video::class, // Then Videos Comment::class, // Comments last ]) ->search('Artisan School'); // Combine with orderByDesc for descending within each type $results = Search::new() ->add(Post::class, 'title', 'published_at') ->add(Video::class, 'title', 'published_at') ->orderByModel([Video::class, Post::class]) ->orderByDesc() ->search('framework'); ``` ## Pagination Paginate search results using Laravel's built-in pagination. Supports both length-aware and simple pagination. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Length-aware pagination (includes total count) $results = Search::add(Post::class, 'title') ->add(Video::class, 'title') ->paginate() ->search('build'); // Returns: LengthAwarePaginator // Custom pagination options $results = Search::add(Post::class, 'title') ->add(Video::class, 'title') ->paginate( $perPage = 15, $pageName = 'page', $page = 1 ) ->search('build'); // Simple pagination (no total count, better performance) $results = Search::add(Post::class, 'title') ->add(Video::class, 'title') ->simplePaginate(15, 'page', 1) ->search('build'); // Returns: Paginator // Preserve query string parameters in pagination links $results = Search::add(Post::class, 'title') ->paginate(15) ->withQueryString() // Uses request()->query() automatically ->search('build'); // Or pass custom parameters $results = Search::add(Post::class, 'title') ->paginate(15) ->withQueryString(['filter' => 'active', 'sort' => 'date']) ->search('build'); // Pagination links include: ?page=2&filter=active&sort=date ``` ## Query Constraints and Scopes Pass Eloquent query builders instead of class names to add constraints and use scoped queries. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Using query scopes and where clauses $results = Search::add(Post::published(), 'title') ->add(Video::where('views', '>', 2500), 'title') ->search('compile'); // Complex query constraints $results = Search::add( Post::where('status', 'published') ->where('created_at', '>', now()->subMonth()), 'title' ) ->add( Video::whereNotNull('published_at') ->where('duration', '>', 60), 'title' ) ->search('tutorial'); ``` ## Relationship Search Search through model relationships using dot notation for nested relations. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Search through direct relationships $results = Search::add(Post::class, ['comments.body']) ->search('solution'); // Finds Posts that have comments containing 'solution' // Search through nested relationships $results = Search::add(Video::class, ['posts.user.biography']) ->search('developer'); // Finds Videos whose posts have users with 'developer' in biography // Combine direct and relationship columns $results = Search::new() ->add(Video::class, ['title', 'posts.comments.body']) ->add(Post::class, ['title', 'comments.body']) ->search('amazing feature'); ``` ## Full-Text Search Use native database full-text search capabilities for better performance and advanced query syntax. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Full-text search with boolean mode $results = Search::new() ->add(Post::class, 'title') // Regular LIKE search ->addFullText(Video::class, 'title', ['mode' => 'boolean']) ->addFullText(Blog::class, ['title', 'subtitle', 'body'], ['mode' => 'boolean']) ->search('framework -css'); // Boolean mode: '+' requires term, '-' excludes term // Matches 'framework' but NOT 'css' // Full-text search through relationships $results = Search::new() ->addFullText(Page::class, [ 'posts' => ['title', 'body'], 'sections' => ['title', 'subtitle', 'body'], ]) ->search('laravel eloquent'); // Mixed direct columns and relations $results = Search::new() ->addFullText(Video::class, [ 'title', // Direct column 'blogs' => ['title', 'subtitle', 'body'], // Relation columns ]) ->search('Laravel'); ``` ## Sounds Like Search Find phonetically similar terms using soundex matching. Useful for handling misspellings. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Enable sounds-like matching $results = Search::new() ->add(Post::class, 'title') ->add(Video::class, 'title') ->soundsLike() ->search('larafel'); // Matches 'laravel' because it sounds similar // Works across multiple models $results = Search::add(User::class, 'name') ->add(Company::class, 'name') ->soundsLike() ->search('Jhon'); // Matches 'John', 'Jon', etc. ``` ## Eager Loading Eager load relationships on search results to avoid N+1 queries. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Eager load with counts $results = Search::add( Post::with('comments')->withCount('comments'), 'title' ) ->add( Video::with('likes', 'author'), 'title' ) ->search('guitar'); // Access eager loaded relations foreach ($results as $result) { if ($result instanceof Post) { echo $result->comments_count; foreach ($result->comments as $comment) { echo $comment->body; } } } ``` ## Model Type Identifier Include a type attribute in results to identify which model each result belongs to. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Add type identifier to results $results = Search::add(Post::class, 'title') ->add(Video::class, 'title') ->includeModelType() ->paginate() ->search('foo'); // Result includes 'type' attribute // {"id": 1, "title": "foo", "type": "Post"} // {"id": 1, "title": "foo", "type": "Video"} // Custom type key name $results = Search::add(Post::class, 'title') ->includeModelType('model_class') ->search('foo'); // Uses 'model_class' instead of 'type' // Custom type value via model method class Video extends Model { public function searchType() { return 'awesome_video'; } } // Result: {"type": "awesome_video"} ``` ## Conditional Queries Conditionally add models and apply options based on runtime conditions. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; use ProtoneMedia\LaravelCrossEloquentSearch\Searcher; // Conditionally add models with when() $results = Search::new() ->when($user->isVerified(), fn($search) => $search->add(Post::class, 'title')) ->when($user->isAdmin(), fn($search) => $search->add(Video::class, 'title')) ->when($sortByRelevance, fn($search) => $search->orderByRelevance()) ->search('howto'); // Tap into the searcher instance $results = Search::add(Post::class, 'title') ->add(Video::class, 'title') ->tap(function (Searcher $searcher) { Log::info('Searching...', [ 'models' => $searcher->getModelsToSearchThrough() ]); }) ->search('laravel'); ``` ## Counting Results Get the count of matching results without fetching the actual models. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Count total matches $count = Search::add(Post::published(), 'title') ->add(Video::where('views', '>', 2500), 'title') ->count('compile'); echo "Found {$count} results"; // Useful for showing result counts before pagination $totalCount = Search::add(Post::class, 'title') ->add(Video::class, 'title') ->count('laravel'); $results = Search::add(Post::class, 'title') ->add(Video::class, 'title') ->paginate(10) ->search('laravel'); ``` ## Search Without Term Retrieve combined model results without a search term, useful for creating unified feeds. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Get all results without searching, ordered by custom columns $results = Search::new() ->add(Post::class) ->orderBy('published_at') ->add(Video::class) ->orderBy('released_at') ->search(); // Paginate a unified feed $feed = Search::new() ->add(Post::published()) ->orderBy('published_at') ->add(Video::where('status', 'public')) ->orderBy('created_at') ->orderByDesc() ->paginate(20) ->search(); ``` ## Term Parser Parse search terms programmatically using the standalone parser. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Parse terms into a collection $terms = Search::parseTerms('drums guitar'); // Returns: Collection ['drums', 'guitar'] $terms = Search::parseTerms('"foo bar"'); // Returns: Collection ['foo bar'] (keeps phrase intact) // Parse with callback Search::parseTerms('drums guitar', function ($term, $key) { echo "Term {$key}: {$term}"; }); // Output: Term 0: drums, Term 1: guitar // Use parsed terms for custom logic $terms = Search::parseTerms($userInput); foreach ($terms as $term) { // Validate or transform each term if (strlen($term) < 3) { continue; } // Build custom query... } ``` ## Add Multiple Models at Once Register multiple models and their configurations in a single call. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Add multiple models with addMany() $results = Search::addMany([ [Post::class, 'title'], [Video::class, 'title', 'published_at'], // With custom order column [Comment::class, ['body', 'author_name']], // Multiple columns [Post::published(), 'body'], // With query builder ])->search('laravel'); // Equivalent to: $results = Search::new() ->add(Post::class, 'title') ->add(Video::class, 'title', 'published_at') ->add(Comment::class, ['body', 'author_name']) ->add(Post::published(), 'body') ->search('laravel'); ``` ## Ignore Case Perform case-insensitive searches regardless of database collation. ```php use ProtoneMedia\LaravelCrossEloquentSearch\Search; // Case-insensitive search $results = Search::add(Post::class, 'title') ->add(Video::class, 'title') ->beginWithWildcard() ->ignoreCase() ->search('FOO'); // Matches: 'foo', 'Foo', 'FOO', 'fOO', etc. // Combine with other options $results = Search::add(Article::class, ['title', 'content']) ->beginWithWildcard() ->ignoreCase() ->paginate(15) ->search('LARAVEL framework'); ``` --- Laravel Cross Eloquent Search excels in scenarios requiring unified search across heterogeneous data: content management systems with multiple content types (articles, videos, podcasts), e-commerce platforms searching products and categories simultaneously, or social platforms searching users, posts, and comments together. The package's strength lies in handling the complexity of cross-model UNION queries while maintaining familiar Eloquent patterns. Integration follows Laravel conventions: install via Composer, use the `Search` facade, and chain methods fluently. The package automatically detects database drivers and applies appropriate optimizations (MySQL full-text indexes, PostgreSQL tsquery, SQLite LIKE alternatives). For production applications, leverage pagination to manage memory, use eager loading to prevent N+1 queries, and consider full-text search for large datasets requiring boolean operators or relevance scoring.