From 8d80e7311cf4166b5c9597dc793a8f53b13fd7f3 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 15 May 2016 13:41:18 +0100 Subject: [PATCH] Added tag searching to search interfaces --- app/Entity.php | 64 +++++++------ app/Repos/BookRepo.php | 5 +- app/Repos/ChapterRepo.php | 5 +- app/Repos/EntityRepo.php | 98 +++++++++++++++++++- app/Repos/PageRepo.php | 5 +- resources/views/pages/page-display.blade.php | 4 +- 6 files changed, 143 insertions(+), 38 deletions(-) diff --git a/app/Entity.php b/app/Entity.php index 5ea1d3876..1342c2997 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -157,48 +157,54 @@ class Entity extends Ownable * @param string[] array $wheres * @return mixed */ - public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = []) + public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = []) { $exactTerms = []; - foreach ($terms as $key => $term) { - $term = htmlentities($term, ENT_QUOTES); - $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term); - if (preg_match('/\s/', $term)) { - $exactTerms[] = '%' . $term . '%'; - $term = '"' . $term . '"'; - } else { - $term = '' . $term . '*'; - } - if ($term !== '*') $terms[$key] = $term; - } - $termString = implode(' ', $terms); - $fields = implode(',', $fieldsToSearch); - $search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]); - $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]); - - // Ensure at least one exact term matches if in search - if (count($exactTerms) > 0) { - $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) { - foreach ($exactTerms as $exactTerm) { - foreach ($fieldsToSearch as $field) { - $query->orWhere($field, 'like', $exactTerm); - } + if (count($terms) === 0) { + $search = $this; + $orderBy = 'updated_at'; + } else { + foreach ($terms as $key => $term) { + $term = htmlentities($term, ENT_QUOTES); + $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term); + if (preg_match('/\s/', $term)) { + $exactTerms[] = '%' . $term . '%'; + $term = '"' . $term . '"'; + } else { + $term = '' . $term . '*'; } - }); - } + if ($term !== '*') $terms[$key] = $term; + } + $termString = implode(' ', $terms); + $fields = implode(',', $fieldsToSearch); + $search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]); + $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]); + + // Ensure at least one exact term matches if in search + if (count($exactTerms) > 0) { + $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) { + foreach ($exactTerms as $exactTerm) { + foreach ($fieldsToSearch as $field) { + $query->orWhere($field, 'like', $exactTerm); + } + } + }); + } + $orderBy = 'title_relevance'; + }; // Add additional where terms foreach ($wheres as $whereTerm) { $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]); } // Load in relations - if (static::isA('page')) { + if ($this->isA('page')) { $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy'); - } else if (static::isA('chapter')) { + } else if ($this->isA('chapter')) { $search = $search->with('book'); } - return $search->orderBy('title_relevance', 'desc'); + return $search->orderBy($orderBy, 'desc'); } } diff --git a/app/Repos/BookRepo.php b/app/Repos/BookRepo.php index e62b101c5..b0530b4f5 100644 --- a/app/Repos/BookRepo.php +++ b/app/Repos/BookRepo.php @@ -286,8 +286,9 @@ class BookRepo extends EntityRepo public function getBySearch($term, $count = 20, $paginationAppends = []) { $terms = $this->prepareSearchTerms($term); - $books = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms)) - ->paginate($count)->appends($paginationAppends); + $bookQuery = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms)); + $bookQuery = $this->addAdvancedSearchQueries($bookQuery, $term); + $books = $bookQuery->paginate($count)->appends($paginationAppends); $words = join('|', explode(' ', preg_quote(trim($term), '/'))); foreach ($books as $book) { //highlight diff --git a/app/Repos/ChapterRepo.php b/app/Repos/ChapterRepo.php index 0980e93a7..048e0a63b 100644 --- a/app/Repos/ChapterRepo.php +++ b/app/Repos/ChapterRepo.php @@ -168,8 +168,9 @@ class ChapterRepo extends EntityRepo public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) { $terms = $this->prepareSearchTerms($term); - $chapters = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)) - ->paginate($count)->appends($paginationAppends); + $chapterQuery = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)); + $chapterQuery = $this->addAdvancedSearchQueries($chapterQuery, $term); + $chapters = $chapterQuery->paginate($count)->appends($paginationAppends); $words = join('|', explode(' ', preg_quote(trim($term), '/'))); foreach ($chapters as $chapter) { //highlight diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index 6b4076e6e..012a64967 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -6,6 +6,7 @@ use BookStack\Entity; use BookStack\Page; use BookStack\Services\PermissionService; use BookStack\User; +use Illuminate\Support\Facades\Log; class EntityRepo { @@ -30,6 +31,12 @@ class EntityRepo */ protected $permissionService; + /** + * Acceptable operators to be used in a query + * @var array + */ + protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!=']; + /** * EntityService constructor. */ @@ -163,6 +170,7 @@ class EntityRepo */ protected function prepareSearchTerms($termString) { + $termString = $this->cleanSearchTermString($termString); preg_match_all('/"(.*?)"/', $termString, $matches); if (count($matches[1]) > 0) { $terms = $matches[1]; @@ -174,5 +182,93 @@ class EntityRepo 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; + } + +} + + + + + + + + + + + -} \ No newline at end of file diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index a28717b76..992e97cda 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -245,8 +245,9 @@ class PageRepo extends EntityRepo public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) { $terms = $this->prepareSearchTerms($term); - $pages = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)) - ->paginate($count)->appends($paginationAppends); + $pageQuery = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)); + $pageQuery = $this->addAdvancedSearchQueries($pageQuery, $term); + $pages = $pageQuery->paginate($count)->appends($paginationAppends); // Add highlights to page text. $words = join('|', explode(' ', preg_quote(trim($term), '/'))); diff --git a/resources/views/pages/page-display.blade.php b/resources/views/pages/page-display.blade.php index df555da07..d0bdcf880 100644 --- a/resources/views/pages/page-display.blade.php +++ b/resources/views/pages/page-display.blade.php @@ -8,8 +8,8 @@ @foreach($page->tags as $tag) - - @if($tag->value) @endif + + @if($tag->value) @endif @endforeach
value) colspan="2" @endif> {{ $tag->name }}{{$tag->value}}value) colspan="2" @endif>{{ $tag->name }}{{$tag->value}}