Laravel package to search through multiple Eloquent models.

Overview

Laravel Cross Eloquent Search

Latest Version on Packagist run-tests Quality Score Total Downloads Buy us a tree

This Laravel package allows you to search through multiple Eloquent models. It supports sorting, pagination, scoped queries, eager load relationships, and searching through single or multiple columns.

Launcher 🚀

Hey! We've built a Docker-based deployment tool to launch apps and sites fully containerized. You can find all features and the roadmap on our website, and we are on Twitter as well!

Requirements

  • PHP 7.4+
  • MySQL 5.7+
  • Laravel 6.0 and higher

Features

📺 Want to watch an implementation of this package? Rewatch the live stream (skip to 13:44 for the good stuff): https://youtu.be/WigAaQsPgSA

Blog post

If you want to know more about this package's background, please read the blog post.

Support

We proudly support the community by developing Laravel packages and giving them away for free. Keeping track of issues and pull requests takes time, but we're happy to help! If this package saves you time or if you're relying on it professionally, please consider supporting the maintenance and development.

Installation

You can install the package via composer:

composer require protonemedia/laravel-cross-eloquent-search

Upgrading from v1

  • The startWithWildcard method has been renamed to beginWithWildcard.
  • The default order column is now evaluated by the getUpdatedAtColumn method. Previously it was hard-coded to updated_at. You still can use another column to order by.
  • The allowEmptySearchQuery method and EmptySearchQueryException class have been removed, but you can still get results without searching.

Usage

Start your search query by adding one or more models to search through. Call the add method with the model's class name and the column you want to search through. Then call the get method with the search term, and you'll get a \Illuminate\Database\Eloquent\Collection instance with the results.

The results are sorted in ascending order by the updated column by default. In most cases, this column is updated_at. If you've customized your model's UPDATED_AT constant, or overwritten the getUpdatedAtColumn method, this package will use the customized column. Of course, you can order by another column as well.

use ProtoneMedia\LaravelCrossEloquentSearch\Search;

$results = Search::add(Post::class, 'title')
    ->add(Video::class, 'title')
    ->get('howto');

If you care about indentation, you can optionally use the new method on the facade:

Search::new()
    ->add(Post::class, 'title')
    ->add(Video::class, 'title')
    ->get('howto');

You can add multiple models at once by using the addMany method:

Search::addMany([
    [Post::class, 'title'],
    [Video::class, 'title'],
])->get('howto');

There's also an addWhen method, that adds the model when the first argument given to the method evaluates to true:

Search::new()
    ->addWhen($user, Post::class, 'title')
    ->addWhen($user->isAdmin(), Video::class, 'title')
    ->get('howto');

Wildcards

By default, we split up the search term, and each keyword will get a wildcard symbol to do partial matching. Practically this means the search term apple ios will result in apple% and ios%. If you want a wildcard symbol to begin with as well, you can call the beginWithWildcard method. This will result in %apple% and %ios%.

Search::add(Post::class, 'title')
    ->add(Video::class, 'title')
    ->beginWithWildcard()
    ->get('os');

Note: in previous versions of this package, this method was called startWithWildcard().

If you want to disable the behaviour where a wildcard is appended to the terms, you should call the endWithWildcard method with false:

Search::add(Post::class, 'title')
    ->add(Video::class, 'title')
    ->beginWithWildcard()
    ->endWithWildcard(false)
    ->get('os');

Multi-word search

Multi-word search is supported out of the box. Simply wrap your phrase into double-quotes.

Search::add(Post::class, 'title')
    ->add(Video::class, 'title')
    ->get('"macos big sur"');

You can disable the parsing of the search term by calling the dontParseTerm method, which gives you the same results as using double-quotes.

Search::add(Post::class, 'title')
    ->add(Video::class, 'title')
    ->dontParseTerm()
    ->get('macos big sur');

Sorting

If you want to sort the results by another column, you can pass that column to the add method as a third parameter. Call the orderByDesc method to sort the results in descending order.

