From 9a82d2754874c8594ee8bcc7334068a2f2162a7b Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 31 Aug 2015 20:11:44 +0100 Subject: [PATCH] Updated Search experience including adding fulltext mysql indicies. --- app/Book.php | 5 ++ app/Entity.php | 20 +++++ app/Http/Controllers/PageController.php | 14 --- app/Http/Controllers/SearchController.php | 52 ++++++++++++ app/Http/routes.php | 2 +- app/Repos/BookRepo.php | 13 +++ app/Repos/ChapterRepo.php | 13 +++ app/Repos/PageRepo.php | 55 ++++++++---- app/Services/SettingService.php | 5 +- .../2015_08_31_175240_add_search_indexes.php | 37 ++++++++ resources/assets/sass/_text.scss | 6 ++ resources/assets/sass/styles.scss | 20 ++++- resources/views/base.blade.php | 9 +- .../views/pages/search-results.blade.php | 30 ------- resources/views/search/all.blade.php | 85 +++++++++++++++++++ 15 files changed, 298 insertions(+), 68 deletions(-) create mode 100644 app/Http/Controllers/SearchController.php create mode 100644 database/migrations/2015_08_31_175240_add_search_indexes.php delete mode 100644 resources/views/pages/search-results.blade.php create mode 100644 resources/views/search/all.blade.php diff --git a/app/Book.php b/app/Book.php index 8a4be213f..c6e5f60a7 100644 --- a/app/Book.php +++ b/app/Book.php @@ -37,4 +37,9 @@ class Book extends Entity return $pages->sortBy('priority'); } + public function getExcerpt($length = 100) + { + return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description; + } + } diff --git a/app/Entity.php b/app/Entity.php index 7c08cee90..0fd5d1e12 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -55,10 +55,30 @@ class Entity extends Model return $this->getName() === strtolower($type); } + /** + * Gets the class name. + * @return string + */ public function getName() { $fullClassName = get_class($this); return strtolower(array_slice(explode('\\', $fullClassName), -1, 1)[0]); } + /** + * Perform a full-text search on this entity. + * @param string[] $fieldsToSearch + * @param string[] $terms + * @return mixed + */ + public static function fullTextSearch($fieldsToSearch, $terms) + { + $termString = ''; + foreach($terms as $term) { + $termString .= $term . '* '; + } + $fields = implode(',', $fieldsToSearch); + return static::whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString])->get(); + } + } diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index b59dd2446..4bcdbb80b 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -142,20 +142,6 @@ class PageController extends Controller return redirect($page->getUrl()); } - /** - * Search all available pages, Across all books. - * @param Request $request - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View - */ - public function searchAll(Request $request) - { - $searchTerm = $request->get('term'); - if (empty($searchTerm)) return redirect()->back(); - - $pages = $this->pageRepo->getBySearch($searchTerm); - return view('pages/search-results', ['pages' => $pages, 'searchTerm' => $searchTerm]); - } - /** * Shows the view which allows pages to be re-ordered and sorted. * @param $bookSlug diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php new file mode 100644 index 000000000..074a4c777 --- /dev/null +++ b/app/Http/Controllers/SearchController.php @@ -0,0 +1,52 @@ +pageRepo = $pageRepo; + $this->bookRepo = $bookRepo; + $this->chapterRepo = $chapterRepo; + parent::__construct(); + } + + /** + * Searches all entities. + * @param Request $request + * @return \Illuminate\View\View + * @internal param string $searchTerm + */ + public function searchAll(Request $request) + { + if(!$request->has('term')) { + return redirect()->back(); + } + $searchTerm = $request->get('term'); + $pages = $this->pageRepo->getBySearch($searchTerm); + $books = $this->bookRepo->getBySearch($searchTerm); + $chapters = $this->chapterRepo->getBySearch($searchTerm); + return view('search/all', ['pages' => $pages, 'books'=>$books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); + } + + +} diff --git a/app/Http/routes.php b/app/Http/routes.php index 2dfd35abd..c7ff66f21 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -65,7 +65,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/link/{id}', 'PageController@redirectFromLink'); // Search - Route::get('/pages/search/all', 'PageController@searchAll'); + Route::get('/search/all', 'SearchController@searchAll'); // Other Pages Route::get('/', 'HomeController@index'); diff --git a/app/Repos/BookRepo.php b/app/Repos/BookRepo.php index 488478aac..5ddf0b1ef 100644 --- a/app/Repos/BookRepo.php +++ b/app/Repos/BookRepo.php @@ -81,4 +81,17 @@ class BookRepo return $slug; } + public function getBySearch($term) + { + $terms = explode(' ', preg_quote(trim($term))); + $books = $this->book->fullTextSearch(['name', 'description'], $terms); + $words = join('|', $terms); + foreach ($books as $book) { + //highlight + $result = preg_replace('#' . $words . '#iu', "\$0", $book->getExcerpt(100)); + $book->searchSnippet = $result; + } + return $books; + } + } \ No newline at end of file diff --git a/app/Repos/ChapterRepo.php b/app/Repos/ChapterRepo.php index 1eb819a82..0dcaa5fbd 100644 --- a/app/Repos/ChapterRepo.php +++ b/app/Repos/ChapterRepo.php @@ -67,4 +67,17 @@ class ChapterRepo return $slug; } + public function getBySearch($term) + { + $terms = explode(' ', preg_quote(trim($term))); + $chapters = $this->chapter->fullTextSearch(['name', 'description'], $terms); + $words = join('|', $terms); + foreach ($chapters as $chapter) { + //highlight + $result = preg_replace('#' . $words . '#iu', "\$0", $chapter->getExcerpt(100)); + $chapter->searchSnippet = $result; + } + return $chapters; + } + } \ No newline at end of file diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index 52620961f..51a3e8ce9 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -13,7 +13,7 @@ class PageRepo /** * PageRepo constructor. - * @param Page $page + * @param Page $page * @param PageRevision $pageRevision */ public function __construct(Page $page, PageRevision $pageRevision) @@ -61,19 +61,42 @@ class PageRepo public function getBySearch($term) { - $terms = explode(' ', trim($term)); - $query = $this->page; - foreach($terms as $term) { - $query = $query->where('text', 'like', '%'.$term.'%'); + $terms = explode(' ', preg_quote(trim($term))); + $pages = $this->page->fullTextSearch(['name', 'text'], $terms); + + // Add highlights to page text. + $words = join('|', $terms); + //lookahead/behind assertions ensures cut between words + $s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words + + foreach ($pages 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', "\$0", $result); + if (strlen($result) < 5) { + $result = $page->getExcerpt(80); + } + $page->searchSnippet = $result; } - return $query->get(); + return $pages; } /** * Updates a page with any fillable data and saves it into the database. * @param Page $page - * @param $book_id - * @param $data + * @param $book_id + * @param $data * @return Page */ public function updatePage(Page $page, $book_id, $data) @@ -95,7 +118,7 @@ class PageRepo public function saveRevision(Page $page) { $lastRevision = $this->getLastRevision($page); - if($lastRevision && ($lastRevision->html === $page->html && $lastRevision->name === $page->name)) { + if ($lastRevision && ($lastRevision->html === $page->html && $lastRevision->name === $page->name)) { return $page; } $revision = $this->pageRevision->fill($page->toArray()); @@ -103,7 +126,7 @@ class PageRepo $revision->created_by = Auth::user()->id; $revision->save(); // Clear old revisions - if($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { + if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { $this->pageRevision->where('page_id', '=', $page->id) ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete(); } @@ -133,15 +156,15 @@ class PageRepo /** * Checks if a slug exists within a book already. - * @param $slug - * @param $bookId + * @param $slug + * @param $bookId * @param bool|false $currentId * @return bool */ public function doesSlugExist($slug, $bookId, $currentId = false) { $query = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId); - if($currentId) { + if ($currentId) { $query = $query->where('id', '!=', $currentId); } return $query->count() > 0; @@ -150,15 +173,15 @@ class PageRepo /** * Gets a suitable slug for the resource * - * @param $name - * @param $bookId + * @param $name + * @param $bookId * @param bool|false $currentId * @return string */ public function findSuitableSlug($name, $bookId, $currentId = false) { $slug = Str::slug($name); - while($this->doesSlugExist($slug, $bookId, $currentId)) { + while ($this->doesSlugExist($slug, $bookId, $currentId)) { $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); } return $slug; diff --git a/app/Services/SettingService.php b/app/Services/SettingService.php index 4c27035fd..46c802a05 100644 --- a/app/Services/SettingService.php +++ b/app/Services/SettingService.php @@ -71,7 +71,7 @@ class SettingService public function remove($key) { $setting = $this->getSettingObjectByKey($key); - if($setting) { + if ($setting) { $setting->delete(); } return true; @@ -82,7 +82,8 @@ class SettingService * @param $key * @return mixed */ - private function getSettingObjectByKey($key) { + private function getSettingObjectByKey($key) + { return $this->setting->where('setting_key', '=', $key)->first(); } diff --git a/database/migrations/2015_08_31_175240_add_search_indexes.php b/database/migrations/2015_08_31_175240_add_search_indexes.php new file mode 100644 index 000000000..99e5a28f0 --- /dev/null +++ b/database/migrations/2015_08_31_175240_add_search_indexes.php @@ -0,0 +1,37 @@ +dropIndex('search'); + }); + Schema::table('books', function(Blueprint $table) { + $table->dropIndex('search'); + }); + Schema::table('chapters', function(Blueprint $table) { + $table->dropIndex('search'); + }); + } +} diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss index 6a63f1e5e..7015400a9 100644 --- a/resources/assets/sass/_text.scss +++ b/resources/assets/sass/_text.scss @@ -211,6 +211,12 @@ p.secondary, p .secondary, span.secondary, .text-secondary { } } +span.highlight { + //background-color: rgba($primary, 0.2); + font-weight: bold; + //padding: 2px 4px; +} + /* * Lists */ diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index 68b1c0125..0101aed9f 100644 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@ -36,9 +36,6 @@ header { padding-right: 0; } } - .search-box { - padding-top: $-l *0.8; - } .avatar, .user-name { display: inline-block; } @@ -59,6 +56,23 @@ header { } } +form.search-box { + padding-top: $-l *0.9; + display: inline-block; + input { + background-color: transparent; + border-radius: 0; + border: none; + border-bottom: 2px solid #EEE; + color: #EEE; + padding-left: $-l; + outline: 0; + } + i { + margin-right: -$-l; + } +} + #content { display: block; position: relative; diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index 2d3e54ae1..f20126b37 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -55,10 +55,15 @@
-
+
+ +
+