Browse Source

Laravel 5 awesomeness

master
Tom Lingham 10 years ago
parent
commit
1e346baf59
  1. 53
      README.md
  2. 11
      src/TomLingham/Searchy/Interfaces/SearchDriverInterface.php
  3. 1
      src/TomLingham/Searchy/Matchers/AcronymMatcher.php
  4. 1
      src/TomLingham/Searchy/Matchers/ConsecutiveCharactersMatcher.php
  5. 2
      src/TomLingham/Searchy/Matchers/StudlyCaseMatcher.php
  6. 29
      src/TomLingham/Searchy/SearchBuilder.php
  7. 115
      src/TomLingham/Searchy/SearchDrivers/BaseSearchDriver.php
  8. 4
      src/TomLingham/Searchy/SearchyServiceProvider.php
  9. 4
      src/config/config.php

53
README.md

@ -2,29 +2,29 @@ Laravel Searchy 2
======================================== ========================================
### Database Searching Made Easy ### Database Searching Made Easy
Searchy is an easy-to-use Laravel Optimized package that makes running user driven searches on data in your models simple and effective.
Searchy is an; easy-to-use, light-weight, MySQL only, Laravel package that makes running user driven searches on data in your models simple and effective.
It uses pseudo fuzzy searching and other weighted mechanics depending on the search driver that you have enabled. It uses pseudo fuzzy searching and other weighted mechanics depending on the search driver that you have enabled.
It requires no other software installed on your server (so can be a little slower than dedicated search programs) but can be set up and ready to go in minutes. It requires no other software installed on your server (so can be a little slower than dedicated search programs) but can be set up and ready to go in minutes.
Installation Installation
---------------------------------------- ----------------------------------------
Add `"tom-lingham/searchy" : "2.0~"` to your composer.json file under `require`:
Add `"tom-lingham/searchy" : "2.0"` to your composer.json file under `require`:
``` ```
"require": { "require": {
"laravel/framework": "5.*", "laravel/framework": "5.*",
"tom-lingham/searchy" : "dev-master"
"tom-lingham/searchy" : "2.0"
} }
``` ```
Run `composer update` in your terminal to pull down the package into your vendors folder. Run `composer update` in your terminal to pull down the package into your vendors folder.
Add the service provider to the `providers` array in Laravel's app/config/app.php file: Add the service provider to the `providers` array in Laravel's app/config/app.php file:
```php ```php
'TomLingham\Searchy\SearchyServiceProvider'
TomLingham\Searchy\SearchyServiceProvider::class
``` ```
Add the Alias to the `aliases` array in Laravel's app/config/app.php file if you want to have quick access to it in your application: Add the Alias to the `aliases` array in Laravel's app/config/app.php file if you want to have quick access to it in your application:
```php ```php
'Searchy' => 'TomLingham\Searchy\Facades\Searchy'
'Searchy' => TomLingham\Searchy\Facades\Searchy::class
``` ```
@ -34,19 +34,21 @@ To use Searchy, you can take advantage of magic methods.
If you are searching the name and email column/field of users in a `users` table you would, for example run: If you are searching the name and email column/field of users in a `users` table you would, for example run:
```php ```php
$users = Searchy::users('name', 'email')->query('John Smith');
$users = Searchy::users('name', 'email')->query('John Smith')->get();
``` ```
you can also write this as: you can also write this as:
```php ```php
$users = Searchy::search('users')->fields('name', 'email')->query('John Smith');
$users = Searchy::search('users')->fields('name', 'email')->query('John Smith')->get();
``` ```
In this case, pass the columns you want to search through to the `fields()` method. In this case, pass the columns you want to search through to the `fields()` method.
These examples both return a Laravel DB Query Builder Object, so you will need to chain `get()` to actually return the results:
These examples both return an array of Objects containing your search results. You can use `getQuery()` instead of
`get()` to return an instance of the Database Query Object in case you want to do further manipulation to the results:
```php ```php
$users = Searchy::search('users')->fields('name', 'email')->query('John Smith')->get();
$users = Searchy::search('users')->fields('name', 'email')->query('John Smith')
->getQuery()->having('relevance', '>', 20)->get();
``` ```
#### Searching multiple Columns #### Searching multiple Columns
@ -54,14 +56,24 @@ You can also add multiple arguments to the list of fields/columns to search by.
For example, if you want to search the name, email address and username of a user, you might run: For example, if you want to search the name, email address and username of a user, you might run:
```php ```php
$users = Searchy::users('name', 'email', 'username')->query('John Smith');
$users = Searchy::users('name', 'email', 'username')->query('John Smith')->get();
``` ```
#### Searching Joined/Concatenated Columns #### Searching Joined/Concatenated Columns
Sometimes you may want to leverage searches on concatenated column. For example, on a `first_name` and `last_name` field but you only want to run the one query. To do this can separate columns with a double colon: Sometimes you may want to leverage searches on concatenated column. For example, on a `first_name` and `last_name` field but you only want to run the one query. To do this can separate columns with a double colon:
```php ```php
$users = Searchy::users('first_name::last_name')->query('John Smith');
$users = Searchy::users('first_name::last_name')->query('John Smith')->get();
```
#### Return only specific columns
You can specify which columns to return in your search:
```php
$users = Searchy::users('first_name::last_name')->query('John Smith')->select('first_name')->get();
// Or you can swap those around...
$users = Searchy::users('first_name::last_name')->select('first_name')->query('John Smith')->get();
``` ```
This will, however, also return the `relevance` aliased column regardless of what is entered here.
Configuration Configuration
---------------------------------------- ----------------------------------------
@ -72,7 +84,7 @@ You can set the default driver to use for searches in the configuration file. Yo
You can also override these methods using the following syntax when running a search: You can also override these methods using the following syntax when running a search:
```php ```php
Searchy::driver('fuzzy')->users('name')->query('Bat Man')->get();
Searchy::driver('fuzzy')->users('name')->query('Batman')->get();
``` ```
@ -88,13 +100,11 @@ Currently there are only three drivers: Simple, Fuzzy and Levenshtein (Experimen
The Simple search driver only uses 3 matchers each with the relevant multipliers that best suited my testing environments. The Simple search driver only uses 3 matchers each with the relevant multipliers that best suited my testing environments.
```php ```php
protected $matchers = [ protected $matchers = [
'TomLingham\Searchy\Matchers\ExactMatcher' => 100, 'TomLingham\Searchy\Matchers\ExactMatcher' => 100,
'TomLingham\Searchy\Matchers\StartOfStringMatcher' => 50, 'TomLingham\Searchy\Matchers\StartOfStringMatcher' => 50,
'TomLingham\Searchy\Matchers\InStringMatcher' => 30, 'TomLingham\Searchy\Matchers\InStringMatcher' => 30,
]; ];
``` ```
@ -102,7 +112,6 @@ protected $matchers = [
The Fuzzy Search Driver is simply another group of matchers setup as follows. The multipliers are what I have used, but feel free to change these or roll your own driver with the same matchers and change the multipliers to suit. The Fuzzy Search Driver is simply another group of matchers setup as follows. The multipliers are what I have used, but feel free to change these or roll your own driver with the same matchers and change the multipliers to suit.
```php ```php
protected $matchers = [ protected $matchers = [
'TomLingham\Searchy\Matchers\ExactMatcher' => 100, 'TomLingham\Searchy\Matchers\ExactMatcher' => 100,
'TomLingham\Searchy\Matchers\StartOfStringMatcher' => 50, 'TomLingham\Searchy\Matchers\StartOfStringMatcher' => 50,
@ -113,18 +122,15 @@ protected $matchers = [
'TomLingham\Searchy\Matchers\InStringMatcher' => 30, 'TomLingham\Searchy\Matchers\InStringMatcher' => 30,
'TomLingham\Searchy\Matchers\TimesInStringMatcher' => 8, 'TomLingham\Searchy\Matchers\TimesInStringMatcher' => 8,
]; ];
``` ```
#### Levenshtein Search Driver (Experimental) #### Levenshtein Search Driver (Experimental)
The Levenshtein Search Driver uses the Levenshetein Distance to calculate the 'distance' between strings. It requires that you have a stored procedure in MySQL similar to the following `levenshtein( string1, string2 )`. There is an SQL file with a suitable function in the `res` folder - feel free to use this one. The Levenshtein Search Driver uses the Levenshetein Distance to calculate the 'distance' between strings. It requires that you have a stored procedure in MySQL similar to the following `levenshtein( string1, string2 )`. There is an SQL file with a suitable function in the `res` folder - feel free to use this one.
```php ```php
protected $matchers = [ protected $matchers = [
'TomLingham\Searchy\Matchers\LevenshteinMatcher' => 100 'TomLingham\Searchy\Matchers\LevenshteinMatcher' => 100
]; ];
``` ```
Matchers Matchers
@ -132,7 +138,6 @@ Matchers
#### ExactMatcher #### ExactMatcher
Matches an exact string and applies a high multiplier to bring any exact matches to the top. 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.
#### StartOfStringMatcher #### StartOfStringMatcher
@ -187,13 +192,3 @@ Contributing & Reporting Bugs
---------------------------------------- ----------------------------------------
If you would like to improve on the code that is here, feel free to submit a pull request. If you would like to improve on the code that is here, feel free to submit a pull request.
If you find any bugs, submit them here and I will respond as soon as possible. Please make sure to include as much information as possible. If you find any bugs, submit them here and I will respond as soon as possible. Please make sure to include as much information as possible.
Road Map
----------------------------------------
To the future! The intention is to (eventually):
1. Remove Searchy's dependancy on Laravel
2. Include more drivers for more advanced searching (Including file system searching, indexing and more)
3. Implement an AJAX friendly interface for searching models and implementing auto-suggestion features on the front end
4. Speed up search performance and improve result relevance

11
src/TomLingham/Searchy/Interfaces/SearchDriverInterface.php

@ -2,12 +2,11 @@
interface SearchDriverInterface interface SearchDriverInterface
{ {
/**
* Execute the query on the Driver
*
* @param $searchString
* @return mixed
*/
public function query( $searchString ); public function query( $searchString );
public function select( /* $columns */ );
public function get();
} }

1
src/TomLingham/Searchy/Matchers/AcronymMatcher.php

@ -22,6 +22,7 @@ class AcronymMatcher extends BaseMatcher
*/ */
public function formatSearchString( $searchString ) public function formatSearchString( $searchString )
{ {
$searchString = preg_replace('/[^0-9a-zA-Z]/', '', $searchString);
return implode( '% ', str_split(strtoupper( $searchString ))) . '%'; return implode( '% ', str_split(strtoupper( $searchString ))) . '%';
} }
} }

1
src/TomLingham/Searchy/Matchers/ConsecutiveCharactersMatcher.php

@ -22,6 +22,7 @@ class ConsecutiveCharactersMatcher extends BaseMatcher
*/ */
public function formatSearchString( $searchString ) public function formatSearchString( $searchString )
{ {
$searchString = preg_replace('/[^0-9a-zA-Z]/', '', $searchString);
return '%'.implode('%', str_split( $searchString )).'%'; return '%'.implode('%', str_split( $searchString )).'%';
} }

2
src/TomLingham/Searchy/Matchers/StudlyCaseMatcher.php

@ -23,7 +23,7 @@ class StudlyCaseMatcher extends BaseMatcher
*/ */
public function formatSearchString( $searchString ) public function formatSearchString( $searchString )
{ {
$searchString = preg_replace('/[^0-9a-zA-Z]/', '', $searchString);
return implode( '%', str_split(strtoupper( $searchString ))) . '%'; return implode( '%', str_split(strtoupper( $searchString ))) . '%';
} }

29
src/TomLingham/Searchy/SearchBuilder.php

@ -25,21 +25,28 @@ class SearchBuilder {
*/ */
private $driverName; private $driverName;
/**
* @var
*/
private $config; private $config;
public function __construct( Repository $config ) public function __construct( Repository $config )
{ {
$this->config = $config; $this->config = $config;
} }
/** /**
* @param $table
* @param $searchable
* @return $this * @return $this
*/ */
public function search( $table )
public function search( $searchable )
{ {
$this->table = $table;
if (is_object( $searchable ) && method_exists($searchable, 'getTable')) {
$this->table = $searchable->getTable();
} else {
$this->table = $searchable;
}
return $this; return $this;
} }
@ -49,13 +56,9 @@ class SearchBuilder {
*/ */
public function fields( /* $fields */ ) public function fields( /* $fields */ )
{ {
$searchFields = func_get_args();
$this->searchFields = $searchFields;
$this->searchFields = func_get_args();
return $this->makeDriver(); return $this->makeDriver();
} }
/** /**
@ -76,9 +79,7 @@ class SearchBuilder {
*/ */
public function __call( $table, $searchFields ) public function __call( $table, $searchFields )
{ {
return call_user_func_array([$this->search( $table ), 'fields'], $searchFields); return call_user_func_array([$this->search( $table ), 'fields'], $searchFields);
} }
/** /**
@ -86,6 +87,7 @@ class SearchBuilder {
*/ */
private function makeDriver() private function makeDriver()
{ {
$relevanceFieldName = $this->config->get('searchy.fieldName');
// Check if default driver is being overridden, otherwise // Check if default driver is being overridden, otherwise
// load the default // load the default
@ -96,11 +98,12 @@ class SearchBuilder {
} }
// Gets the details for the selected driver from the configuration file // Gets the details for the selected driver from the configuration file
$driverMap = $this->config->get("searchy.drivers.$driverName");
$driver = $this->config->get("searchy.drivers.$driverName")['class'];
// Create a new instance of the selected drivers 'class' and pass // Create a new instance of the selected drivers 'class' and pass
// through table and fields to search // through table and fields to search
return new $driverMap['class']( $this->table, $this->searchFields );
$driverInstance = new $driver( $this->table, $this->searchFields, $relevanceFieldName );
return $driverInstance;
} }

115
src/TomLingham/Searchy/SearchDrivers/BaseSearchDriver.php

@ -2,60 +2,98 @@
use TomLingham\Searchy\Interfaces\SearchDriverInterface; use TomLingham\Searchy\Interfaces\SearchDriverInterface;
/**
* @property mixed methods
* @property mixed matchers
*/
abstract class BaseSearchDriver implements SearchDriverInterface {
/**
* @var array
*/
protected $searchFields;
/**
* @var
*/
protected $searchString;
abstract class BaseSearchDriver implements SearchDriverInterface {
/**
* @var null
*/
protected $table; protected $table;
protected $columns; protected $columns;
protected $searchFields;
protected $searchString;
protected $relevanceFieldName;
protected $query;
/** /**
* @param null $table * @param null $table
* @param array $searchFields * @param array $searchFields
* @param $relevanceFieldName
* @param array $columns * @param array $columns
* @internal param $relevanceField
*/ */
public function __construct( $table = null, $searchFields = [], $columns = ['*'] )
public function __construct( $table = null, $searchFields = [], $relevanceFieldName, $columns = ['*'] )
{ {
$this->searchFields = $searchFields; $this->searchFields = $searchFields;
$this->table = $table; $this->table = $table;
$this->columns = $columns; $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 * @param $searchString
* @return \Illuminate\Database\Query\Builder|mixed|static * @return \Illuminate\Database\Query\Builder|mixed|static
*/ */
public function query( $searchString ) 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();
}
if(\Config::get('searchy.sanitize'))
$this->searchString = $this->sanitize($searchString);
/**
* Returns an instance of the Laravel Fluent Database Query Object with the search
* queries applied
*
* @return array
*/
public function getQuery()
{
return $this->run();
}
$results = \DB::table($this->table)
->select( implode($this->columns, ', ') )
->addSelect($this->buildSelectQuery( $this->searchFields ))
->orderBy(\Config::get('searchy.fieldName'), 'desc')
->having(\Config::get('searchy.fieldName'),'>', 0);
/**
* 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());
}
die($results->toSQL());
/**
* @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 $results;
return $this->query;
} }
/** /**
@ -64,20 +102,18 @@ abstract class BaseSearchDriver implements SearchDriverInterface {
*/ */
protected function buildSelectQuery( array $searchFields ) protected function buildSelectQuery( array $searchFields )
{ {
$query = []; $query = [];
foreach ($searchFields as $searchField) { foreach ($searchFields as $searchField) {
if (strpos($searchField, '::')){ if (strpos($searchField, '::')){
$concatString = explode('::', $searchField);
$query[] = $this->buildSelectCriteria( "CONCAT({$concatString[0]}, ' ', {$concatString[1]})");
$concatString = str_replace('::', ", ' ', ", $searchField);
$query[] = $this->buildSelectCriteria( "CONCAT({$concatString})");
} else { } else {
$query[] = $this->buildSelectCriteria( $searchField ); $query[] = $this->buildSelectCriteria( $searchField );
} }
} }
return \DB::raw(implode(' + ', $query) . ' AS ' . \Config::get('searchy.fieldName'));
return \DB::raw(implode(' + ', $query) . ' AS ' . $this->relevanceFieldName);
} }
/** /**
@ -86,7 +122,6 @@ abstract class BaseSearchDriver implements SearchDriverInterface {
*/ */
protected function buildSelectCriteria( $searchField = null ) protected function buildSelectCriteria( $searchField = null )
{ {
$criteria = []; $criteria = [];
foreach( $this->matchers as $matcher => $multiplier){ foreach( $this->matchers as $matcher => $multiplier){
@ -105,20 +140,8 @@ abstract class BaseSearchDriver implements SearchDriverInterface {
*/ */
protected function makeMatcher( $searchField, $matcherClass, $multiplier ) protected function makeMatcher( $searchField, $matcherClass, $multiplier )
{ {
$matcher = new $matcherClass( $multiplier ); $matcher = new $matcherClass( $multiplier );
return $matcher->buildQueryString( $searchField, $this->searchString ); return $matcher->buildQueryString( $searchField, $this->searchString );
}
/**
* @param $searchString
* @return mixed
*/
private function sanitize( $searchString )
{
return preg_replace(\Config::get('searchy.sanitizeRegEx'), '', $searchString );
} }
} }

4
src/TomLingham/Searchy/SearchyServiceProvider.php

@ -23,6 +23,10 @@ class SearchyServiceProvider extends ServiceProvider {
{ {
return new SearchBuilder( $app['config'] ); return new SearchBuilder( $app['config'] );
}); });
$this->mergeConfigFrom(
__DIR__ . '/../../config/config.php', 'searchy'
);
} }
/** /**

4
src/config/config.php

@ -4,10 +4,6 @@ return [
'default' => 'fuzzy', 'default' => 'fuzzy',
'sanitize' => true,
'sanitizeRegEx' => '/[%\']+/i',
'fieldName' => 'relevance', 'fieldName' => 'relevance',
'drivers' => [ 'drivers' => [

Loading…
Cancel
Save