Search::add(Post::class, 'title', 'published_at')
    ->add(Video::class, 'title', 'released_at')
    ->orderByDesc()
    ->get('learn');

You can call the orderByRelevance method to sort the results by the number of occurrences of the search terms. Imagine these two sentences:

  • Apple introduces iPhone 13 and iPhone 13 mini
  • Apple unveils new iPad mini with breakthrough performance in stunning new design

If you search for Apple iPad, the second sentence will come up first, as there are more matches of the search terms.

Search::add(Post::class, 'title')
    ->beginWithWildcard()
    ->orderByRelevance()
    ->get('Apple iPad');

Ordering by relevance is not supported if you're searching through (nested) relationships.

To sort the results by model type, you can use the orderByModel method by giving it your preferred order of the models:

Search::new()
    ->add(Comment::class, ['body'])
    ->add(Post::class, ['title'])
    ->add(Video::class, ['title', 'description'])
    ->orderByModel([
        Post::class, Video::class, Comment::class,
    ])
    ->get('Artisan School');

Pagination

We highly recommend paginating your results. Call the paginate method before the get method, and you'll get an instance of \Illuminate\Contracts\Pagination\LengthAwarePaginator as a result. The paginate method takes three (optional) parameters to customize the paginator. These arguments are the same as Laravel's database paginator.

Search::add(Post::class, 'title')
    ->add(Video::class, 'title')

    ->paginate()
    // or
    ->paginate($perPage = 15, $pageName = 'page', $page = 1)

    ->get('build');

You may also use simple pagination. This will return an instance of \Illuminate\Contracts\Pagination\Paginator, which is not length aware:

Search::add(Post::class, 'title')
    ->add(Video::class, 'title')

    ->simplePaginate()
    // or
    ->simplePaginate($perPage = 15, $pageName = 'page', $page = 1)

    ->get('build');

Constraints and scoped queries

Instead of the class name, you can also pass an instance of the Eloquent query builder to the add method. This allows you to add constraints to each model.

Search::add(Post::published(), 'title')
    ->add(Video::where('views', '>', 2500), 'title')
    ->get('compile');

Multiple columns per model

You can search through multiple columns by passing an array of columns as the second argument.

Search::add(Post::class, ['title', 'body'])
    ->add(Video::class, ['title', 'subtitle'])
    ->get('eloquent');

Search through (nested) relationships

You can search through (nested) relationships by using the dot notation:

Search::add(Post::class, ['comments.body'])
    ->add(Video::class, ['posts.user.biography'])
    ->get('solution');

Sounds like

MySQL has a soundex algorithm built-in so you can search for terms that sound almost the same. You can use this feature by calling the soundsLike method:

Search::new()
    ->add(Post::class, 'framework')
    ->add(Video::class, 'framework')
    ->soundsLike()
    ->get('larafel');

Eager load relationships

Not much to explain here, but this is supported as well :)

Search::add(Post::with('comments'), 'title')
    ->add(Video::with('likes'), 'title')
    ->get('guitar');

Getting results without searching

You call the get method without a term or with an empty term. In this case, you can discard the second argument of the add method. With the orderBy method, you can set the column to sort by (previously the third argument):

Search::add(Post::class)
    ->orderBy('published_at')
    ->add(Video::class)
    ->orderBy('released_at')
    ->get();

Counting records

You can count the number of results with the count method:

Search::add(Post::published(), 'title')
    ->add(Video::where('views', '>', 2500), 'title')
    ->count('compile');

Standalone parser

You can use the parser with the parseTerms method:

$terms = Search::parseTerms('drums guitar');

You can also pass in a callback as a second argument to loop through each term:

Search::parseTerms('drums guitar', function($term, $key) {
    //
});

Testing

composer test

Changelog

