Git Product home page Git Product logo

searchable's Introduction

Searchable

Pattern-matching search for Laravel eloquent models.

  • Currently supports MySQL only.
  • Helpful for complex table queries with multiple joins and derived columns.
  • Fluent columns definitions.

Demo Project

See demo project.

Overview

Simple setup for searchable model and can search on derived columns.

use AjCastro\Searchable\Searchable;

class Post
{
    use Searchable;

    protected $searchable = [
        // This will search on the defined searchable columns
        'columns' => [
            'posts.title',
            'posts.body',
            'author_full_name' => 'CONCAT(authors.first_name, " ", authors.last_name)'
        ],
        'joins' => [
            'authors' => ['authors.id', 'posts.author_id']
        ]
    ];

    public function author()
    {
        return $this->belongsTo(Author::class);
    }
}

// Usage
Post::search("Some title or body content or even the author's full name")
    ->with('author')
    ->paginate();

Imagine we have an api for a table or list that has searching and column sorting and pagination. This is a usual setup for a table or list. The internal explanations will be available on the documentation below. Our api call may look like this:

http://awesome-app.com/api/posts?per_page=10&page=1&sort_by=title&descending=true&search=SomePostTitle

Your code can look like this:

class PostsController
{
    public function index(Request $request)
    {
        $query = Post::query();

        return $query
            ->with('author')
            // advance usage with custom search string parsing
            ->when($request->parse_using === 'exact', function ($query) {
                $query->parseUsing(function ($searchStr) {
                    return "%{$searchStr}%";
                });
            })
            ->search($request->search)
            ->when(
                $request->has('sort_by') && $query->getModel()->isColumnValid($request->sort_by),
                function ($query) use ($request) {
                    $query->orderBy(
                        DB::raw($query->getModel()->getColumn($request->sort_by)),
                        $request->descending ? 'desc' : 'asc'
                    );
                },
                function ($query) {
                    $query->sortByRelevance();
                },
            )
            ->paginate();
    }

}

Documentation

Installation

composer require ajcastro/searchable

Searchable Model

use AjCastro\Searchable\Searchable;

class Post extends Model
{
    use Searchable;

    /**
     * Searchable model definitions.
     */
     protected $searchable = [
        // Searchable columns of the model.
        // If this is not defined it will default to all table columns.
        'columns' => [
            'posts.title',
            'posts.body',
            'author_full_name' => 'CONCAT(authors.first_name, " ", authors.last_name)'
        ],
        // This is needed if there is a need to join other tables for derived columns.
        'joins' => [
            'authors' => ['authors.id', 'posts.author_id'], // defaults to leftJoin method of eloquent builder
            'another_table' => ['another_table.id', 'authors.another_table_id', 'join'], // can pass leftJoin, rightJoin, join of eloquent builder.
        ]
    ];

    /**
     * Can also be written like this for searchable columns.
     *
     * @var array
     */
    protected $searchableColumns = [
        'title',
        'body',
        'author_full_name' => 'CONCAT(authors.first_name, " ", authors.last_name)'
    ];

    /**
     * Can also be written like this for searchable joins.
     *
     * @var array
     */
    protected $searchableJoins = [
        'authors' => ['authors.id', 'posts.author_id']
    ];
}

// Usage
// Call search anywhere
// This only search on the defined columns.
Post::search('Some post')->paginate();
Post::where('likes', '>', 100)->search('Some post')->paginate();

If you want to sort by relevance, call method sortByRelevance() after search() method. This will addSelect field sort_index which will be used to order or sort by relevance.

Example:

Post::search('Some post')->sortByRelevance()->paginate();
Post::where('likes', '>', 100)->search('Some post')->sortByRelevance()->paginate();

Set searchable configurations on runtime.

$post = new Post;
$post->setSearchable([ // addSearchable() method is also available
    'columns' => [
        'posts.title',
        'posts.body',
    ],
    'joins' => [
        'authors' => ['authors.id', 'posts.author_id']
    ]
]);
// or
$post->setSearchableColumns([ // addSearchableColumns() method is also available
    'posts.title',
    'posts.body',
]);
$post->setSearchableJoins([ // addSearchableJoins() method is also available
    'authors' => ['authors.id', 'posts.author_id']
]);

Easy Sortable Columns

You can define columns to be only sortable but not be part of search query constraint. Just put it under sortable_columns as shown below . This column can be easily access to put in orderBy of query builder. All searchable columns are also sortable columns.

class Post {
     protected $searchable = [
        'columns' => [
            'title' => 'posts.title',
        ],
        'sortable_columns' => [
            'status_name' => 'statuses.name',
        ],
        'joins' => [
            'statuses' => ['statuses.id', 'posts.status_id']
        ]
    ];
}

// Usage

Post::search('A post title')->orderBy(Post::make()->getSortableColumn('status_name'));
// This will only perform search on `posts`.`title` column and it will append "order by `statuses`.`name`" in the query.
// This is beneficial if your column is mapped to a different column name coming from front-end request.

