Tom Lingham
11 years ago
20 changed files with 683 additions and 1 deletions
-
24src/TomLingham/Searchy/Facades/Searchy.php
-
14src/TomLingham/Searchy/Interfaces/MatcherInterface.php
-
13src/TomLingham/Searchy/Interfaces/SearchDriverInterface.php
-
32src/TomLingham/Searchy/Matchers/AcronymMatcher.php
-
33src/TomLingham/Searchy/Matchers/BaseMatcher.php
-
46src/TomLingham/Searchy/Matchers/ConsecutiveCharactersMatcher.php
-
24src/TomLingham/Searchy/Matchers/ExactMatcher.php
-
32src/TomLingham/Searchy/Matchers/InStringMatcher.php
-
33src/TomLingham/Searchy/Matchers/LevenshteinMatcher.php
-
32src/TomLingham/Searchy/Matchers/StartOfStringMatcher.php
-
32src/TomLingham/Searchy/Matchers/StartOfWordsMatcher.php
-
39src/TomLingham/Searchy/Matchers/StudlyCaseMatcher.php
-
34src/TomLingham/Searchy/Matchers/TimesInStringMatcher.php
-
80src/TomLingham/Searchy/SearchBuilder.php
-
110src/TomLingham/Searchy/SearchDrivers/BaseSearchDriver.php
-
19src/TomLingham/Searchy/SearchDrivers/FuzzySearchDriver.php
-
36src/TomLingham/Searchy/SearchDrivers/LevenshteinSearchDriver.php
-
14src/TomLingham/Searchy/SearchDrivers/SimpleSearchDriver.php
-
5src/TomLingham/Searchy/SearchyServiceProvider.php
-
32src/config/config.php
@ -0,0 +1,24 @@ |
|||
<?php namespace TomLingham\Searchy\Facades; |
|||
|
|||
use TomLingham\Searchy\SearchyServiceProvider; |
|||
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() |
|||
{ |
|||
if (!static::$app) { |
|||
static::$app = SearchyServiceProvider::make(); |
|||
} |
|||
|
|||
return 'searchy'; |
|||
} |
|||
} |
@ -0,0 +1,14 @@ |
|||
<?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,13 @@ |
|||
<?php namespace TomLingham\Searchy\Interfaces; |
|||
|
|||
interface SearchDriverInterface |
|||
{ |
|||
/** |
|||
* Execute the query on the Driver |
|||
* |
|||
* @param $searchString |
|||
* @return mixed |
|||
*/ |
|||
public function query( $searchString ); |
|||
|
|||
} |
@ -0,0 +1,32 @@ |
|||
<?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'; |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
protected $multiplier = 42; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* @return mixed|string |
|||
*/ |
|||
public function formatSearchString( $searchString ) { |
|||
|
|||
return implode( '% ', str_split(strtoupper( $searchString ))) . '%'; |
|||
} |
|||
} |
@ -0,0 +1,33 @@ |
|||
<?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,46 @@ |
|||
<?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'; |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
protected $multiplier = 40; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString( $searchString ) { |
|||
return '%'.implode('%', str_split( $searchString )).'%'; |
|||
} |
|||
|
|||
/** |
|||
* @param $column |
|||
* @param $rawString |
|||
* @return mixed|string |
|||
*/ |
|||
public function buildQueryString( $column, $rawString ){ |
|||
|
|||
$searchString = $this->formatSearchString( $rawString ); |
|||
|
|||
$query = "IF($column {$this->operator} '$searchString', ROUND({$this->multiplier} * (CHAR_LENGTH( '$rawString' ) / CHAR_LENGTH( REPLACE($column, ' ', '') ))), 0)"; |
|||
|
|||
return $query; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,24 @@ |
|||
<?php namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches an exact string and applies a high multiplier to bring any exact matches to the top |
|||
* When sanitize is on, if the expression strips some of the characters from the search query |
|||
* then this may not be able to match against a string despite entering in an exact match. |
|||
* |
|||
* Class ExactMatcher |
|||
* @package TomLingham\Searchy\Matchers |
|||
*/ |
|||
|
|||
class ExactMatcher extends BaseMatcher |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $operator = '='; |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
protected $multiplier = 100; |
|||
|
|||
} |
@ -0,0 +1,32 @@ |
|||
<?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'; |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
protected $multiplier = 30; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString( $searchString ){ |
|||
return "%$searchString%"; |
|||
} |
|||
} |
@ -0,0 +1,33 @@ |
|||
<?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 |
|||
{ |
|||
|
|||
private $sensitivity; |
|||
|
|||
public function setSensitivity( $sensitivity ) |
|||
{ |
|||
$this->sensitivity = $sensitivity; |
|||
} |
|||
|
|||
/** |
|||
* @param $column |
|||
* @param $searchString |
|||
* @return mixed|string |
|||
*/ |
|||
public function buildQueryString( $column, $searchString ){ |
|||
|
|||
return "levenshtein($column, '$searchString', {$this->sensitivity})"; |
|||
|
|||
} |
|||
|
|||
} |
@ -0,0 +1,32 @@ |
|||
<?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'; |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
protected $multiplier = 50; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString( $searchString ) { |
|||
return "$searchString%"; |
|||
} |
|||
} |
@ -0,0 +1,32 @@ |
|||
<?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'; |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
protected $multiplier = 35; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString( $searchString ) { |
|||
return implode('% ', explode(' ', $searchString)) . '%'; |
|||
} |
|||
} |
@ -0,0 +1,39 @@ |
|||
<?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'; |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
protected $multiplier = 32; |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* @return string |
|||
*/ |
|||
public function formatSearchString( $searchString ) { |
|||
|
|||
return implode( '%', str_split(strtoupper( $searchString ))) . '%'; |
|||
} |
|||
|
|||
public function buildQueryString( $column, $searchString ){ |
|||
|
|||
$query = "IF( CHAR_LENGTH( TRIM($column)) = CHAR_LENGTH( REPLACE( TRIM($column), ' ', '')) AND $column {$this->operator} '{$this->formatSearchString($searchString)}', {$this->multiplier}, 0)"; |
|||
|
|||
return $query; |
|||
} |
|||
} |
@ -0,0 +1,34 @@ |
|||
<?php namespace TomLingham\Searchy\Matchers; |
|||
|
|||
/** |
|||
* Matches a string based on how many times the search string appears inside the string |
|||
* it then applies the multiplier for each occurrence. |
|||
* |
|||
* For example, a search for 'tha' would match; 'I hope that that cat has caught that mouse' (3 x multiplier) or 'Thanks, it was great!' (1 x multiplier) |
|||
* |
|||
* Class TimesInStringMatcher |
|||
* @package TomLingham\Searchy\Matchers |
|||
*/ |
|||
|
|||
class TimesInStringMatcher extends BaseMatcher |
|||
{ |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
protected $multiplier = 8; |
|||
|
|||
/** |
|||
* @param $column |
|||
* @param $searchString |
|||
* @return mixed|string |
|||
*/ |
|||
public function buildQueryString( $column, $searchString ){ |
|||
|
|||
$query = "{$this->multiplier} * ROUND ((
|
|||
CHAR_LENGTH($column) - CHAR_LENGTH( REPLACE ( LOWER($column), lower('$searchString'), '')) |
|||
) / LENGTH('$searchString'))";
|
|||
|
|||
return $query; |
|||
} |
|||
} |
@ -0,0 +1,80 @@ |
|||
<?php namespace TomLingham\Searchy; |
|||
|
|||
|
|||
use Illuminate\Support\Facades\Config; |
|||
use TomLingham\Searchy\SearchDrivers\FuzzySearchDriver; |
|||
|
|||
/** |
|||
* @property mixed driverName |
|||
*/ |
|||
class SearchBuilder { |
|||
|
|||
|
|||
private $table; |
|||
private $searchFields; |
|||
private $driverName; |
|||
|
|||
/** |
|||
* @param $table |
|||
* @return $this |
|||
*/ |
|||
public function search( $table ) |
|||
{ |
|||
$this->table = $table; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @return FuzzySearchDriver |
|||
*/ |
|||
public function fields( /* $fields */ ) |
|||
{ |
|||
|
|||
$searchFields = func_get_args(); |
|||
|
|||
$this->searchFields = $searchFields; |
|||
|
|||
return $this->makeDriver(); |
|||
|
|||
} |
|||
|
|||
/** |
|||
* @param $driverName |
|||
* @return $this |
|||
*/ |
|||
public function driver( $driverName ) |
|||
{ |
|||
$this->driverName = $driverName; |
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @param $table |
|||
* @param $fields |
|||
* @return mixed |
|||
*/ |
|||
public function __call( $table, $fields ) |
|||
{ |
|||
|
|||
return call_user_func_array([$this->search( $table ), 'fields'], $fields); |
|||
|
|||
} |
|||
|
|||
/** |
|||
* @return mixed |
|||
*/ |
|||
private function makeDriver() |
|||
{ |
|||
if (! $this->driverName){ |
|||
$driverName = \Config::get('searchy::default'); |
|||
} else { |
|||
$driverName = $this->driverName; |
|||
} |
|||
$driverMap = \Config::get("searchy::drivers.$driverName"); |
|||
|
|||
return new $driverMap['class']( $this->table, $this->searchFields ); |
|||
|
|||
} |
|||
|
|||
} |
@ -0,0 +1,110 @@ |
|||
<?php namespace TomLingham\Searchy\SearchDrivers; |
|||
|
|||
use TomLingham\Searchy\Interfaces\SearchDriverInterface; |
|||
|
|||
/** |
|||
* @property mixed methods |
|||
* @property mixed matchers |
|||
*/ |
|||
abstract class BaseSearchDriver implements SearchDriverInterface { |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $fields; |
|||
|
|||
/** |
|||
* @var |
|||
*/ |
|||
protected $searchString; |
|||
|
|||
/** |
|||
* @var null |
|||
*/ |
|||
protected $table; |
|||
|
|||
/** |
|||
* @param null $table |
|||
* @param array $fields |
|||
*/ |
|||
public function __construct( $table = null, $fields = [] ){ |
|||
$this->fields = $fields; |
|||
$this->table = $table; |
|||
} |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* @return \Illuminate\Database\Query\Builder|mixed|static |
|||
* @throws \Whoops\Example\Exception |
|||
*/ |
|||
public function query( $searchString ){ |
|||
|
|||
if(\Config::get('searchy::sanitize')) |
|||
$this->searchString = $this->sanitize($searchString); |
|||
|
|||
$results = \DB::table($this->table) |
|||
->select( \DB::raw('*') ) |
|||
->addSelect($this->buildSelectQuery( $this->fields )) |
|||
->orderBy(\Config::get('searchy::fieldName'), 'desc') |
|||
->having(\Config::get('searchy::fieldName'),'>', 0); |
|||
|
|||
dd($results->toSql()); |
|||
|
|||
return $results; |
|||
} |
|||
|
|||
/** |
|||
* @param array $fields |
|||
* @return array|\Illuminate\Database\Query\Expression |
|||
*/ |
|||
protected function buildSelectQuery( array $fields ){ |
|||
$query = []; |
|||
|
|||
foreach ($fields as $field) { |
|||
$query[] = $this->buildSelectCriteria( $field ); |
|||
} |
|||
|
|||
$query = \DB::raw(implode(' + ', $query) . ' AS ' . \Config::get('searchy::fieldName')); |
|||
|
|||
return $query; |
|||
} |
|||
|
|||
/** |
|||
* @param null $field |
|||
* @return string |
|||
*/ |
|||
protected function buildSelectCriteria( $field = null ) { |
|||
$criteria = []; |
|||
|
|||
foreach( $this->matchers as $matcher => $multiplier){ |
|||
$criteria[] = $this->makeMatcher( $field, $matcher, $multiplier ); |
|||
} |
|||
|
|||
return implode(' + ', $criteria); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* @param $field |
|||
* @param $matcherClass |
|||
* @param $multiplier |
|||
* @return mixed |
|||
*/ |
|||
protected function makeMatcher( $field, $matcherClass, $multiplier ) |
|||
{ |
|||
|
|||
$matcher = new $matcherClass( $multiplier ); |
|||
|
|||
return $matcher->buildQueryString( $field, $this->searchString ); |
|||
|
|||
} |
|||
|
|||
/** |
|||
* @param $searchString |
|||
* @return mixed |
|||
*/ |
|||
private function sanitize( $searchString ) { |
|||
return preg_replace(\Config::get('searchy::sanitizeRegEx'), '', $searchString); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,19 @@ |
|||
<?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,36 @@ |
|||
<?php namespace TomLingham\Searchy\SearchDrivers; |
|||
|
|||
class LevenshteinSearchDriver extends BaseSearchDriver { |
|||
|
|||
|
|||
private $sensitivity = 10; |
|||
|
|||
protected $matchers = [ |
|||
'TomLingham\Searchy\Matchers\LevenshteinMatcher' => 100 |
|||
]; |
|||
|
|||
|
|||
public function setSensitivity( $sensitivity ){ |
|||
|
|||
$this->sensitivity = $sensitivity; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* @param $column |
|||
* @param $matcherClass |
|||
* @param $multiplier |
|||
* @return mixed |
|||
*/ |
|||
protected function makeMatcher( $column, $matcherClass, $multiplier ) |
|||
{ |
|||
$matcher = new $matcherClass( $multiplier ); |
|||
$matcher->setSensitivity( $this->sensitivity ); |
|||
|
|||
return $matcher->buildQueryString( $column, $this->searchString ); |
|||
|
|||
} |
|||
|
|||
} |
@ -0,0 +1,14 @@ |
|||
<?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,32 @@ |
|||
<?php |
|||
|
|||
return [ |
|||
|
|||
'default' => 'fuzzy', |
|||
|
|||
'sanitize' => true, |
|||
|
|||
'sanitizeRegEx' => '/[%]+/i', |
|||
|
|||
'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