Please see CHANGELOG for more information about what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Other Laravel packages

  • Laravel Analytics Event Tracking: Laravel package to easily send events to Google Analytics.
  • Laravel Blade On Demand: Laravel package to compile Blade templates in memory.
  • Laravel Eloquent Scope as Select: Stop duplicating your Eloquent query scopes and constraints in PHP. This package lets you re-use your query scopes and constraints by adding them as a subquery.
  • Laravel Eloquent Where Not: This Laravel package allows you to flip/invert an Eloquent scope, or really any query constraint.
  • Laravel FFMpeg: This package provides an integration with FFmpeg for Laravel. The storage of the files is handled by Laravel's Filesystem.
  • Laravel Form Components: Blade components to rapidly build forms with Tailwind CSS Custom Forms and Bootstrap 4. Supports validation, model binding, default values, translations, includes default vendor styling and fully customizable!
  • Laravel Mixins: A collection of Laravel goodies.
  • Laravel Paddle: Paddle.com API integration for Laravel with support for webhooks/events.
  • Laravel Verify New Email: This package adds support for verifying new email addresses: when a user updates its email address, it won't replace the old one until the new one is verified.
  • Laravel WebDAV: WebDAV driver for Laravel's Filesystem.

Security

If you discover any security-related issues, please email [email protected] instead of using the issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.

Treeware

This package is Treeware. If you use it in production, we ask that you buy the world a tree to thank us for our work. By contributing to the Treeware forest, you'll create employment for local families and restoring wildlife habitats.