Custom Search String Parser - Exact Search Example

Override the deafultSearchQuery in the model like so:

use AjCastro\Searchable\BaseSearch;

class User extends Model
{
    public function defaultSearchQuery()
    {
        return BaseSearch::make($this->buildSearchableColumns())
            ->parseUsing(function ($searchStr) {
                return $searchStr; // produces "where `column` like '{$searchStr}'"
                return "%{$searchStr}%"; // produces "where `column` like '%{$searchStr}%'"
            });
    }
}

You may also check the build query by dd-ing it:

$query = User::search('John Doe');
dd($query->toSql());

which may output to

select * from users where `column` like 'John Doe'
// or
select * from users where `column` like '%John Doe%'

Using derived columns for order by and where conditions

Usually we have queries that has a derived columns like our example for Post's author_full_name. Sometimes we need to sort our query results by this column.

$query = Post::query();
$post = $query->getModel();
// (A)
$query->search('Some search')->orderBy($post->getColumn('author_full_name'), 'desc')->paginate();
// (B)
$query->search('Some search')->where($post->getColumn('author_full_name'), 'William%')->paginate();

which may output to

-- (A)
select * from posts where ... order by CONCAT(authors.first_name, " ", authors.last_name) desc limit 0, 15;
-- (B)
select * from posts where ... and CONCAT(authors.first_name, " ", authors.last_name) like 'William%' limit 0, 15;

Helper methods available

TableColumns::get() [static]

  • Get the table columns.
TableColumns::get('posts');

isColumnValid

  • Identifies if the column is a valid column, either a regular table column or derived column.
  • Useful for checking valid columns to avoid sql injection especially in orderBy query, see post.
$query->getModel()->isColumnValid(request('sort_by'));

enableSearchable

  • Enable the searchable behavior.
$query->getModel()->enableSearchable();
$query->search('foo');

disableSearchable

  • Disable the searchable behavior.
  • Calling search() method will not perform a search.
$query->getModel()->disableSearchable();
$query->search('foo');

setSearchable

  • Set or override the model's $searchable property.
  • Useful for building searchable config on runtime.
$query->getModel()->setSearchable([
  'columns' => ['title', 'status'],
  'joins' => [...],
]);
$query->search('foo');

addSearchable

  • Add columns or joins in the model's $searchable property.
  • Useful for building searchable config on runtime.
$query->getModel()->addSearchable([
  'columns' => ['title', 'status'],
  'joins' => [...],
]);
$query->search('foo');

Warning

Calling select() after search() will overwrite sort_index field, so it is recommended to call select() before search(). Or you can use addSelect() instead.

Credits

searchable's People

Contributors

ajcastro avatar raymadrona avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

searchable's Issues

Searchable relationship return many values

When I indicate a table join and the table contains several values, I get in search response the elements multiplied by the number of occurrences of the join table. Is there a method to declare the type of join? (Left, Right, Inner)? Thank you

protected $searchable = [
        'columns' => [
            'listings.title',
            'listings.description',
            'categories.title',
            'keywords.title',
            'author_full_name' => 'CONCAT(users.firstname, " ", users.lastname)'
        ],
        'joins' => [
            'users' => ['users.id', 'listings.user_id'],
            'categories' => ['categories.id', 'listings.category_id'],
            'keywords' => ['keywords.listing_id', 'listings.id'] // SUCKS HERE (i get 10 records the same in search response)
        ]
    ];

A way to exact match ?

I have successfully implemented your package and I am very happy with it but the search results are not strict enough. Is there a method to make an exact mactch of what we type in the search? Your package is intended to find trades. Suddenly if I currently type: "gardener", the search also sends me booksellers when no occurrence of this word exists in the text.

Tests carried out with several records and only one.
Thank you

Broken search string?

I'm not sure how this was supposed to work, but if I search for TEST, MySQL query is %T%E%S%T%—which looks for those letters.

In SublimeSearch::parseSearchStr, I see:

'%'.join('%', str_split($searchStr)).'%';

What's the purpose of this?

Example searching via relations attributes

The structure of my database is as follows:
Table listings:

protected $ fillable = [
        'user_id',
        'category_id',
        'title',
        'slug',
        'description',
        'address',
        'number',
        'city',
        'latitude',
        'longitude',
        'phone',
        'E-mail',
        'website',
        'Facebook',
        'twitter',
        'Youtube',
        'has_timetable',
        'timetable',
        'cover',
        'gallery',
        'is_verified',
        'in_postmoderation',
        'is_featured',
        'status'
    ];

Table Keywords:

protected $ fillable = [
        'listing_id',
        'title',
        'slug'
    ];

I query the keywords table against the listing_id to find out if there are keywords for the listing table.

Suddenly my searchable results gives me more results than expected. 11 results for 4 real records in the listings table). Changing the join method does not change anything. If I apply distinct () in the SQL request, the elements are well filtered but the response for:

