Update v1.0.6

This commit is contained in:
Bhanu Slathia
2016-02-16 23:24:52 +05:30
parent c710c20b9e
commit b1f62846ab
7662 changed files with 1361647 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Nicolas Lopez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,196 @@
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.
Searchable allows you to perform searches in a table giving priorities to each field for the table and it's relations.
This is not optimized for big searches, but sometimes you just need to make it simple (Although it is not slow).
# Installation
Simply add the package to your `composer.json` file and run `composer update`.
```
"nicolaslopezj/searchable": "1.*"
```
# Usage
Add the trait to your model and your search rules.
```php
use Nicolaslopezj\Searchable\SearchableTrait;
class User extends \Eloquent
{
use SearchableTrait;
/**
* Searchable rules.
*
* @var array
*/
protected $searchable = [
'columns' => [
'users.first_name' => 10,
'users.last_name' => 10,
'users.bio' => 2,
'users.email' => 5,
'posts.title' => 2,
'posts.body' => 1,
],
'joins' => [
'posts' => ['users.id','posts.user_id'],
],
];
public function posts()
{
return $this->hasMany('Post');
}
}
```
Now you can search your model.
```php
// Simple search
$users = User::search($query)->get();
// Search and get relations
// It will not get the relations if you don't do this
$users = User::search($query)
->with('posts')
->get();
```
## Search Paginated
As easy as laravel default queries
```php
// Search with relations and paginate
$users = User::search($query)
->with('posts')
->paginate(20);
```
## Mix queries
Search method is compatible with any eloquent method. You can do things like this:
```php
// Search only active users
$users = User::where('status', 'active')
->search($query)
->paginate(20);
```
## Custom Threshold
The default threshold for accepted relevance is the sum of all attribute relevance divided by 4.
To change this value you can pass in a second parameter to search() like so:
```php
// Search with lower relevance threshold
$users = User::where('status', 'active')
->search($query, 0)
->paginate(20);
```
The above, will return all users in order of relevance.
# How does it work?
Searchable builds a query that search through your model using Laravel's Eloquent.
Here is an example query
####Eloquent Model:
```php
use Nicolaslopezj\Searchable\SearchableTrait;
class User extends \Eloquent
{
use SearchableTrait;
/**
* Searchable rules.
*
* @var array
*/
protected $searchable = [
'columns' => [
'first_name' => 10,
'last_name' => 10,
'bio' => 2,
'email' => 5,
],
];
}
```
####Search:
```php
$search = User::search('Sed neque labore', null, true)->get();
```
####Result:
```sql
select `users`.*,
-- If third parameter is set as true, it will check if the column starts with the search
-- if then it adds relevance * 30
-- this ensures that relevant results will be at top
(case when first_name LIKE 'Sed neque labore%' then 300 else 0 end) +
-- For each column you specify makes 3 "ifs" containing
-- each word of the search input and adds relevace to
-- the row
-- The first checks if the column is equal to the word,
-- if then it adds relevance * 15
(case when first_name LIKE 'Sed' || first_name LIKE 'neque' || first_name LIKE 'labore' then 150 else 0 end) +
-- The second checks if the column starts with the word,
-- if then it adds relevance * 5
(case when first_name LIKE 'Sed%' || first_name LIKE 'neque%' || first_name LIKE 'labore%' then 50 else 0 end) +
-- The third checks if the column contains the word,
-- if then it adds relevance * 1
(case when first_name LIKE '%Sed%' || first_name LIKE '%neque%' || first_name LIKE '%labore%' then 10 else 0 end) +
-- Repeats with each column
(case when last_name LIKE 'Sed' || last_name LIKE 'neque' || last_name LIKE 'labore' then 150 else 0 end) +
(case when last_name LIKE 'Sed%' || last_name LIKE 'neque%' || last_name LIKE 'labore%' then 50 else 0 end) +
(case when last_name LIKE '%Sed%' || last_name LIKE '%neque%' || last_name LIKE '%labore%' then 10 else 0 end) +
(case when bio LIKE 'Sed' || bio LIKE 'neque' || bio LIKE 'labore' then 30 else 0 end) +
(case when bio LIKE 'Sed%' || bio LIKE 'neque%' || bio LIKE 'labore%' then 10 else 0 end) +
(case when bio LIKE '%Sed%' || bio LIKE '%neque%' || bio LIKE '%labore%' then 2 else 0 end) +
(case when email LIKE 'Sed' || email LIKE 'neque' || email LIKE 'labore' then 75 else 0 end) +
(case when email LIKE 'Sed%' || email LIKE 'neque%' || email LIKE 'labore%' then 25 else 0 end) +
(case when email LIKE '%Sed%' || email LIKE '%neque%' || email LIKE '%labore%' then 5 else 0 end)
as relevance
from `users`
group by `id`
-- Selects only the rows that have more than
-- the sum of all attributes relevances and divided by 4
-- Ej: (20 + 5 + 2) / 4 = 6.75
having relevance > 6.75
-- Orders the results by relevance
order by `relevance` desc
```
## Contributing
Anyone is welcome to contribute. Fork, make your changes, and then submit a pull request.
[![Support via Gittip](https://rawgithub.com/twolfson/gittip-badge/0.2.0/dist/gittip.png)](https://gratipay.com/nicolaslopezj/)

View File

@@ -0,0 +1,23 @@
{
"name": "nicolaslopezj/searchable",
"description": "Eloquent model search trait.",
"keywords": ["laravel", "eloquent", "search", "searchable", "database", "model"],
"license": "MIT",
"authors": [
{
"name": "Nicolas Lopez",
"email": "nicolaslopezj@me.com"
}
],
"require": {
"php": ">=5.4.0",
"illuminate/database": "4.2.x|~5.0",
"ext-mbstring": "*"
},
"autoload": {
"psr-4": {
"Nicolaslopezj\\Searchable\\": "src/"
}
},
"minimum-stability": "stable"
}

View File

@@ -0,0 +1,329 @@
<?php namespace Nicolaslopezj\Searchable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/**
* Trait SearchableTrait
* @package Nicolaslopezj\Searchable
* @property array $searchable
* @property string $table
* @property string $primaryKey
* @method string getTable()
*/
trait SearchableTrait
{
/**
* @var array
*/
protected $search_bindings = [];
/**
* Creates the search scope.
*
* @param \Illuminate\Database\Eloquent\Builder $q
* @param string $search
* @param float|null $threshold
* @param boolean $entireText
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeSearch(Builder $q, $search, $threshold = null, $entireText = false)
{
return $this->scopeSearchRestricted($q, $search, null, $threshold, $entireText);
}
public function scopeSearchRestricted(Builder $q, $search, $restriction, $threshold = null, $entireText = false)
{
$query = clone $q;
$query->select($this->getTable() . '.*');
$this->makeJoins($query);
if ( ! $search)
{
return $q;
}
$search = mb_strtolower(trim($search));
$words = explode(' ', $search);
$selects = [];
$this->search_bindings = [];
$relevance_count = 0;
foreach ($this->getColumns() as $column => $relevance)
{
$relevance_count += $relevance;
$queries = $this->getSearchQueriesForColumn($query, $column, $relevance, $words);
if ( $entireText === true )
{
$queries[] = $this->getSearchQuery($query, $column, $relevance, [$search], 30, '', '%');
}
foreach ($queries as $select)
{
$selects[] = $select;
}
}
$this->addSelectsToQuery($query, $selects);
// Default the threshold if no value was passed.
if (is_null($threshold)) {
$threshold = $relevance_count / 4;
}
$this->filterQueryWithRelevance($query, $selects, $threshold);
$this->makeGroupBy($query);
$this->addBindingsToQuery($query, $this->search_bindings);
if(is_callable($restriction)) {
$query = $restriction($query);
}
$this->mergeQueries($query, $q);
return $q;
}
/**
* Returns database driver Ex: mysql, pgsql, sqlite.
*
* @return array
*/
protected function getDatabaseDriver() {
$key = $this->connection ?: Config::get('database.default');
return Config::get('database.connections.' . $key . '.driver');
}
/**
* Returns the search columns.
*
* @return array
*/
protected function getColumns()
{
if (array_key_exists('columns', $this->searchable)) {
return $this->searchable['columns'];
} else {
return DB::connection()->getSchemaBuilder()->getColumnListing($this->table);
}
}
/**
* Returns whether or not to keep duplicates.
*
* @return array
*/
protected function getGroupBy()
{
if (array_key_exists('groupBy', $this->searchable)) {
return $this->searchable['groupBy'];
}
return false;
}
/**
* Returns the table columns.
*
* @return array
*/
public function getTableColumns()
{
return $this->searchable['table_columns'];
}
/**
* Returns the tables that are to be joined.
*
* @return array
*/
protected function getJoins()
{
return array_get($this->searchable, 'joins', []);
}
/**
* Adds the sql joins to the query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
*/
protected function makeJoins(Builder $query)
{
foreach ($this->getJoins() as $table => $keys) {
$query->leftJoin($table, function ($join) use ($keys) {
$join->on($keys[0], '=', $keys[1]);
if (array_key_exists(2, $keys) && array_key_exists(3, $keys)) {
$join->where($keys[2], '=', $keys[3]);
}
});
}
}
/**
* Makes the query not repeat the results.
*
* @param \Illuminate\Database\Eloquent\Builder $query
*/
protected function makeGroupBy(Builder $query)
{
if ($groupBy = $this->getGroupBy()) {
$query->groupBy($groupBy);
} else {
$driver = $this->getDatabaseDriver();
if ($driver == 'sqlsrv') {
$columns = $this->getTableColumns();
} else {
$columns = $this->getTable() . '.' .$this->primaryKey;
}
$query->groupBy($columns);
$joins = array_keys(($this->getJoins()));
foreach ($this->getColumns() as $column => $relevance) {
array_map(function ($join) use ($column, $query) {
if (Str::contains($column, $join)) {
$query->groupBy($column);
}
}, $joins);
}
}
}
/**
* Puts all the select clauses to the main query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $selects
*/
protected function addSelectsToQuery(Builder $query, array $selects)
{
$selects = new Expression('max(' . implode(' + ', $selects) . ') as relevance');
$query->addSelect($selects);
}
/**
* Adds the relevance filter to the query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $selects
* @param float $relevance_count
*/
protected function filterQueryWithRelevance(Builder $query, array $selects, $relevance_count)
{
$comparator = $this->getDatabaseDriver() != 'mysql' ? implode(' + ', $selects) : 'relevance';
$relevance_count=number_format($relevance_count,2,'.','');
$query->havingRaw("$comparator > $relevance_count");
$query->orderBy('relevance', 'desc');
// add bindings to postgres
}
/**
* Returns the search queries for the specified column.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $column
* @param float $relevance
* @param array $words
* @return array
*/
protected function getSearchQueriesForColumn(Builder $query, $column, $relevance, array $words)
{
$queries = [];
$queries[] = $this->getSearchQuery($query, $column, $relevance, $words, 15);
$queries[] = $this->getSearchQuery($query, $column, $relevance, $words, 5, '', '%');
$queries[] = $this->getSearchQuery($query, $column, $relevance, $words, 1, '%', '%');
return $queries;
}
/**
* Returns the sql string for the given parameters.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $column
* @param string $relevance
* @param array $words
* @param string $compare
* @param float $relevance_multiplier
* @param string $pre_word
* @param string $post_word
* @return string
*/
protected function getSearchQuery(Builder $query, $column, $relevance, array $words, $relevance_multiplier, $pre_word = '', $post_word = '')
{
$like_comparator = $this->getDatabaseDriver() == 'pgsql' ? 'ILIKE' : 'LIKE';
$cases = [];
foreach ($words as $word)
{
$cases[] = $this->getCaseCompare($column, $like_comparator, $relevance * $relevance_multiplier);
$this->search_bindings[] = $pre_word . $word . $post_word;
}
return implode(' + ', $cases);
}
/**
* Returns the comparison string.
*
* @param string $column
* @param string $compare
* @param float $relevance
* @return string
*/
protected function getCaseCompare($column, $compare, $relevance) {
if($this->getDatabaseDriver() == 'pgsql') {
$field = "LOWER(" . $column . ") " . $compare . " ?";
return '(case when ' . $field . ' then ' . $relevance . ' else 0 end)';
}
$column = str_replace('.', '`.`', $column);
$field = "LOWER(`" . $column . "`) " . $compare . " ?";
return '(case when ' . $field . ' then ' . $relevance . ' else 0 end)';
}
/**
* Adds the bindings to the query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $bindings
*/
protected function addBindingsToQuery(Builder $query, array $bindings) {
$count = $this->getDatabaseDriver() != 'mysql' ? 2 : 1;
for ($i = 0; $i < $count; $i++) {
foreach($bindings as $binding) {
$type = $i == 0 ? 'select' : 'having';
$query->addBinding($binding, $type);
}
}
}
/**
* Merge our cloned query builder with the original one.
*
* @param \Illuminate\Database\Eloquent\Builder $clone
* @param \Illuminate\Database\Eloquent\Builder $original
*/
protected function mergeQueries(Builder $clone, Builder $original) {
$tableName = DB::connection($this->connection)->getTablePrefix() . $this->getTable();
if ($this->getDatabaseDriver() == 'pgsql') {
$original->from(DB::connection($this->connection)->raw("({$clone->toSql()}) as {$tableName}"));
} else {
$original->from(DB::connection($this->connection)->raw("({$clone->toSql()}) as `{$tableName}`"));
}
$original->mergeBindings($clone->getQuery());
}
}