Comments
  • addMany returns inconsitent results

    addMany returns inconsitent results

    Hello,

    I have an issue : In the example below there is 100 Project and 0 Formation in DB, so /testA and /testB should return the exact same thing :

        Route::get('/testA', function (Request $request) {
            return Search::addMany([
                [Project::query(), 'title'],
            ])->get('');
        });
    
        Route::get('/testB', function (Request $request) {
            return Search::addMany([
                [Project::query(), 'title'],
                [Formation::query(), 'title'],
            ])->get('');
        });
    

    But it is not the case, I don't get the results in the same order in the 2 queries, event if I add ->orderByRelevance()

    Any idea why is this ? Thank you

    opened by vwasteels 4
  • Order by relationship count

    Order by relationship count

    Hi,

    Is there any way to order by relationship count ?

    $query = User::withCount('posts');
    
    Search::new()
        ->add($query, 'email', 'posts_count')
        ->get();
    

    If I try this, I get this error:

    SQLSTATE[42S22]: Column not found: 1054 Unknown column 'users.posts_count' in 'field list' (SQL: select users.id as 0_user_key, users.posts_count as 0_user_order from users order by COALESCE(0_user_order) asc)

    I guess I have to replace the eager loaded count with a join or something like that, right ?

    opened by fredericseiler 4
  • Sort by identically matched/without wildcard on the top when use startWithWildcard

    Sort by identically matched/without wildcard on the top when use startWithWildcard

    Search::add(Post::class, 'title') ->add(Video::class, 'title') ->startWithWildcard() // Should sort identically match/without wildcard on the top. ->get('os');

    opened by KhaledLela 4
  • Dynamic Models

    Dynamic Models

    Right now it's as such

    $results = Search::add(Post::class, 'title')
        ->add(Video::class, 'title')
        ->get('howto');
    

    My users can check what models they want to search, which means the searched models are dynamic. In the above method, there's no way to dynamically search only the models selected by a user. I'm merely suggesting something that works like this.

    $models = [
            [Post::class,'title'],
            [Video::class, 'title']
        ];
    $results = Search::addMany($models)
        ->get('howto');
    

    Thanks!

    enhancement 
    opened by anubra266 4
  • Support simplePaginate()

    Support simplePaginate()

    Hi,

    I will submit this as a PR this time.. I should have time over Christmas to do it :)

    Would you be happy with an option to use simplePaginate() vs paginate()? If so, do you have any preference on how it's implemented?

    enhancement 
    opened by mewejo 4
  • type addition each row

    type addition each row

    Hello, if you can add type(class_basename or model_name) column each row it will be better solution, how we can handle when we are using API service ? On your example you are using blade but we arent. Can you please add an option to add each row to type. Thanks. @pascalbaljet

    opened by bsormagec 4
  • Models with nullable relationships do not appear in search results

    Models with nullable relationships do not appear in search results

    Perhaps the package already contains a solution to this problem, but I was not able to find it. Say you have a model (e.g. Post) with a relationship to another model (e.g. Author). In this example use case, the relationship field can be null, meaning that a post has no author. First, a simple search query:

    Search::add(Post::class, ['title'])->get();

    The query above works fine, showing all posts (including posts with no author) and allowing the user to search using the post title column. If we want to add the ability to search on the author's name, we have to add the related model column to the query using the dot notation.

    Search::add(Post::class, ['title', 'author.name'])->get();

    This query works, but appears to use an inner join behind the scenes. As a result, all posts without an author are excluded from the results. The desired behavior would be to show all posts (when no search query is used) and all matching posts (when a search query is used), including posts that lack an author but where the title is a match to the search query.

    opened by ronvdberkmortel 3
  • Searching for column in a relationship

    Searching for column in a relationship

    Imagine you have a Users table and a Posts table, where a post belongs to a user.

    How could I search at the same time for, say, all users "name" fields AND all users posts 'body' fields at the same time? In other words, how could I use this package to search for all users name that match an "X" search input or users that have a post with a body that matches that same search input?

    opened by edmilsonrobson 3
  • Sort results by nested relationship

    Sort results by nested relationship

    I want to sort main result set by relation in an order.. eg lets say a post has many tags i want to get all post but they should be sorted by their tags using eager load.

    enhancement 
    opened by spartyboy 3
  • Use for pagination only

    Use for pagination only

    Hi, and thanks for the great lib!

    I was hoping to just use it for pagination only, but it's throwing an exception with no search terms. Removing that exception, it works fine - but I wondered if there is a reason for enforcing search terms, and if you think allowing pagination only would be possible?

    I'm currently getting around it by searching the created_at column for %- which returns all results, and allows pagination only.#

    Happy to contribute a PR, but wanted to check there wasn't a reason against it first. I've not actually looked how you're doing multi-model pagination.

    opened by mewejo 3
  • "Contact" is not a valid backing value for enum App\Enums\ContactType

    Hi, superb package!

    However, I have a problem adding multiple models to the search. I think it has something to do with my implementation. When I run the following code, I get this error message: "Contact" is not a valid backing value for enum App\Enums\ContactType.

    <?php
    
    namespace App\Http\Controllers;
    
    use App\Models\Contact;
    use App\Models\Person;
    use App\Models\Zaak;
    use ProtoneMedia\LaravelCrossEloquentSearch\Search;
    use Illuminate\Http\Request;
    use Illuminate\Http\Response;
    
    class SearchController extends Controller
    {
        /**
         * Show search page
         *
         * @param  Request  $request
         * @return Response
         */
        public function __invoke(Request $request)
        {
            $zaak = Zaak::query()
                ->when($request->query('lines'), fn ($query, $lines) => $query->whereIn('lines', $lines))
                ->when($request->query('states'), fn ($query, $states) => $query->whereIn('states', $states))
                ->when($request->query('user'), fn ($query, $user) => $query->where('user_id', $user))
                ->when($request->query('country_from'), fn ($query, $country_from) => $query->where('country_from', $country_from))
                ->when($request->query('country_via'), fn ($query, $country_via) => $query->where('country_via', $country_via))
                ->when($request->query('country_to'), fn ($query, $country_to) => $query->where('country_to', $country_to))
                ->when($request->query('codes'), fn ($query, $codes) => $query->whereIn('codes', $codes));
    
            $person = Person::query()
                ->when($request->query('created_from'), fn ($query, $created_from) => $query->whereDate('created_at', '>=', $created_from))
                ->when($request->query('created_to'), fn ($query, $created_to) => $query->whereDate('created_at', '<=', $created_to));
    
            $contact = Contact::query()
                ->when($request->query('created_from'), fn ($query, $created_from) => $query->whereDate('created_at', '>=', $created_from))
                ->when($request->query('created_to'), fn ($query, $created_to) => $query->whereDate('created_at', '<=', $created_to));
    
            $results = Search::new()
                ->add($zaak, 'description', 'reference')
    
                // TODO: https://github.com/protonemedia/laravel-cross-eloquent-search/issues/74
                ->add($person, ['name', 'organisation'])
                ->add($contact, ['summary', 'description'])
    
                ->includeModelType()
                ->beginWithWildcard()
                ->paginate(
                    perPage: 10,
                    page: $request->query('page', 1)
                )
                ->search($request->query('search', 'verylongunfindablestring'));
    
            $query = $request->query();
            $results->appends($query);
    
            return view('search.index', compact('results', 'query'));
        }
    }
    

    Can you spot what went wrong?

    opened by Luca-Castelnuovo 2
  • Search on pivot table

    Search on pivot table

    @pascalbaljet Is it possible to search on pivot table?

    I have the following models (participant belongsToMany project),

    participants
        id
    
    participant_project 	
        participant_id
        project_id
        first_name
        last_name
    
    projects
        id	
    

    Getting Column not found: 1054 Unknown column 'project_participant.id' in 'field list' error when I try this; as I don't have an id column in the pivot table.

    Search::new()
        ->add(\App\Order::whereIn('project_id', $projects), 'confirmation_number')
        ->add(\App\ProjectParticipant::whereIn('project_id', $projects), ['first_name', 'last_name'])
        ->orderByRelevance()
        ->beginWithWildcard()
        ->paginate(20)
        ->search($request->search);
    

    Also, I tried using join, but not working as expected ->add(\App\Participant::join('project_participant', fn($join) => $join->on('participants.id', '=', 'project_participant.participant_id')->where('project_participant.project_id', 8)), ['project_participant.first_name', 'project_participant.last_name'])

    Is there any way to achieve this?

    needs more info 
    opened by anzalpa 1
  • limit() and offset() without using paginate

    limit() and offset() without using paginate

    Hello,

    Love this package!

    Any thoughts on how I could use the limit / offset eloquent methods vs paginate? I'm handling pagination a little differently in one project and would like to use this package but without the paginate() method.

    Thank you

    opened by iamthesoundman 0
  • Using orWhereRaw

    Using orWhereRaw

    I kinda need to search in some column that holds html stuff and I must search as

    orWhereRaw('regexp_replace(description, "<[^>]*>", "") like ?', "%{$searchTerm}%")
    ->orWhereRaw('regexp_replace(description, "<[^>]*>", "") like ?', '%' . htmlentities($searchTerm) . '%');
    

    is there any way to make it work?

    enhancement 
    opened by fabiobap 1
  • Support SQLite3

    Support SQLite3

    Hej,

    are there any plans to support SQLite3 as database driver? I am developing my apps with SQLite3 on my machine and with MySQL on the production server. So far I cannot use this package as much as I would like to. And I don't think that I would be this hard:

    • use LENGTH() instead of CHAR_LENGTH()
    • escape column names because SQLite3 does not support a number as the first character of a column name

    If you would like I could create a pull request. I only need one hint: where are the columns like 0_post_key are defined? Couldn't find it for now.

    enhancement 
    opened by MeiKatz 1