$ listings = $ results-> paginate (1); // test
$ listings-> lastPage (); // => results 11 (not 4).

I guess if I apply a groupBy it will solve the problem but goupBy seems not to want to work.

I have the following error if I apply as follows:

LISTING MODEL

protected function applySearchableJoins ($ query)
    {
        foreach ($ this-> searchableJoins () as $ table => $ join) {
            $ joinMethod = $ join [2] ?? 'leftJoin';
            $ query -> {$ joinMethod} ($ table, $ join [0], '=', $ join [1]) -> groupBy ('listings.id'); // id or other fields not work
        }
    }

ERROR
SQLSTATE [42000]: Syntax error or access violation: 1055 Expression # 29 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'hybrisdev3.keywords.title' which is not functionally dependent on columns in GROUP BY

SQL
select listings. *, (LOCATE ('b', CONCAT (IFNULL ((listings.title), ''), IFNULL ((listings.description), ''), IFNULL ((categories.title), '' ), IFNULL ((keywords.title), ''), IFNULL ((CONCAT (users.firstname, "", users.lastname)), '')), 1) + LOCATE ('o', CONCAT (IFNULL ( (listings.title), ''), IFNULL ((listings.description), ''), IFNULL ((categories.title), ''), IFNULL ((keywords.title), ''), IFNULL ((CONCAT (users.firstname, "", users.lastname)), '')), 2) + LOCATE ('u', CONCAT (IFNULL ((listings.title), ''), IFNULL ((listings.description), ''), IFNULL ((categories.title), ''), IFNULL ((keywords.title), ''), IFNULL ((CONCAT (users.firstname, "", users.lastname)), '')) , 3) + LOCATE ('l', CONCAT (IFNULL ((listings.title), ''), IFNULL ((listings.description), ''), IFNULL ((categories.title), ''), IFNULL ( (keywords.title), ''), IFNULL ((CONCAT (users.firstname, "", users.lastname)), '')), 4) + LOCATE ('a', CONCAT (IFNULL ((listings.title ), ''), IFNULL ((listings.description), ''), IFNULL ((categories.title), ''), IFNULL ((keywords.title), ''), IFNULL ( (CONCAT (users.firstname, "", users.lastname)), '')), 5) + LOCATE ('n', CONCAT (IFNULL ((listings.title), ''), IFNULL ((listings.description ), ''), IFNULL ((categories.title), ''), IFNULL ((keywords.title), ''), IFNULL ((CONCAT (users.firstname, "", users.lastname)), '' ))), 6) + LOCATE ('g', CONCAT (IFNULL ((listings.title), ''), IFNULL ((listings.description), ''), IFNULL ((categories.title), ''), IFNULL ((keywords.title), ''), IFNULL ((CONCAT (users.firstname, "", users.lastname)), '')), 7) + LOCATE ('e', CONCAT (IFNULL ((listings .title), ''), IFNULL ((listings.description), ''), IFNULL ((categories.title), ''), IFNULL ((keywords.title), ''), IFNULL ((CONCAT (users .firstname, "", users.lastname)), '')), 8) + LOCATE ('r', CONCAT (IFNULL ((listings.title), ''), IFNULL ((listings.description), '' ), IFNULL ((categories.title), ''), IFNULL ((keywords.title), ''), IFNULL ((CONCAT (users.firstname, "", users.lastname)), '')), 9 ) + LOCATE ('i', CONCAT (IFNULL ((listings.title), ''), IFNULL ((listings.description), ''), IFNULL ((categories.title), ''), I FNULL ((keywords.title), ''), IFNULL ((CONCAT (users.firstname, "", users.lastname)), '')), 10) + LOCATE ('e', CONCAT (IFNULL ((listings .title), ''), IFNULL ((listings.description), ''), IFNULL ((categories.title), ''), IFNULL ((keywords.title), ''), IFNULL ((CONCAT (users .firstname, "", users.lastname)), '')), 11)) AS sort_index from listings left join users on users.id = listings.user_id left join categories on categories.id = listings.category_id left join keywords on keywords.listing_id = listings.id where listings.status =? and (listings.title like "% boulangerie%" OR listings.description like "% boulangerie%" OR categories.title like "% boulangerie%" OR keywords.title like "% boulangerie%" OR CONCAT (users.firstname, "" , users.lastname) like "% boulangerie%") and listings.deleted_at is null group by listings.id, listings.id, listings.id order by sort_index asc

LISTING MODEL

protected $searchable = [
        'columns' => [
            'listings.title',
            'listings.description',
            'categories.title',
            'keywords.title',
            'author_full_name' => 'CONCAT(users.firstname, " ", users.lastname)'
        ],
        'joins' => [
            'users' => ['users.id', 'listings.user_id'],
            'categories' => ['categories.id', 'listings.category_id'],
            'keywords' => ['keywords.listing_id', 'listings.id']
        ]
    ];

How I can get only 4 record s as expected without a groupBy method OR how i can apply the groupBy on the search query ? Thank you

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.