From 01cb22af37535d9d12d76a56413fdb645568972a Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Mon, 27 Mar 2017 18:05:34 +0100 Subject: [PATCH] Added tag searches and advanced filters to new search --- app/Repos/EntityRepo.php | 153 --------------------------------- app/Services/SearchService.php | 148 +++++++++++++++++++++++++++++-- resources/views/base.blade.php | 2 +- routes/web.php | 2 +- 4 files changed, 144 insertions(+), 161 deletions(-) diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index 8e3f68859..b1b69814d 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -64,12 +64,6 @@ class EntityRepo */ protected $searchService; - /** - * Acceptable operators to be used in a query - * @var array - */ - protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!=']; - /** * EntityRepo constructor. * @param Book $book @@ -370,56 +364,6 @@ class EntityRepo ->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get(); } - /** - * Search entities of a type via a given query. - * @param string $type - * @param string $term - * @param array $whereTerms - * @param int $count - * @param array $paginationAppends - * @return mixed - */ - public function getBySearch($type, $term, $whereTerms = [], $count = 20, $paginationAppends = []) - { - $terms = $this->prepareSearchTerms($term); - $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)->fullTextSearchQuery($terms, $whereTerms)); - $q = $this->addAdvancedSearchQueries($q, $term); - $entities = $q->paginate($count)->appends($paginationAppends); - $words = join('|', explode(' ', preg_quote(trim($term), '/'))); - - // Highlight page content - if ($type === 'page') { - //lookahead/behind assertions ensures cut between words - $s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words - - foreach ($entities as $page) { - preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER); - //delimiter between occurrences - $results = []; - foreach ($matches as $line) { - $results[] = htmlspecialchars($line[0], 0, 'UTF-8'); - } - $matchLimit = 6; - if (count($results) > $matchLimit) $results = array_slice($results, 0, $matchLimit); - $result = join('... ', $results); - - //highlight - $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result); - if (strlen($result) < 5) $result = $page->getExcerpt(80); - - $page->searchSnippet = $result; - } - return $entities; - } - - // Highlight chapter/book content - foreach ($entities as $entity) { - //highlight - $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $entity->getExcerpt(100)); - $entity->searchSnippet = $result; - } - return $entities; - } /** * Get the next sequential priority for a new child element in the given book. @@ -501,104 +445,7 @@ class EntityRepo $this->permissionService->buildJointPermissionsForEntity($entity); } - /** - * Prepare a string of search terms by turning - * it into an array of terms. - * Keeps quoted terms together. - * @param $termString - * @return array - */ - public function prepareSearchTerms($termString) - { - $termString = $this->cleanSearchTermString($termString); - preg_match_all('/(".*?")/', $termString, $matches); - $terms = []; - if (count($matches[1]) > 0) { - foreach ($matches[1] as $match) { - $terms[] = $match; - } - $termString = trim(preg_replace('/"(.*?)"/', '', $termString)); - } - if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString)); - return $terms; - } - /** - * Removes any special search notation that should not - * be used in a full-text search. - * @param $termString - * @return mixed - */ - protected function cleanSearchTermString($termString) - { - // Strip tag searches - $termString = preg_replace('/\[.*?\]/', '', $termString); - // Reduced multiple spacing into single spacing - $termString = preg_replace("/\s{2,}/", " ", $termString); - return $termString; - } - - /** - * Get the available query operators as a regex escaped list. - * @return mixed - */ - protected function getRegexEscapedOperators() - { - $escapedOperators = []; - foreach ($this->queryOperators as $operator) { - $escapedOperators[] = preg_quote($operator); - } - return join('|', $escapedOperators); - } - - /** - * Parses advanced search notations and adds them to the db query. - * @param $query - * @param $termString - * @return mixed - */ - protected function addAdvancedSearchQueries($query, $termString) - { - $escapedOperators = $this->getRegexEscapedOperators(); - // Look for tag searches - preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags); - if (count($tags[0]) > 0) { - $this->applyTagSearches($query, $tags); - } - - return $query; - } - - /** - * Apply extracted tag search terms onto a entity query. - * @param $query - * @param $tags - * @return mixed - */ - protected function applyTagSearches($query, $tags) { - $query->where(function($query) use ($tags) { - foreach ($tags[1] as $index => $tagName) { - $query->whereHas('tags', function($query) use ($tags, $index, $tagName) { - $tagOperator = $tags[3][$index]; - $tagValue = $tags[4][$index]; - if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) { - if (is_numeric($tagValue) && $tagOperator !== 'like') { - // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will - // search the value as a string which prevents being able to do number-based operations - // on the tag values. We ensure it has a numeric value and then cast it just to be sure. - $tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'"); - $query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}"); - } else { - $query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue); - } - } else { - $query->where('name', '=', $tagName); - } - }); - } - }); - return $query; - } /** * Create a new entity from request input. diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index 2df02bc3e..ec505af00 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -12,7 +12,6 @@ use Illuminate\Support\Collection; class SearchService { - protected $searchTerm; protected $book; protected $chapter; @@ -21,6 +20,12 @@ class SearchService protected $permissionService; protected $entities; + /** + * Acceptable operators to be used in a query + * @var array + */ + protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!=']; + /** * SearchService constructor. * @param SearchTerm $searchTerm @@ -55,11 +60,7 @@ class SearchService */ public function searchEntities($searchString, $entityType = 'all', $page = 0, $count = 20) { - // TODO - Add Tag Searches - // TODO - Add advanced custom column searches // TODO - Check drafts don't show up in results - // TODO - Move search all page to just /search?term=cat - if ($entityType !== 'all') return $this->searchEntityTable($searchString, $entityType, $page, $count); $bookSearch = $this->searchEntityTable($searchString, 'book', $page, $count); @@ -109,6 +110,19 @@ class SearchService }); } + // Handle tag searches + foreach ($searchTerms['tags'] as $inputTerm) { + $this->applyTagSearch($entitySelect, $inputTerm); + } + + // Handle filters + foreach ($searchTerms['filters'] as $filterTerm) { + $splitTerm = explode(':', $filterTerm); + $functionName = camel_case('filter_' . $splitTerm[0]); + $param = count($splitTerm) > 1 ? $splitTerm[1] : ''; + if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $param); + } + $entitySelect->skip($page * $count)->take($count); $query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view'); return $query->get(); @@ -120,7 +134,7 @@ class SearchService * @param $searchString * @return array */ - public function parseSearchString($searchString) + protected function parseSearchString($searchString) { $terms = [ 'search' => [], @@ -151,6 +165,50 @@ class SearchService return $terms; } + /** + * Get the available query operators as a regex escaped list. + * @return mixed + */ + protected function getRegexEscapedOperators() + { + $escapedOperators = []; + foreach ($this->queryOperators as $operator) { + $escapedOperators[] = preg_quote($operator); + } + return join('|', $escapedOperators); + } + + /** + * Apply a tag search term onto a entity query. + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $tagTerm + * @return mixed + */ + protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) { + preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit); + $query->whereHas('tags', function(\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) { + $tagName = $tagSplit[1]; + $tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : ''; + $tagValue = count($tagSplit) > 3 ? $tagSplit[4] : ''; + $validOperator = in_array($tagOperator, $this->queryOperators); + if (!empty($tagOperator) && !empty($tagValue) && $validOperator) { + if (!empty($tagName)) $query->where('name', '=', $tagName); + if (is_numeric($tagValue) && $tagOperator !== 'like') { + // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will + // search the value as a string which prevents being able to do number-based operations + // on the tag values. We ensure it has a numeric value and then cast it just to be sure. + $tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'"); + $query->whereRaw("value ${tagOperator} ${tagValue}"); + } else { + $query->where('value', $tagOperator, $tagValue); + } + } else { + $query->where('name', '=', $tagName); + } + }); + return $query; + } + /** * Get an entity instance via type. * @param $type @@ -258,4 +316,82 @@ class SearchService return $terms; } + + + + /** + * Custom entity search filters + */ + + protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + try { $date = date_create($input); + } catch (\Exception $e) {return;} + $query->where('updated_at', '>=', $date); + } + + protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + try { $date = date_create($input); + } catch (\Exception $e) {return;} + $query->where('updated_at', '<', $date); + } + + protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + try { $date = date_create($input); + } catch (\Exception $e) {return;} + $query->where('created_at', '>=', $date); + } + + protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + try { $date = date_create($input); + } catch (\Exception $e) {return;} + $query->where('created_at', '<', $date); + } + + protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + if (!is_numeric($input)) return; + $query->where('created_by', '=', $input); + } + + protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + if (!is_numeric($input)) return; + $query->where('updated_by', '=', $input); + } + + protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + $query->where('name', 'like', '%' .$input. '%'); + } + + protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) {$this->filterInName($query, $model, $input);} + + protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + $query->where($model->textField, 'like', '%' .$input. '%'); + } + + protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + $query->where('restricted', '=', true); + } + + protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + $query->whereHas('views', function($query) { + $query->where('user_id', '=', user()->id); + }); + } + + protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + $query->whereDoesntHave('views', function($query) { + $query->where('user_id', '=', user()->id); + }); + } + } \ No newline at end of file diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index 4287014c2..95a9d72b0 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -47,7 +47,7 @@ </a> </div> <div class="col-lg-4 col-sm-3 text-center"> - <form action="{{ baseUrl('/search/all') }}" method="GET" class="search-box"> + <form action="{{ baseUrl('/search') }}" method="GET" class="search-box"> <input id="header-search-box-input" type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}"> <button id="header-search-box-button" type="submit" class="text-button"><i class="zmdi zmdi-search"></i></button> </form> diff --git a/routes/web.php b/routes/web.php index 8259a633b..f5ee3f827 100644 --- a/routes/web.php +++ b/routes/web.php @@ -123,7 +123,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/link/{id}', 'PageController@redirectFromLink'); // Search - Route::get('/search/all', 'SearchController@searchAll'); + Route::get('/search', 'SearchController@searchAll'); Route::get('/search/pages', 'SearchController@searchPages'); Route::get('/search/books', 'SearchController@searchBooks'); Route::get('/search/chapters', 'SearchController@searchChapters');