Dan Brown

Added tag searches and advanced filters to new search

...@@ -65,12 +65,6 @@ class EntityRepo ...@@ -65,12 +65,6 @@ class EntityRepo
65 protected $searchService; 65 protected $searchService;
66 66
67 /** 67 /**
68 - * Acceptable operators to be used in a query
69 - * @var array
70 - */
71 - protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
72 -
73 - /**
74 * EntityRepo constructor. 68 * EntityRepo constructor.
75 * @param Book $book 69 * @param Book $book
76 * @param Chapter $chapter 70 * @param Chapter $chapter
...@@ -370,56 +364,6 @@ class EntityRepo ...@@ -370,56 +364,6 @@ class EntityRepo
370 ->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get(); 364 ->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
371 } 365 }
372 366
373 - /**
374 - * Search entities of a type via a given query.
375 - * @param string $type
376 - * @param string $term
377 - * @param array $whereTerms
378 - * @param int $count
379 - * @param array $paginationAppends
380 - * @return mixed
381 - */
382 - public function getBySearch($type, $term, $whereTerms = [], $count = 20, $paginationAppends = [])
383 - {
384 - $terms = $this->prepareSearchTerms($term);
385 - $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)->fullTextSearchQuery($terms, $whereTerms));
386 - $q = $this->addAdvancedSearchQueries($q, $term);
387 - $entities = $q->paginate($count)->appends($paginationAppends);
388 - $words = join('|', explode(' ', preg_quote(trim($term), '/')));
389 -
390 - // Highlight page content
391 - if ($type === 'page') {
392 - //lookahead/behind assertions ensures cut between words
393 - $s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
394 -
395 - foreach ($entities as $page) {
396 - preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
397 - //delimiter between occurrences
398 - $results = [];
399 - foreach ($matches as $line) {
400 - $results[] = htmlspecialchars($line[0], 0, 'UTF-8');
401 - }
402 - $matchLimit = 6;
403 - if (count($results) > $matchLimit) $results = array_slice($results, 0, $matchLimit);
404 - $result = join('... ', $results);
405 -
406 - //highlight
407 - $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
408 - if (strlen($result) < 5) $result = $page->getExcerpt(80);
409 -
410 - $page->searchSnippet = $result;
411 - }
412 - return $entities;
413 - }
414 -
415 - // Highlight chapter/book content
416 - foreach ($entities as $entity) {
417 - //highlight
418 - $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $entity->getExcerpt(100));
419 - $entity->searchSnippet = $result;
420 - }
421 - return $entities;
422 - }
423 367
424 /** 368 /**
425 * Get the next sequential priority for a new child element in the given book. 369 * Get the next sequential priority for a new child element in the given book.
...@@ -501,104 +445,7 @@ class EntityRepo ...@@ -501,104 +445,7 @@ class EntityRepo
501 $this->permissionService->buildJointPermissionsForEntity($entity); 445 $this->permissionService->buildJointPermissionsForEntity($entity);
502 } 446 }
503 447
504 - /**
505 - * Prepare a string of search terms by turning
506 - * it into an array of terms.
507 - * Keeps quoted terms together.
508 - * @param $termString
509 - * @return array
510 - */
511 - public function prepareSearchTerms($termString)
512 - {
513 - $termString = $this->cleanSearchTermString($termString);
514 - preg_match_all('/(".*?")/', $termString, $matches);
515 - $terms = [];
516 - if (count($matches[1]) > 0) {
517 - foreach ($matches[1] as $match) {
518 - $terms[] = $match;
519 - }
520 - $termString = trim(preg_replace('/"(.*?)"/', '', $termString));
521 - }
522 - if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
523 - return $terms;
524 - }
525 448
526 - /**
527 - * Removes any special search notation that should not
528 - * be used in a full-text search.
529 - * @param $termString
530 - * @return mixed
531 - */
532 - protected function cleanSearchTermString($termString)
533 - {
534 - // Strip tag searches
535 - $termString = preg_replace('/\[.*?\]/', '', $termString);
536 - // Reduced multiple spacing into single spacing
537 - $termString = preg_replace("/\s{2,}/", " ", $termString);
538 - return $termString;
539 - }
540 -
541 - /**
542 - * Get the available query operators as a regex escaped list.
543 - * @return mixed
544 - */
545 - protected function getRegexEscapedOperators()
546 - {
547 - $escapedOperators = [];
548 - foreach ($this->queryOperators as $operator) {
549 - $escapedOperators[] = preg_quote($operator);
550 - }
551 - return join('|', $escapedOperators);
552 - }
553 -
554 - /**
555 - * Parses advanced search notations and adds them to the db query.
556 - * @param $query
557 - * @param $termString
558 - * @return mixed
559 - */
560 - protected function addAdvancedSearchQueries($query, $termString)
561 - {
562 - $escapedOperators = $this->getRegexEscapedOperators();
563 - // Look for tag searches
564 - preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags);
565 - if (count($tags[0]) > 0) {
566 - $this->applyTagSearches($query, $tags);
567 - }
568 -
569 - return $query;
570 - }
571 -
572 - /**
573 - * Apply extracted tag search terms onto a entity query.
574 - * @param $query
575 - * @param $tags
576 - * @return mixed
577 - */
578 - protected function applyTagSearches($query, $tags) {
579 - $query->where(function($query) use ($tags) {
580 - foreach ($tags[1] as $index => $tagName) {
581 - $query->whereHas('tags', function($query) use ($tags, $index, $tagName) {
582 - $tagOperator = $tags[3][$index];
583 - $tagValue = $tags[4][$index];
584 - if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) {
585 - if (is_numeric($tagValue) && $tagOperator !== 'like') {
586 - // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
587 - // search the value as a string which prevents being able to do number-based operations
588 - // on the tag values. We ensure it has a numeric value and then cast it just to be sure.
589 - $tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
590 - $query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}");
591 - } else {
592 - $query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue);
593 - }
594 - } else {
595 - $query->where('name', '=', $tagName);
596 - }
597 - });
598 - }
599 - });
600 - return $query;
601 - }
602 449
603 /** 450 /**
604 * Create a new entity from request input. 451 * Create a new entity from request input.
......
...@@ -12,7 +12,6 @@ use Illuminate\Support\Collection; ...@@ -12,7 +12,6 @@ use Illuminate\Support\Collection;
12 12
13 class SearchService 13 class SearchService
14 { 14 {
15 -
16 protected $searchTerm; 15 protected $searchTerm;
17 protected $book; 16 protected $book;
18 protected $chapter; 17 protected $chapter;
...@@ -22,6 +21,12 @@ class SearchService ...@@ -22,6 +21,12 @@ class SearchService
22 protected $entities; 21 protected $entities;
23 22
24 /** 23 /**
24 + * Acceptable operators to be used in a query
25 + * @var array
26 + */
27 + protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
28 +
29 + /**
25 * SearchService constructor. 30 * SearchService constructor.
26 * @param SearchTerm $searchTerm 31 * @param SearchTerm $searchTerm
27 * @param Book $book 32 * @param Book $book
...@@ -55,11 +60,7 @@ class SearchService ...@@ -55,11 +60,7 @@ class SearchService
55 */ 60 */
56 public function searchEntities($searchString, $entityType = 'all', $page = 0, $count = 20) 61 public function searchEntities($searchString, $entityType = 'all', $page = 0, $count = 20)
57 { 62 {
58 - // TODO - Add Tag Searches
59 - // TODO - Add advanced custom column searches
60 // TODO - Check drafts don't show up in results 63 // TODO - Check drafts don't show up in results
61 - // TODO - Move search all page to just /search?term=cat
62 -
63 if ($entityType !== 'all') return $this->searchEntityTable($searchString, $entityType, $page, $count); 64 if ($entityType !== 'all') return $this->searchEntityTable($searchString, $entityType, $page, $count);
64 65
65 $bookSearch = $this->searchEntityTable($searchString, 'book', $page, $count); 66 $bookSearch = $this->searchEntityTable($searchString, 'book', $page, $count);
...@@ -109,6 +110,19 @@ class SearchService ...@@ -109,6 +110,19 @@ class SearchService
109 }); 110 });
110 } 111 }
111 112
113 + // Handle tag searches
114 + foreach ($searchTerms['tags'] as $inputTerm) {
115 + $this->applyTagSearch($entitySelect, $inputTerm);
116 + }
117 +
118 + // Handle filters
119 + foreach ($searchTerms['filters'] as $filterTerm) {
120 + $splitTerm = explode(':', $filterTerm);
121 + $functionName = camel_case('filter_' . $splitTerm[0]);
122 + $param = count($splitTerm) > 1 ? $splitTerm[1] : '';
123 + if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $param);
124 + }
125 +
112 $entitySelect->skip($page * $count)->take($count); 126 $entitySelect->skip($page * $count)->take($count);
113 $query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view'); 127 $query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
114 return $query->get(); 128 return $query->get();
...@@ -120,7 +134,7 @@ class SearchService ...@@ -120,7 +134,7 @@ class SearchService
120 * @param $searchString 134 * @param $searchString
121 * @return array 135 * @return array
122 */ 136 */
123 - public function parseSearchString($searchString) 137 + protected function parseSearchString($searchString)
124 { 138 {
125 $terms = [ 139 $terms = [
126 'search' => [], 140 'search' => [],
...@@ -152,6 +166,50 @@ class SearchService ...@@ -152,6 +166,50 @@ class SearchService
152 } 166 }
153 167
154 /** 168 /**
169 + * Get the available query operators as a regex escaped list.
170 + * @return mixed
171 + */
172 + protected function getRegexEscapedOperators()
173 + {
174 + $escapedOperators = [];
175 + foreach ($this->queryOperators as $operator) {
176 + $escapedOperators[] = preg_quote($operator);
177 + }
178 + return join('|', $escapedOperators);
179 + }
180 +
181 + /**
182 + * Apply a tag search term onto a entity query.
183 + * @param \Illuminate\Database\Eloquent\Builder $query
184 + * @param string $tagTerm
185 + * @return mixed
186 + */
187 + protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) {
188 + preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
189 + $query->whereHas('tags', function(\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
190 + $tagName = $tagSplit[1];
191 + $tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
192 + $tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';
193 + $validOperator = in_array($tagOperator, $this->queryOperators);
194 + if (!empty($tagOperator) && !empty($tagValue) && $validOperator) {
195 + if (!empty($tagName)) $query->where('name', '=', $tagName);
196 + if (is_numeric($tagValue) && $tagOperator !== 'like') {
197 + // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
198 + // search the value as a string which prevents being able to do number-based operations
199 + // on the tag values. We ensure it has a numeric value and then cast it just to be sure.
200 + $tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
201 + $query->whereRaw("value ${tagOperator} ${tagValue}");
202 + } else {
203 + $query->where('value', $tagOperator, $tagValue);
204 + }
205 + } else {
206 + $query->where('name', '=', $tagName);
207 + }
208 + });
209 + return $query;
210 + }
211 +
212 + /**
155 * Get an entity instance via type. 213 * Get an entity instance via type.
156 * @param $type 214 * @param $type
157 * @return Entity 215 * @return Entity
...@@ -258,4 +316,82 @@ class SearchService ...@@ -258,4 +316,82 @@ class SearchService
258 return $terms; 316 return $terms;
259 } 317 }
260 318
319 +
320 +
321 +
322 + /**
323 + * Custom entity search filters
324 + */
325 +
326 + protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
327 + {
328 + try { $date = date_create($input);
329 + } catch (\Exception $e) {return;}
330 + $query->where('updated_at', '>=', $date);
331 + }
332 +
333 + protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
334 + {
335 + try { $date = date_create($input);
336 + } catch (\Exception $e) {return;}
337 + $query->where('updated_at', '<', $date);
338 + }
339 +
340 + protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
341 + {
342 + try { $date = date_create($input);
343 + } catch (\Exception $e) {return;}
344 + $query->where('created_at', '>=', $date);
345 + }
346 +
347 + protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
348 + {
349 + try { $date = date_create($input);
350 + } catch (\Exception $e) {return;}
351 + $query->where('created_at', '<', $date);
352 + }
353 +
354 + protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
355 + {
356 + if (!is_numeric($input)) return;
357 + $query->where('created_by', '=', $input);
358 + }
359 +
360 + protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
361 + {
362 + if (!is_numeric($input)) return;
363 + $query->where('updated_by', '=', $input);
364 + }
365 +
366 + protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
367 + {
368 + $query->where('name', 'like', '%' .$input. '%');
369 + }
370 +
371 + protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) {$this->filterInName($query, $model, $input);}
372 +
373 + protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
374 + {
375 + $query->where($model->textField, 'like', '%' .$input. '%');
376 + }
377 +
378 + protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
379 + {
380 + $query->where('restricted', '=', true);
381 + }
382 +
383 + protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
384 + {
385 + $query->whereHas('views', function($query) {
386 + $query->where('user_id', '=', user()->id);
387 + });
388 + }
389 +
390 + protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
391 + {
392 + $query->whereDoesntHave('views', function($query) {
393 + $query->where('user_id', '=', user()->id);
394 + });
395 + }
396 +
261 } 397 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
47 </a> 47 </a>
48 </div> 48 </div>
49 <div class="col-lg-4 col-sm-3 text-center"> 49 <div class="col-lg-4 col-sm-3 text-center">
50 - <form action="{{ baseUrl('/search/all') }}" method="GET" class="search-box"> 50 + <form action="{{ baseUrl('/search') }}" method="GET" class="search-box">
51 <input id="header-search-box-input" type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}"> 51 <input id="header-search-box-input" type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}">
52 <button id="header-search-box-button" type="submit" class="text-button"><i class="zmdi zmdi-search"></i></button> 52 <button id="header-search-box-button" type="submit" class="text-button"><i class="zmdi zmdi-search"></i></button>
53 </form> 53 </form>
......
...@@ -123,7 +123,7 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -123,7 +123,7 @@ Route::group(['middleware' => 'auth'], function () {
123 Route::get('/link/{id}', 'PageController@redirectFromLink'); 123 Route::get('/link/{id}', 'PageController@redirectFromLink');
124 124
125 // Search 125 // Search
126 - Route::get('/search/all', 'SearchController@searchAll'); 126 + Route::get('/search', 'SearchController@searchAll');
127 Route::get('/search/pages', 'SearchController@searchPages'); 127 Route::get('/search/pages', 'SearchController@searchPages');
128 Route::get('/search/books', 'SearchController@searchBooks'); 128 Route::get('/search/books', 'SearchController@searchBooks');
129 Route::get('/search/chapters', 'SearchController@searchChapters'); 129 Route::get('/search/chapters', 'SearchController@searchChapters');
......