Releases(3.1.0)
Owner
Protone Media
We are a Dutch software company that develops apps, websites, and cloud platforms. As we're building projects, we gladly contribute to OSS by sharing our work.
Protone Media
This package gives Eloquent models the ability to manage their friendships.

Laravel 5 Friendships This package gives Eloquent models the ability to manage their friendships. You can easily design a Facebook like Friend System.

Alex Kyriakidis 690 Nov 27, 2022
A small package for adding UUIDs to Eloquent models.

A small package for adding UUIDs to Eloquent models. Installation You can install the package via composer: composer require ryangjchandler/laravel-uu

Ryan Chandler 40 Jun 5, 2022
An opinionated package to create slugs for Eloquent models

Generate slugs when saving Eloquent models This package provides a trait that will generate a unique slug when saving any Eloquent model. $model = new

Spatie 1.1k Jan 4, 2023
A package to generate YouTube-like IDs for Eloquent models

Laravel Hashids This package provides a trait that will generate hashids when saving any Eloquent model. Hashids Hashids is a small package to generat

Λгi 25 Aug 31, 2022
An Eloquent Way To Filter Laravel Models And Their Relationships

Eloquent Filter An Eloquent way to filter Eloquent Models and their relationships Introduction Lets say we want to return a list of users filtered by

