James Brooks
9 years ago
40 changed files with 753 additions and 723 deletions
-
8.gitattributes
-
4composer.json
-
25config/searchy.php
-
21src/Facades/Searchy.php
-
16src/Interfaces/MatcherInterface.php
-
12src/Interfaces/SearchDriverInterface.php
-
30src/Matchers/AcronymMatcher.php
-
36src/Matchers/BaseMatcher.php
-
44src/Matchers/ConsecutiveCharactersMatcher.php
-
16src/Matchers/ExactMatcher.php
-
28src/Matchers/InStringMatcher.php
-
24src/Matchers/LevenshteinMatcher.php
-
28src/Matchers/StartOfStringMatcher.php
-
28src/Matchers/StartOfWordsMatcher.php
-
35src/Matchers/StudlyCaseMatcher.php
-
28src/Matchers/TimesInStringMatcher.php
-
111src/SearchBuilder.php
-
154src/SearchDrivers/BaseSearchDriver.php
-
20src/SearchDrivers/FuzzySearchDriver.php
-
13src/SearchDrivers/LevenshteinSearchDriver.php
-
15src/SearchDrivers/SimpleSearchDriver.php
-
56src/SearchyServiceProvider.php
-
19src/TomLingham/Searchy/Facades/Searchy.php
-
14src/TomLingham/Searchy/Interfaces/MatcherInterface.php
-
12src/TomLingham/Searchy/Interfaces/SearchDriverInterface.php
-
28src/TomLingham/Searchy/Matchers/AcronymMatcher.php
-
34src/TomLingham/Searchy/Matchers/BaseMatcher.php
-
40src/TomLingham/Searchy/Matchers/ConsecutiveCharactersMatcher.php
-
28src/TomLingham/Searchy/Matchers/InStringMatcher.php
-
25src/TomLingham/Searchy/Matchers/LevenshteinMatcher.php
-
28src/TomLingham/Searchy/Matchers/StartOfStringMatcher.php
-
28src/TomLingham/Searchy/Matchers/StartOfWordsMatcher.php
-
34src/TomLingham/Searchy/Matchers/StudlyCaseMatcher.php
-
110src/TomLingham/Searchy/SearchBuilder.php
-
147src/TomLingham/Searchy/SearchDrivers/BaseSearchDriver.php
-
19src/TomLingham/Searchy/SearchDrivers/FuzzySearchDriver.php
-
12src/TomLingham/Searchy/SearchDrivers/LevenshteinSearchDriver.php
-
14src/TomLingham/Searchy/SearchDrivers/SimpleSearchDriver.php
-
52src/TomLingham/Searchy/SearchyServiceProvider.php
-
28src/config/config.php
@ -0,0 +1,8 @@ |
|||
* text=auto |
|||
|
|||
/tests export-ignore |
|||
/.gitattributes export-ignore |
|||
/.gitignore export-ignore |
|||
/.travis.yml export-ignore |
|||
/phpunit.xml export-ignore |
|||
/README.md export-ignore |
@ -0,0 +1,25 @@ |
|||
<?php |
|||
|
|||
return [ |
|||
|
|||
'default' => 'fuzzy', |
|||
|
|||
'fieldName' => 'relevance', |
|||
|
|||
'drivers' => [ |
|||
|
|||
'fuzzy' => [ |
|||
'class' => 'TomLingham\Searchy\SearchDrivers\FuzzySearchDriver', |
|||
], |
|||
|
|||
'simple' => [ |
|||
'class' => 'TomLingham\Searchy\SearchDrivers\SimpleSearchDriver', |
|||
], |
|||
|
|||
'levenshtein' => [ |
|||
'class' => 'TomLingham\Searchy\SearchDrivers\LevenshteinSearchDriver', |
|||
], |
|||
|
|||
], |
|||
|
|||
]; |
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\Facades; |
|||
|
|||
use Illuminate\Support\Facades\Facade; |
|||
|
|||
/** |
|||
* Searchy facade for the Laravel framework. |
|||
*/ |
|||
class Searchy extends Facade |
|||
{ |
|||
/** |
|||
* Get the registered component. |
|||
* |
|||
* @return object |
|||
*/ |
|||
protected static function getFacadeAccessor() |
|||
{ |
|||
return 'searchy'; |
|||
} |
|||
} |
@ -0,0 +1,16 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\Interfaces; |
|||
|
|||
interface MatcherInterface |
|||
{ |
|||
/** |
|||
* Builds the string to add to the SELECT statement for the Matcher. |
|||
* |
|||
* @param $column |
|||
* @param $searchString |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public function buildQueryString($column, $searchString); |
|||
} |
@ -0,0 +1,12 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\Interfaces; |
|||
|
|||
interface SearchDriverInterface |
|||
{ |
|||
public function query($searchString); |
|||
|
|||
public function select(/* $columns */); |
|||
|
|||
public function get(); |
|||
} |
@ -0,0 +1,30 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches strings for Acronym 'like' matches but does NOT return Studly Case Matches. |
|||
* |
|||
* for example, a search for 'fb' would match; 'foo bar' or 'Fred Brown' but not 'FreeBeer'. |
|||
* |
|||
* Class AcronymMatcher |
|||
*/ |
|||
class AcronymMatcher extends BaseMatcher |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $operator = 'LIKE'; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* |
|||
* @return mixed|string |
|||
*/ |
|||
public function formatSearchString($searchString) |
|||
{ |
|||
$searchString = preg_replace('/[^0-9a-zA-Z]/', '', $searchString); |
|||
|
|||
return implode('% ', str_split(strtoupper($searchString))).'%'; |
|||
} |
|||
} |
@ -0,0 +1,36 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\Matchers; |
|||
|
|||
use TomLingham\Searchy\Interfaces\MatcherInterface; |
|||
|
|||
/** |
|||
* @property mixed multiplier |
|||
* @property mixed operator |
|||
*/ |
|||
abstract class BaseMatcher implements MatcherInterface |
|||
{ |
|||
protected $multiplier; |
|||
|
|||
public function __construct($multiplier) |
|||
{ |
|||
$this->multiplier = $multiplier; |
|||
} |
|||
|
|||
/** |
|||
* The default process for building the Query string. |
|||
* |
|||
* @param $column |
|||
* @param $searchString |
|||
* |
|||
* @return mixed|string |
|||
*/ |
|||
public function buildQueryString($column, $searchString) |
|||
{ |
|||
if (method_exists($this, 'formatSearchString')) { |
|||
$searchString = $this->formatSearchString($searchString); |
|||
} |
|||
|
|||
return "IF($column {$this->operator} '$searchString', {$this->multiplier}, 0)"; |
|||
} |
|||
} |
@ -0,0 +1,44 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches strings that include all the characters in the search relatively position within the string. |
|||
* It also calculates the percentage of characters in the string that are matched and applies the multiplier accordingly. |
|||
* |
|||
* For Example, a search for 'fba' would match; 'Foo Bar' or 'Afraid of bats' |
|||
* |
|||
* Class ConsecutiveCharactersMatcher |
|||
*/ |
|||
class ConsecutiveCharactersMatcher extends BaseMatcher |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $operator = 'LIKE'; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString($searchString) |
|||
{ |
|||
$searchString = preg_replace('/[^0-9a-zA-Z]/', '', $searchString); |
|||
|
|||
return '%'.implode('%', str_split($searchString)).'%'; |
|||
} |
|||
|
|||
/** |
|||
* @param $column |
|||
* @param $rawString |
|||
* |
|||
* @return mixed|string |
|||
*/ |
|||
public function buildQueryString($column, $rawString) |
|||
{ |
|||
$searchString = $this->formatSearchString($rawString); |
|||
|
|||
return "IF(REPLACE($column, '\.', '') {$this->operator} '$searchString', ROUND({$this->multiplier} * (CHAR_LENGTH( '$rawString' ) / CHAR_LENGTH( REPLACE($column, ' ', '') ))), 0)"; |
|||
} |
|||
} |
@ -0,0 +1,28 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches against any occurrences of a string within a string and is case-insensitive. |
|||
* |
|||
* For example, a search for 'smi' would match; 'John Smith' or 'Smiley Face' |
|||
* |
|||
* Class InStringMatcher |
|||
*/ |
|||
class InStringMatcher extends BaseMatcher |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $operator = 'LIKE'; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString($searchString) |
|||
{ |
|||
return "%$searchString%"; |
|||
} |
|||
} |
@ -0,0 +1,24 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches strings for Acronym 'like' matches but does NOT return Studly Case Matches. |
|||
* |
|||
* for example, a search for 'fb' would match; 'foo bar' or 'Fred Brown' but not 'FreeBeer'. |
|||
* |
|||
* Class AcronymMatcher |
|||
*/ |
|||
class LevenshteinMatcher extends BaseMatcher |
|||
{ |
|||
/** |
|||
* @param $column |
|||
* @param $searchString |
|||
* |
|||
* @return mixed|string |
|||
*/ |
|||
public function buildQueryString($column, $searchString) |
|||
{ |
|||
return "levenshtein($column, '$searchString')"; |
|||
} |
|||
} |
@ -0,0 +1,28 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches Strings that begin with the search string. |
|||
* |
|||
* For example, a search for 'hel' would match; 'Hello World' or 'helping hand' |
|||
* |
|||
* Class StartOfStringMatcher |
|||
*/ |
|||
class StartOfStringMatcher extends BaseMatcher |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $operator = 'LIKE'; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString($searchString) |
|||
{ |
|||
return "$searchString%"; |
|||
} |
|||
} |
@ -0,0 +1,28 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches the start of each word against each word in a search. |
|||
* |
|||
* For example, a search for 'jo ta' would match; 'John Taylor' or 'Joshua B. Takashi' |
|||
* |
|||
* Class StartOfWordsMatcher |
|||
*/ |
|||
class StartOfWordsMatcher extends BaseMatcher |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $operator = 'LIKE'; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString($searchString) |
|||
{ |
|||
return implode('% ', explode(' ', $searchString)).'%'; |
|||
} |
|||
} |
@ -0,0 +1,35 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches Studly Case strings using the first letters of the words only. |
|||
* |
|||
* For example a search for 'hp' would match; 'HtmlServiceProvider' or 'HashParser' but not 'hasProvider' |
|||
* |
|||
* Class StudlyCaseMatcher |
|||
*/ |
|||
class StudlyCaseMatcher extends BaseMatcher |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $operator = 'LIKE BINARY'; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString($searchString) |
|||
{ |
|||
$searchString = preg_replace('/[^0-9a-zA-Z]/', '', $searchString); |
|||
|
|||
return implode('%', str_split(strtoupper($searchString))).'%'; |
|||
} |
|||
|
|||
public function buildQueryString($column, $searchString) |
|||
{ |
|||
return "IF( CHAR_LENGTH( TRIM($column)) = CHAR_LENGTH( REPLACE( TRIM($column), ' ', '')) AND $column {$this->operator} '{$this->formatSearchString($searchString)}', {$this->multiplier}, 0)"; |
|||
} |
|||
} |
@ -0,0 +1,111 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy; |
|||
|
|||
use Illuminate\Config\Repository; |
|||
use TomLingham\Searchy\SearchDrivers\FuzzySearchDriver; |
|||
|
|||
/** |
|||
* @property mixed driverName |
|||
*/ |
|||
class SearchBuilder |
|||
{ |
|||
/** |
|||
* @var |
|||
*/ |
|||
private $table; |
|||
|
|||
/** |
|||
* @var |
|||
*/ |
|||
private $searchFields; |
|||
|
|||
/** |
|||
* @var |
|||
*/ |
|||
private $driverName; |
|||
|
|||
/** |
|||
* @var |
|||
*/ |
|||
private $config; |
|||
|
|||
public function __construct(Repository $config) |
|||
{ |
|||
$this->config = $config; |
|||
} |
|||
|
|||
/** |
|||
* @param $searchable |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function search($searchable) |
|||
{ |
|||
if (is_object($searchable) && method_exists($searchable, 'getTable')) { |
|||
$this->table = $searchable->getTable(); |
|||
} else { |
|||
$this->table = $searchable; |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @return FuzzySearchDriver |
|||
*/ |
|||
public function fields(/* $fields */) |
|||
{ |
|||
$this->searchFields = func_get_args(); |
|||
|
|||
return $this->makeDriver(); |
|||
} |
|||
|
|||
/** |
|||
* @param $driverName |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function driver($driverName) |
|||
{ |
|||
$this->driverName = $driverName; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @param $table |
|||
* @param $searchFields |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public function __call($table, $searchFields) |
|||
{ |
|||
return call_user_func_array([$this->search($table), 'fields'], $searchFields); |
|||
} |
|||
|
|||
/** |
|||
* @return mixed |
|||
*/ |
|||
private function makeDriver() |
|||
{ |
|||
$relevanceFieldName = $this->config->get('searchy.fieldName'); |
|||
|
|||
// Check if default driver is being overridden, otherwise
|
|||
// load the default
|
|||
if ($this->driverName) { |
|||
$driverName = $this->driverName; |
|||
} else { |
|||
$driverName = $this->config->get('searchy.default'); |
|||
} |
|||
|
|||
// Gets the details for the selected driver from the configuration file
|
|||
$driver = $this->config->get("searchy.drivers.$driverName")['class']; |
|||
|
|||
// Create a new instance of the selected drivers 'class' and pass
|
|||
// through table and fields to search
|
|||
$driverInstance = new $driver($this->table, $this->searchFields, $relevanceFieldName); |
|||
|
|||
return $driverInstance; |
|||
} |
|||
} |
@ -0,0 +1,154 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\SearchDrivers; |
|||
|
|||
use TomLingham\Searchy\Interfaces\SearchDriverInterface; |
|||
|
|||
abstract class BaseSearchDriver implements SearchDriverInterface |
|||
{ |
|||
protected $table; |
|||
protected $columns; |
|||
protected $searchFields; |
|||
protected $searchString; |
|||
protected $relevanceFieldName; |
|||
protected $query; |
|||
|
|||
/** |
|||
* @param null $table |
|||
* @param array $searchFields |
|||
* @param $relevanceFieldName |
|||
* @param array $columns |
|||
* |
|||
* @internal param $relevanceField |
|||
*/ |
|||
public function __construct($table = null, $searchFields = [], $relevanceFieldName, $columns = ['*']) |
|||
{ |
|||
$this->searchFields = $searchFields; |
|||
$this->table = $table; |
|||
$this->columns = $columns; |
|||
$this->relevanceFieldName = $relevanceFieldName; |
|||
} |
|||
|
|||
/** |
|||
* Specify which columns to return. |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function select() |
|||
{ |
|||
$this->columns = func_get_args(); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Specify the string that is is being searched for. |
|||
* |
|||
* @param $searchString |
|||
* |
|||
* @return \Illuminate\Database\Query\Builder|mixed|static |
|||
*/ |
|||
public function query($searchString) |
|||
{ |
|||
$this->searchString = trim(\DB::connection()->getPdo()->quote($searchString), "'"); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Get the results of the search as an Array. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function get() |
|||
{ |
|||
return $this->run()->get(); |
|||
} |
|||
|
|||
/** |
|||
* Returns an instance of the Laravel Fluent Database Query Object with the search |
|||
* queries applied. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function getQuery() |
|||
{ |
|||
return $this->run(); |
|||
} |
|||
|
|||
/** |
|||
* Runs the 'having' method directly on the Laravel Fluent Database Query Object |
|||
* and returns the instance of the object. |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public function having() |
|||
{ |
|||
return call_user_func_array([$this->run(), 'having'], func_get_args()); |
|||
} |
|||
|
|||
/** |
|||
* @return $this |
|||
*/ |
|||
protected function run() |
|||
{ |
|||
$this->query = \DB::table($this->table) |
|||
->select($this->columns) |
|||
->addSelect($this->buildSelectQuery($this->searchFields)) |
|||
->orderBy($this->relevanceFieldName, 'desc') |
|||
->having($this->relevanceFieldName, '>', 0); |
|||
|
|||
return $this->query; |
|||
} |
|||
|
|||
/** |
|||
* @param array $searchFields |
|||
* |
|||
* @return array|\Illuminate\Database\Query\Expression |
|||
*/ |
|||
protected function buildSelectQuery(array $searchFields) |
|||
{ |
|||
$query = []; |
|||
|
|||
foreach ($searchFields as $searchField) { |
|||
if (strpos($searchField, '::')) { |
|||
$concatString = str_replace('::', ", ' ', ", $searchField); |
|||
$query[] = $this->buildSelectCriteria("CONCAT({$concatString})"); |
|||
} else { |
|||
$query[] = $this->buildSelectCriteria($searchField); |
|||
} |
|||
} |
|||
|
|||
return \DB::raw(implode(' + ', $query).' AS '.$this->relevanceFieldName); |
|||
} |
|||
|
|||
/** |
|||
* @param null $searchField |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function buildSelectCriteria($searchField = null) |
|||
{ |
|||
$criteria = []; |
|||
|
|||
foreach ($this->matchers as $matcher => $multiplier) { |
|||
$criteria[] = $this->makeMatcher($searchField, $matcher, $multiplier); |
|||
} |
|||
|
|||
return implode(' + ', $criteria); |
|||
} |
|||
|
|||
/** |
|||
* @param $searchField |
|||
* @param $matcherClass |
|||
* @param $multiplier |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
protected function makeMatcher($searchField, $matcherClass, $multiplier) |
|||
{ |
|||
$matcher = new $matcherClass($multiplier); |
|||
|
|||
return $matcher->buildQueryString($searchField, $this->searchString); |
|||
} |
|||
} |
@ -0,0 +1,20 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\SearchDrivers; |
|||
|
|||
class FuzzySearchDriver extends BaseSearchDriver |
|||
{ |
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $matchers = [ |
|||
'TomLingham\Searchy\Matchers\ExactMatcher' => 100, |
|||
'TomLingham\Searchy\Matchers\StartOfStringMatcher' => 50, |
|||
'TomLingham\Searchy\Matchers\AcronymMatcher' => 42, |
|||
'TomLingham\Searchy\Matchers\ConsecutiveCharactersMatcher' => 40, |
|||
'TomLingham\Searchy\Matchers\StartOfWordsMatcher' => 35, |
|||
'TomLingham\Searchy\Matchers\StudlyCaseMatcher' => 32, |
|||
'TomLingham\Searchy\Matchers\InStringMatcher' => 30, |
|||
'TomLingham\Searchy\Matchers\TimesInStringMatcher' => 8, |
|||
]; |
|||
} |
@ -0,0 +1,13 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\SearchDrivers; |
|||
|
|||
class LevenshteinSearchDriver extends BaseSearchDriver |
|||
{ |
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $matchers = [ |
|||
'TomLingham\Searchy\Matchers\LevenshteinMatcher' => 100, |
|||
]; |
|||
} |
@ -0,0 +1,15 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy\SearchDrivers; |
|||
|
|||
class SimpleSearchDriver extends BaseSearchDriver |
|||
{ |
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $matchers = [ |
|||
'TomLingham\Searchy\Matchers\ExactMatcher' => 100, |
|||
'TomLingham\Searchy\Matchers\StartOfStringMatcher' => 50, |
|||
'TomLingham\Searchy\Matchers\InStringMatcher' => 30, |
|||
]; |
|||
} |
@ -0,0 +1,56 @@ |
|||
<?php |
|||
|
|||
namespace TomLingham\Searchy; |
|||
|
|||
use Illuminate\Support\ServiceProvider; |
|||
|
|||
class SearchyServiceProvider extends ServiceProvider |
|||
{ |
|||
/** |
|||
* Indicates if loading of the provider is deferred. |
|||
* |
|||
* @var bool |
|||
*/ |
|||
protected $defer = false; |
|||
|
|||
/** |
|||
* Register the service provider. |
|||
*/ |
|||
public function register() |
|||
{ |
|||
//
|
|||
} |
|||
|
|||
/** |
|||
* Registers searchy. |
|||
*/ |
|||
public function registerSearchy() |
|||
{ |
|||
$this->app->bindShared('searchy', function ($app) { |
|||
return new SearchBuilder($app['config']); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Loads the configuration file. |
|||
*/ |
|||
public function setupConfig() |
|||
{ |
|||
$source = realpath(__DIR__.'/../config/searchy.php'); |
|||
|
|||
if (class_exists('Illuminate\Foundation\Application', false)) { |
|||
$this->publishes([$source => config_path('searchy.php')]); |
|||
} |
|||
|
|||
$this->mergeConfigFrom($source, 'searchy'); |
|||
} |
|||
|
|||
/** |
|||
* Boot the service provider. |
|||
*/ |
|||
public function boot() |
|||
{ |
|||
$this->setupConfig(); |
|||
$this->registerSearchy(); |
|||
} |
|||
} |
@ -1,19 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\Facades; |
|||
|
|||
use Illuminate\Support\Facades\Facade; |
|||
|
|||
/** |
|||
* Searchy facade for the Laravel framework |
|||
*/ |
|||
class Searchy extends Facade |
|||
{ |
|||
/** |
|||
* Get the registered component. |
|||
* |
|||
* @return object |
|||
*/ |
|||
protected static function getFacadeAccessor() |
|||
{ |
|||
return 'searchy'; |
|||
} |
|||
} |
@ -1,14 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\Interfaces; |
|||
|
|||
interface MatcherInterface |
|||
{ |
|||
/** |
|||
* Builds the string to add to the SELECT statement for the Matcher |
|||
* |
|||
* @param $column |
|||
* @param $searchString |
|||
* @return mixed |
|||
*/ |
|||
public function buildQueryString( $column, $searchString ); |
|||
|
|||
} |
@ -1,12 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\Interfaces; |
|||
|
|||
interface SearchDriverInterface |
|||
{ |
|||
|
|||
public function query( $searchString ); |
|||
|
|||
public function select( /* $columns */ ); |
|||
|
|||
public function get(); |
|||
|
|||
} |
@ -1,28 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches strings for Acronym 'like' matches but does NOT return Studly Case Matches |
|||
* |
|||
* for example, a search for 'fb' would match; 'foo bar' or 'Fred Brown' but not 'FreeBeer'. |
|||
* |
|||
* Class AcronymMatcher |
|||
* @package TomLingham\Searchy\Matchers |
|||
*/ |
|||
|
|||
class AcronymMatcher extends BaseMatcher |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $operator = 'LIKE'; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* @return mixed|string |
|||
*/ |
|||
public function formatSearchString( $searchString ) |
|||
{ |
|||
$searchString = preg_replace('/[^0-9a-zA-Z]/', '', $searchString); |
|||
return implode( '% ', str_split(strtoupper( $searchString ))) . '%'; |
|||
} |
|||
} |
@ -1,34 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\Matchers; |
|||
|
|||
use TomLingham\Searchy\Interfaces\MatcherInterface; |
|||
|
|||
/** |
|||
* @property mixed multiplier |
|||
* @property mixed operator |
|||
*/ |
|||
abstract class BaseMatcher implements MatcherInterface |
|||
{ |
|||
|
|||
protected $multiplier; |
|||
|
|||
public function __construct( $multiplier ) |
|||
{ |
|||
$this->multiplier = $multiplier; |
|||
} |
|||
|
|||
/** |
|||
* The default process for building the Query string |
|||
* |
|||
* @param $column |
|||
* @param $searchString |
|||
* @return mixed|string |
|||
*/ |
|||
public function buildQueryString( $column, $searchString ) |
|||
{ |
|||
if ( method_exists($this, 'formatSearchString') ) |
|||
$searchString = $this->formatSearchString( $searchString ); |
|||
|
|||
return "IF($column {$this->operator} '$searchString', {$this->multiplier}, 0)"; |
|||
} |
|||
|
|||
} |
@ -1,40 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches strings that include all the characters in the search relatively position within the string. |
|||
* It also calculates the percentage of characters in the string that are matched and applies the multiplier accordingly. |
|||
* |
|||
* For Example, a search for 'fba' would match; 'Foo Bar' or 'Afraid of bats' |
|||
* |
|||
* Class ConsecutiveCharactersMatcher |
|||
* @package TomLingham\Searchy\Matchers |
|||
*/ |
|||
class ConsecutiveCharactersMatcher extends BaseMatcher |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $operator = 'LIKE'; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString( $searchString ) |
|||
{ |
|||
$searchString = preg_replace('/[^0-9a-zA-Z]/', '', $searchString); |
|||
return '%'.implode('%', str_split( $searchString )).'%'; |
|||
} |
|||
|
|||
/** |
|||
* @param $column |
|||
* @param $rawString |
|||
* @return mixed|string |
|||
*/ |
|||
public function buildQueryString( $column, $rawString ) |
|||
{ |
|||
$searchString = $this->formatSearchString( $rawString ); |
|||
|
|||
return "IF(REPLACE($column, '\.', '') {$this->operator} '$searchString', ROUND({$this->multiplier} * (CHAR_LENGTH( '$rawString' ) / CHAR_LENGTH( REPLACE($column, ' ', '') ))), 0)"; |
|||
} |
|||
} |
@ -1,28 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches against any occurrences of a string within a string and is case-insensitive. |
|||
* |
|||
* For example, a search for 'smi' would match; 'John Smith' or 'Smiley Face' |
|||
* |
|||
* Class InStringMatcher |
|||
* @package TomLingham\Searchy\Matchers |
|||
*/ |
|||
|
|||
class InStringMatcher extends BaseMatcher |
|||
{ |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $operator = 'LIKE'; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString( $searchString ) |
|||
{ |
|||
return "%$searchString%"; |
|||
} |
|||
} |
@ -1,25 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches strings for Acronym 'like' matches but does NOT return Studly Case Matches |
|||
* |
|||
* for example, a search for 'fb' would match; 'foo bar' or 'Fred Brown' but not 'FreeBeer'. |
|||
* |
|||
* Class AcronymMatcher |
|||
* @package TomLingham\Searchy\Matchers |
|||
*/ |
|||
|
|||
class LevenshteinMatcher extends BaseMatcher |
|||
{ |
|||
|
|||
/** |
|||
* @param $column |
|||
* @param $searchString |
|||
* @return mixed|string |
|||
*/ |
|||
public function buildQueryString( $column, $searchString ) |
|||
{ |
|||
return "levenshtein($column, '$searchString')"; |
|||
} |
|||
|
|||
} |
@ -1,28 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches Strings that begin with the search string. |
|||
* |
|||
* For example, a search for 'hel' would match; 'Hello World' or 'helping hand' |
|||
* |
|||
* Class StartOfStringMatcher |
|||
* @package TomLingham\Searchy\Matchers |
|||
*/ |
|||
|
|||
class StartOfStringMatcher extends BaseMatcher |
|||
{ |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $operator = 'LIKE'; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString( $searchString ) |
|||
{ |
|||
return "$searchString%"; |
|||
} |
|||
} |
@ -1,28 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches the start of each word against each word in a search |
|||
* |
|||
* For example, a search for 'jo ta' would match; 'John Taylor' or 'Joshua B. Takashi' |
|||
* |
|||
* Class StartOfWordsMatcher |
|||
* @package TomLingham\Searchy\Matchers |
|||
*/ |
|||
|
|||
class StartOfWordsMatcher extends BaseMatcher |
|||
{ |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $operator = 'LIKE'; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString( $searchString ) |
|||
{ |
|||
return implode('% ', explode(' ', $searchString)) . '%'; |
|||
} |
|||
} |
@ -1,34 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches Studly Case strings using the first letters of the words only |
|||
* |
|||
* For example a search for 'hp' would match; 'HtmlServiceProvider' or 'HashParser' but not 'hasProvider' |
|||
* |
|||
* Class StudlyCaseMatcher |
|||
* @package TomLingham\Searchy\Matchers |
|||
*/ |
|||
|
|||
class StudlyCaseMatcher extends BaseMatcher |
|||
{ |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $operator = 'LIKE BINARY'; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString( $searchString ) |
|||
{ |
|||
$searchString = preg_replace('/[^0-9a-zA-Z]/', '', $searchString); |
|||
return implode( '%', str_split(strtoupper( $searchString ))) . '%'; |
|||
} |
|||
|
|||
public function buildQueryString( $column, $searchString ) |
|||
{ |
|||
return "IF( CHAR_LENGTH( TRIM($column)) = CHAR_LENGTH( REPLACE( TRIM($column), ' ', '')) AND $column {$this->operator} '{$this->formatSearchString($searchString)}', {$this->multiplier}, 0)"; |
|||
} |
|||
} |
@ -1,110 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy; |
|||
|
|||
use Illuminate\Config\Repository; |
|||
use TomLingham\Searchy\SearchDrivers\FuzzySearchDriver; |
|||
|
|||
|
|||
/** |
|||
* @property mixed driverName |
|||
*/ |
|||
class SearchBuilder { |
|||
|
|||
|
|||
/** |
|||
* @var |
|||
*/ |
|||
private $table; |
|||
|
|||
/** |
|||
* @var |
|||
*/ |
|||
private $searchFields; |
|||
|
|||
/** |
|||
* @var |
|||
*/ |
|||
private $driverName; |
|||
|
|||
/** |
|||
* @var |
|||
*/ |
|||
private $config; |
|||
|
|||
|
|||
public function __construct( Repository $config ) |
|||
{ |
|||
$this->config = $config; |
|||
} |
|||
|
|||
/** |
|||
* @param $searchable |
|||
* @return $this |
|||
*/ |
|||
public function search( $searchable ) |
|||
{ |
|||
if (is_object( $searchable ) && method_exists($searchable, 'getTable')) { |
|||
$this->table = $searchable->getTable(); |
|||
} else { |
|||
$this->table = $searchable; |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @return FuzzySearchDriver |
|||
*/ |
|||
public function fields( /* $fields */ ) |
|||
{ |
|||
$this->searchFields = func_get_args(); |
|||
|
|||
return $this->makeDriver(); |
|||
} |
|||
|
|||
/** |
|||
* @param $driverName |
|||
* @return $this |
|||
*/ |
|||
public function driver( $driverName ) |
|||
{ |
|||
$this->driverName = $driverName; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @param $table |
|||
* @param $searchFields |
|||
* @return mixed |
|||
*/ |
|||
public function __call( $table, $searchFields ) |
|||
{ |
|||
return call_user_func_array([$this->search( $table ), 'fields'], $searchFields); |
|||
} |
|||
|
|||
/** |
|||
* @return mixed |
|||
*/ |
|||
private function makeDriver() |
|||
{ |
|||
$relevanceFieldName = $this->config->get('searchy.fieldName'); |
|||
|
|||
// Check if default driver is being overridden, otherwise
|
|||
// load the default
|
|||
if ( $this->driverName ){ |
|||
$driverName = $this->driverName; |
|||
} else { |
|||
$driverName = $this->config->get('searchy.default'); |
|||
} |
|||
|
|||
// Gets the details for the selected driver from the configuration file
|
|||
$driver = $this->config->get("searchy.drivers.$driverName")['class']; |
|||
|
|||
// Create a new instance of the selected drivers 'class' and pass
|
|||
// through table and fields to search
|
|||
$driverInstance = new $driver( $this->table, $this->searchFields, $relevanceFieldName ); |
|||
return $driverInstance; |
|||
|
|||
} |
|||
|
|||
} |
@ -1,147 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\SearchDrivers; |
|||
|
|||
use TomLingham\Searchy\Interfaces\SearchDriverInterface; |
|||
|
|||
|
|||
abstract class BaseSearchDriver implements SearchDriverInterface { |
|||
|
|||
protected $table; |
|||
protected $columns; |
|||
protected $searchFields; |
|||
protected $searchString; |
|||
protected $relevanceFieldName; |
|||
protected $query; |
|||
|
|||
/** |
|||
* @param null $table |
|||
* @param array $searchFields |
|||
* @param $relevanceFieldName |
|||
* @param array $columns |
|||
* @internal param $relevanceField |
|||
*/ |
|||
public function __construct( $table = null, $searchFields = [], $relevanceFieldName, $columns = ['*'] ) |
|||
{ |
|||
$this->searchFields = $searchFields; |
|||
$this->table = $table; |
|||
$this->columns = $columns; |
|||
$this->relevanceFieldName = $relevanceFieldName; |
|||
} |
|||
|
|||
/** |
|||
* Specify which columns to return |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function select() |
|||
{ |
|||
$this->columns = func_get_args(); |
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Specify the string that is is being searched for |
|||
* |
|||
* @param $searchString |
|||
* @return \Illuminate\Database\Query\Builder|mixed|static |
|||
*/ |
|||
public function query( $searchString ) |
|||
{ |
|||
$this->searchString = trim(\DB::connection()->getPdo()->quote( $searchString ), "'"); |
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Get the results of the search as an Array |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function get() |
|||
{ |
|||
return $this->run()->get(); |
|||
} |
|||
|
|||
/** |
|||
* Returns an instance of the Laravel Fluent Database Query Object with the search |
|||
* queries applied |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function getQuery() |
|||
{ |
|||
return $this->run(); |
|||
} |
|||
|
|||
/** |
|||
* Runs the 'having' method directly on the Laravel Fluent Database Query Object |
|||
* and returns the instance of the object |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public function having() |
|||
{ |
|||
return call_user_func_array([$this->run(), 'having'], func_get_args()); |
|||
} |
|||
|
|||
/** |
|||
* @return $this |
|||
*/ |
|||
protected function run() |
|||
{ |
|||
$this->query = \DB::table( $this->table ) |
|||
->select( $this->columns ) |
|||
->addSelect( $this->buildSelectQuery( $this->searchFields ) ) |
|||
->orderBy( $this->relevanceFieldName, 'desc' ) |
|||
->having( $this->relevanceFieldName, '>', 0 ); |
|||
|
|||
return $this->query; |
|||
} |
|||
|
|||
/** |
|||
* @param array $searchFields |
|||
* @return array|\Illuminate\Database\Query\Expression |
|||
*/ |
|||
protected function buildSelectQuery( array $searchFields ) |
|||
{ |
|||
$query = []; |
|||
|
|||
foreach ($searchFields as $searchField) { |
|||
if (strpos($searchField, '::')){ |
|||
$concatString = str_replace('::', ", ' ', ", $searchField); |
|||
$query[] = $this->buildSelectCriteria( "CONCAT({$concatString})"); |
|||
} else { |
|||
$query[] = $this->buildSelectCriteria( $searchField ); |
|||
} |
|||
} |
|||
|
|||
return \DB::raw(implode(' + ', $query) . ' AS ' . $this->relevanceFieldName); |
|||
} |
|||
|
|||
/** |
|||
* @param null $searchField |
|||
* @return string |
|||
*/ |
|||
protected function buildSelectCriteria( $searchField = null ) |
|||
{ |
|||
$criteria = []; |
|||
|
|||
foreach( $this->matchers as $matcher => $multiplier){ |
|||
$criteria[] = $this->makeMatcher( $searchField, $matcher, $multiplier ); |
|||
} |
|||
|
|||
return implode(' + ', $criteria); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* @param $searchField |
|||
* @param $matcherClass |
|||
* @param $multiplier |
|||
* @return mixed |
|||
*/ |
|||
protected function makeMatcher( $searchField, $matcherClass, $multiplier ) |
|||
{ |
|||
$matcher = new $matcherClass( $multiplier ); |
|||
|
|||
return $matcher->buildQueryString( $searchField, $this->searchString ); |
|||
} |
|||
} |
@ -1,19 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\SearchDrivers; |
|||
|
|||
class FuzzySearchDriver extends BaseSearchDriver { |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $matchers = [ |
|||
'TomLingham\Searchy\Matchers\ExactMatcher' => 100, |
|||
'TomLingham\Searchy\Matchers\StartOfStringMatcher' => 50, |
|||
'TomLingham\Searchy\Matchers\AcronymMatcher' => 42, |
|||
'TomLingham\Searchy\Matchers\ConsecutiveCharactersMatcher' => 40, |
|||
'TomLingham\Searchy\Matchers\StartOfWordsMatcher' => 35, |
|||
'TomLingham\Searchy\Matchers\StudlyCaseMatcher' => 32, |
|||
'TomLingham\Searchy\Matchers\InStringMatcher' => 30, |
|||
'TomLingham\Searchy\Matchers\TimesInStringMatcher' => 8, |
|||
]; |
|||
|
|||
} |
@ -1,12 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\SearchDrivers; |
|||
|
|||
class LevenshteinSearchDriver extends BaseSearchDriver { |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $matchers = [ |
|||
'TomLingham\Searchy\Matchers\LevenshteinMatcher' => 100 |
|||
]; |
|||
|
|||
} |
@ -1,14 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy\SearchDrivers; |
|||
|
|||
class SimpleSearchDriver extends BaseSearchDriver { |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $matchers = [ |
|||
'TomLingham\Searchy\Matchers\ExactMatcher' => 100, |
|||
'TomLingham\Searchy\Matchers\StartOfStringMatcher' => 50, |
|||
'TomLingham\Searchy\Matchers\InStringMatcher' => 30, |
|||
]; |
|||
|
|||
} |
@ -1,52 +0,0 @@ |
|||
<?php namespace TomLingham\Searchy; |
|||
|
|||
use Illuminate\Config\Repository; |
|||
use Illuminate\Support\ServiceProvider; |
|||
|
|||
class SearchyServiceProvider extends ServiceProvider { |
|||
|
|||
/** |
|||
* Indicates if loading of the provider is deferred. |
|||
* |
|||
* @var bool |
|||
*/ |
|||
protected $defer = false; |
|||
|
|||
/** |
|||
* Register the service provider. |
|||
* |
|||
* @return void |
|||
*/ |
|||
public function register() |
|||
{ |
|||
$this->app->bindShared('searchy', function( $app ) |
|||
{ |
|||
return new SearchBuilder( $app['config'] ); |
|||
}); |
|||
|
|||
$this->mergeConfigFrom( |
|||
__DIR__ . '/../../config/config.php', 'searchy' |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* |
|||
*/ |
|||
public function boot() |
|||
{ |
|||
$this->publishes([ |
|||
__DIR__.'/../../config/config.php' => config_path('searchy.php'), |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Get the services provided by the provider. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function provides() |
|||
{ |
|||
return array(); |
|||
} |
|||
|
|||
} |
@ -1,28 +0,0 @@ |
|||
<?php |
|||
|
|||
return [ |
|||
|
|||
'default' => 'fuzzy', |
|||
|
|||
'fieldName' => 'relevance', |
|||
|
|||
'drivers' => [ |
|||
|
|||
'fuzzy' => [ |
|||
'class' => 'TomLingham\Searchy\SearchDrivers\FuzzySearchDriver' |
|||
], |
|||
|
|||
'simple' => [ |
|||
'class' => 'TomLingham\Searchy\SearchDrivers\SimpleSearchDriver' |
|||
], |
|||
|
|||
'levenshtein' => [ |
|||
'class' => 'TomLingham\Searchy\SearchDrivers\LevenshteinSearchDriver' |
|||
], |
|||
|
|||
|
|||
], |
|||
|
|||
|
|||
|
|||
]; |
Write
Preview
Loading…
Cancel
Save
Reference in new issue