Laravel package to search through multiple Eloquent models. Supports sorting, pagination, scoped queries, eager load relationships and searching through single or multiple columns.

Overview

Laravel Cross Eloquent Search

Latest Version on Packagist run-tests 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 8.0 + 8.1
  • MySQL 5.7+
  • Laravel 8.0

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');

Model Identifier

You can use the includeModelType to add the model type to the search result.

Search::add(Post::class, 'title')
    ->add(Video::class, 'title')
    ->includeModelType()
    ->paginate()
    ->get('foo');

// Example result with model identifier.
{
    "current_page": 1,
    "data": [
        {
            "id": 1,
            "video_id": null,
            "title": "foo",
            "published_at": null,
            "created_at": "2021-12-03T09:39:10.000000Z",
            "updated_at": "2021-12-03T09:39:10.000000Z",
            "type": "Post",
        },
        {
            "id": 1,
            "title": "foo",
            "subtitle": null,
            "published_at": null,
            "created_at": "2021-12-03T09:39:10.000000Z",
            "updated_at": "2021-12-03T09:39:10.000000Z",
            "type": "Video",
        },
    ],
    ...
}

By default, it uses the type key, but you can customize this by passing the key to the method.

Search::new()
    ->includeModelType('model_type');

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
Easily add weighted searches through model attributes and relationships

Laravel Searchable ?? Easily add weighted searches through model attributes and relationships. This package currently supports MySQL and PostgreSQL. I

H-FARM 93 Nov 1, 2022
Unmaintained: Laravel Searchy makes user driven searching easy with fuzzy search, basic string matching and more to come!

!! UNMAINTAINED !! This package is no longer maintained Please see Issue #117 Here are some links to alternatives that you may be able to use (I do no

Tom Lingham 533 Nov 25, 2022
Sphinx Search library provides SphinxQL indexing and searching features

Sphinx Search Sphinx Search library provides SphinxQL indexing and searching features. Introduction Installation Configuration (simple) Usage Search I

Ripa Club 62 Mar 14, 2022
Think-scout - A driver based solution to searching your models. Inspired By Laravel Scout

前言 whereof/think-scout根据thinkphp设计思想参考laravel/scout进行扩展 whereof/think-scout 为模型的全文搜索提供了一个简单的、基于驱动程序的解决方案。 目前,Scout 自带了一个 Elasticsearch 驱动;而编写自定义驱动程序很简

wangzhiqiang 6 Mar 18, 2022
Search among multiple models with ElasticSearch and Laravel Scout

For PHP8 support use php8 branch For Laravel Framework < 6.0.0 use 3.x branch The package provides the perfect starting point to integrate ElasticSear

Sergey Shlyakhov 592 Dec 25, 2022
Illusionist Searcher - Generates database queries based on search syntax

Illusionist Searcher Generates database queries based on search syntax. English | 中文 ✨ Features Zero configuration Compatible with laravel/scout and l

A doer with magic 2 Feb 24, 2022
Laravel search is package you can use it to make search query easy.

Laravel Search Installation First, install the package through Composer. composer require theamasoud/laravel-search or add this in your project's comp

Abdulrahman Masoud 6 Nov 2, 2022
This package offers advanced functionality for searching and filtering data in Elasticsearch.

Scout Elasticsearch Driver ?? Introducing a new Elasticsearch ecosystem for Laravel. ?? This package offers advanced functionality for searching and f

Ivan Babenko 1.2k Dec 20, 2022
Fulltext indexing and searching for Laravel

Laravel fulltext index and search This package creates a MySQL fulltext index for models and enables you to search through those. Install Install with

SWIS 171 Jan 4, 2023
A php trait to search laravel models

Searchable, a search trait for Laravel Searchable is a trait for Laravel 4.2+ and Laravel 5.0 that adds a simple search function to Eloquent Models. S

Nicolás López Jullian 2k Dec 27, 2022
Plastic is an Elasticsearch ODM and mapper for Laravel. It renders the developer experience more enjoyable while using Elasticsearch, by providing a fluent syntax for mapping, querying, and storing eloquent models.

Plastic is an Elasticsearch ODM and mapper for Laravel. It renders the developer experience more enjoyable while using Elasticsearch, by providing a f

Sleiman Sleiman 511 Dec 31, 2022
Maps Laravel Eloquent models to Elasticsearch types

Elasticquent Elasticsearch for Eloquent Laravel Models Elasticquent makes working with Elasticsearch and Eloquent models easier by mapping them to Ela

Elasticquent 1.3k Jan 4, 2023
A search package for Laravel 5.

Search Package for Laravel 5 This package provides a unified API across a variety of different full text search services. It currently supports driver

Mark Manos 354 Nov 16, 2022
Driver for Laravel Scout search package based on https://github.com/teamtnt/tntsearch

TNTSearch Driver for Laravel Scout - Laravel 5.3 - 8.0 This package makes it easy to add full text search support to your models with Laravel 5.3 to 8

TNT Studio 1k Dec 27, 2022
Laravel Searchable - This package makes it easy to get structured search from a variety of sources

This package makes it easy to get structured search from a variety of sources. Here's an example where we search through some model

Spatie 1.1k Dec 31, 2022
SphinxQL Query Builder generates SphinxQL, a SQL dialect, which is used to query the Sphinx search engine. (Composer Package)

Query Builder for SphinxQL About This is a SphinxQL Query Builder used to work with SphinxQL, a SQL dialect used with the Sphinx search engine and it'

FoolCode 318 Oct 21, 2022
This is an open source demo of smart search feature implemented with Laravel and Selectize plugin

Laravel smart search implementation See demo at: http://demos.maxoffsky.com/shop-search/ Tutorial at: http://maxoffsky.com/code-blog/laravel-shop-tuto

Maksim Surguy 215 Sep 8, 2022
Build and execute an Elasticsearch search query using a fluent PHP API

PACKAGE IN DEVELOPMENT, DO NOT USE YET Build and execute ElasticSearch queries using a fluent PHP API This package is a lightweight query builder for

Spatie 94 Dec 14, 2022
A metadata catalog and search engine for geospatialized data

resto is a metadata catalog and a search engine dedicated to geospatialized data. Originally, it’s main purpose it to handle Earth Observation satellite imagery but it can be used to store any kind of metadata localized in time and space.

Jerome Gasperi 50 Nov 17, 2022