Eric Tucker 1.5k Jan 7, 2023
Easy creation of slugs for your Eloquent models in Laravel

Eloquent-Sluggable Easy creation of slugs for your Eloquent models in Laravel. NOTE: These instructions are for the latest version of Laravel. If you

Colin Viebrock 3.6k Dec 30, 2022
Automatically validating Eloquent models for Laravel

Validating, a validation trait for Laravel Validating is a trait for Laravel Eloquent models which ensures that models meet their validation criteria

Dwight Watson 955 Dec 25, 2022
Laravel Ban simplify blocking and banning Eloquent models.

Laravel Ban Introduction Laravel Ban simplify management of Eloquent model's ban. Make any model bannable in a minutes! Use case is not limited to Use

cybercog 879 Dec 30, 2022
cybercog 996 Dec 28, 2022
Tag support for Laravel Eloquent models - Taggable Trait

Laravel Taggable Trait This package is not meant to handle javascript or html in any way. This package handles database storage and read/writes only.

Rob 859 Dec 11, 2022
Preferences for Laravel Eloquent models

Preferences for Laravel Eloquent models Use this library to bind multiple key/value pair preferences to your application's Eloquent models. Preference

Kevin Laude 32 Oct 30, 2022
Automatic human timestamps for Laravel Eloquent models.

Automatic human timestamp properties in Laravel This package provides a trait you can add to an Eloquent model that will automatically create human-re

Christopher Di Carlo 25 Jul 17, 2022
An Eloquent Way To Filter Laravel Models And Their Relationships

Eloquent Filter An Eloquent way to filter Eloquent Models and their relationships Introduction Lets say we want to return a list of users filtered by

Eric Tucker 1.5k Dec 30, 2022
🕵️ Inspect Laravel Eloquent models to collect properties, relationships and more.

??️ Eloquent Inspector Inspect Laravel Eloquent models to collect properties, relationships and more. Install Via Composer composer require cerbero/el

Andrea Marco Sartori 111 Nov 4, 2022
Laravel Nova Ban simplify blocking and banning Eloquent models.

Laravel Nova Ban Introduction Behind the scenes cybercog/laravel-ban is used. Contents Installation Usage Prepare bannable model Prepare bannable mode

cybercog 39 Sep 29, 2022
Sortable behaviour for Eloquent models

Sortable behaviour for Eloquent models This package provides a trait that adds sortable behaviour to an Eloquent model. The value of the order column

Spatie 1.2k Dec 22, 2022
Create presenters for Eloquent Models

Laravel Presentable This package allows the information to be presented in a different way by means of methods that can be defined in the model's pres

The Hive Team 67 Dec 7, 2022
Use auto generated UUID slugs to identify and retrieve your Eloquent models.

Laravel Eloquent UUID slug Summary About Features Requirements Installation Examples Compatibility table Alternatives Tests About By default, when get

Khalyomede 25 Dec 14, 2022
Record created by, updated by and deleted by on Eloquent models automatically.

quarks/laravel-auditors Record created by, updated by and deleted by (if SoftDeletes added) on Eloquent models automatically. Installation composer re

Quarks 3 Jun 13, 2022