You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

163 lines
4.7 KiB

<?php
/**
* Created by PhpStorm.
* User: 91373
* Date: 7/8/2016
* Time: 6:27 PM
*/
namespace Glmdev\Search;
class Search {
/**
* Searches the collection of given $modelClass type for
* $string and returns the results.
*
* @param string $modelClass
* @param $string
* @return \Illuminate\Support\Collection
*/
public static function search( $modelClass, $string ){
// define the model container in the correct scope
$model = null;
// check if the provided class name is searchable
if ( new $modelClass() instanceof SearchableContract ){
// set the model
$model = new $modelClass();
}
else {
throw new \Exception('Cannot attempt to search non-searchable class.');
return;
}
// sanitize and format the query
$query = self::formatQuery( $string );
// get all the models
$models = $model->all();
// initialize working arrays
$returns = [];
$toOpt = [];
$optHits = [];
$orgHits = [];
/**
* Count the number of times each word in the search
* string is found in the $item's searchable fields
* and push those to an array.
*/
/* @var $item \Illuminate\Database\Eloquent\Model */
foreach ( $models as $item ){
$counts = [];
foreach ( $query as $word ){
$wordcount = 0;
foreach ( $model->getSearchable() as $field ){
$wordcount = $wordcount + substr_count( strtolower( $item->$field ), $word );
}
$counts[ $word ] = $wordcount;
}
array_push( $toOpt, [
'counts' => $counts,
'item' => $item
]);
};
/**
* Convert the word found count for each individual
* word to a single unified hit count, adjusting
* for common words and letters and push to an
* array.
*/
foreach ( $toOpt as $formattedItem ){
$hits = 0;
foreach ( $formattedItem['counts'] as $word => $count ){
if ( strlen( $word ) == 1 ){
$hits += ($count*0.1);
}
elseif ( strlen( $word ) == 2 ){
$hits += ($count*0.3);
}
else {
$hits += $count;
}
}
array_push( $optHits, [
'hits' => $hits,
'item' => $formattedItem['item']
]);
}
/**
* Sort the items in an array based on hit count.
* Where the key is the number of hits, push all
* items with that specific number of hits to
* the sub array.
*/
foreach ( $optHits as $hitCountItem ){
if ( isset( $orgHits[ (string) $hitCountItem['hits'] ] ) ){
array_push( $orgHits[ (string) $hitCountItem['hits'] ], $hitCountItem['item'] );
}
else {
$orgHits[ (string) $hitCountItem['hits'] ] = [ $hitCountItem['item'] ];
}
}
/**
* Sort the array by the number of hits
* from low to high. So, the items with
* least hits to highest hits, still
* grouped.
*/
ksort( $orgHits );
/**
* Push each item to a single-dimensional array
* in order of least number of hits to highest
* number of hits.
*/
foreach ( $orgHits as $hitCountGroup ){
foreach ( $hitCountGroup as $item ){
array_push( $returns, $item );
}
}
/**
* Reverse the order of the array
* so that the first item has the
* greatest number of hits, and
* the last one has the least.
*/
$returns = array_reverse( $returns );
/**
* return the array in the form of a
* Laravel model collection
*/
return collect( $returns );
}
/**
* Formats the string into an array of non-punctuated,
* non-duplicated, lowercase words.
*
* @param $string
* @return array
*/
public static function formatQuery( $string ){
$words = explode(' ', $string);
$return = [];
foreach ( $words as $word ){
$word = preg_replace('/[^[:alpha:]]/', '', $word); // removes all punctuation
$word = strtolower( $word );
if ( ! in_array( $word, $return ) ) {
array_push($return, $word);
}
}
return $return;
}
}