Dan Brown

Added entity-specific search results pages. Cleaned & Fixed search results bugs

Added search result pages for pages, chapters and books.
Limited the results on the global search as it just listed out an infinate amount.
Fixed styling on new detailed page listings and also removed the 'bars' from the side to create  a cleaner view.
Fixed bad sql fulltext query format that may have thrown off searches.
Reduced the number of database queries down a thousand or so.
...@@ -98,7 +98,7 @@ abstract class Entity extends Model ...@@ -98,7 +98,7 @@ abstract class Entity extends Model
98 * @param string[] array $wheres 98 * @param string[] array $wheres
99 * @return mixed 99 * @return mixed
100 */ 100 */
101 - public static function fullTextSearch($fieldsToSearch, $terms, $wheres = []) 101 + public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
102 { 102 {
103 $termString = ''; 103 $termString = '';
104 foreach ($terms as $term) { 104 foreach ($terms as $term) {
...@@ -107,7 +107,7 @@ abstract class Entity extends Model ...@@ -107,7 +107,7 @@ abstract class Entity extends Model
107 $fields = implode(',', $fieldsToSearch); 107 $fields = implode(',', $fieldsToSearch);
108 $termStringEscaped = \DB::connection()->getPdo()->quote($termString); 108 $termStringEscaped = \DB::connection()->getPdo()->quote($termString);
109 $search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance')); 109 $search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance'));
110 - $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termStringEscaped]); 110 + $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
111 111
112 // Add additional where terms 112 // Add additional where terms
113 foreach ($wheres as $whereTerm) { 113 foreach ($wheres as $whereTerm) {
...@@ -115,10 +115,13 @@ abstract class Entity extends Model ...@@ -115,10 +115,13 @@ abstract class Entity extends Model
115 } 115 }
116 116
117 // Load in relations 117 // Load in relations
118 - if (!static::isA('book')) $search = $search->with('book'); 118 + if (static::isA('page')) {
119 - if (static::isA('page')) $search = $search->with('chapter'); 119 + $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
120 + } else if (static::isA('chapter')) {
121 + $search = $search->with('book');
122 + }
120 123
121 - return $search->orderBy('title_relevance', 'desc')->get(); 124 + return $search->orderBy('title_relevance', 'desc');
122 } 125 }
123 126
124 /** 127 /**
......
...@@ -42,11 +42,77 @@ class SearchController extends Controller ...@@ -42,11 +42,77 @@ class SearchController extends Controller
42 return redirect()->back(); 42 return redirect()->back();
43 } 43 }
44 $searchTerm = $request->get('term'); 44 $searchTerm = $request->get('term');
45 - $pages = $this->pageRepo->getBySearch($searchTerm); 45 + $paginationAppends = $request->only('term');
46 - $books = $this->bookRepo->getBySearch($searchTerm); 46 + $pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
47 - $chapters = $this->chapterRepo->getBySearch($searchTerm); 47 + $books = $this->bookRepo->getBySearch($searchTerm, 10, $paginationAppends);
48 + $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends);
48 $this->setPageTitle('Search For ' . $searchTerm); 49 $this->setPageTitle('Search For ' . $searchTerm);
49 - return view('search/all', ['pages' => $pages, 'books' => $books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); 50 + return view('search/all', [
51 + 'pages' => $pages,
52 + 'books' => $books,
53 + 'chapters' => $chapters,
54 + 'searchTerm' => $searchTerm
55 + ]);
56 + }
57 +
58 + /**
59 + * Search only the pages in the system.
60 + * @param Request $request
61 + * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
62 + */
63 + public function searchPages(Request $request)
64 + {
65 + if (!$request->has('term')) return redirect()->back();
66 +
67 + $searchTerm = $request->get('term');
68 + $paginationAppends = $request->only('term');
69 + $pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
70 + $this->setPageTitle('Page Search For ' . $searchTerm);
71 + return view('search/entity-search-list', [
72 + 'entities' => $pages,
73 + 'title' => 'Page Search Results',
74 + 'searchTerm' => $searchTerm
75 + ]);
76 + }
77 +
78 + /**
79 + * Search only the chapters in the system.
80 + * @param Request $request
81 + * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
82 + */
83 + public function searchChapters(Request $request)
84 + {
85 + if (!$request->has('term')) return redirect()->back();
86 +
87 + $searchTerm = $request->get('term');
88 + $paginationAppends = $request->only('term');
89 + $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
90 + $this->setPageTitle('Chapter Search For ' . $searchTerm);
91 + return view('search/entity-search-list', [
92 + 'entities' => $chapters,
93 + 'title' => 'Chapter Search Results',
94 + 'searchTerm' => $searchTerm
95 + ]);
96 + }
97 +
98 + /**
99 + * Search only the books in the system.
100 + * @param Request $request
101 + * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
102 + */
103 + public function searchBooks(Request $request)
104 + {
105 + if (!$request->has('term')) return redirect()->back();
106 +
107 + $searchTerm = $request->get('term');
108 + $paginationAppends = $request->only('term');
109 + $books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends);
110 + $this->setPageTitle('Book Search For ' . $searchTerm);
111 + return view('search/entity-search-list', [
112 + 'entities' => $books,
113 + 'title' => 'Book Search Results',
114 + 'searchTerm' => $searchTerm
115 + ]);
50 } 116 }
51 117
52 /** 118 /**
......
...@@ -74,6 +74,9 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -74,6 +74,9 @@ Route::group(['middleware' => 'auth'], function () {
74 74
75 // Search 75 // Search
76 Route::get('/search/all', 'SearchController@searchAll'); 76 Route::get('/search/all', 'SearchController@searchAll');
77 + Route::get('/search/pages', 'SearchController@searchPages');
78 + Route::get('/search/books', 'SearchController@searchBooks');
79 + Route::get('/search/chapters', 'SearchController@searchChapters');
77 Route::get('/search/book/{bookId}', 'SearchController@searchBook'); 80 Route::get('/search/book/{bookId}', 'SearchController@searchBook');
78 81
79 // Other Pages 82 // Other Pages
......
...@@ -218,12 +218,15 @@ class BookRepo ...@@ -218,12 +218,15 @@ class BookRepo
218 /** 218 /**
219 * Get books by search term. 219 * Get books by search term.
220 * @param $term 220 * @param $term
221 + * @param int $count
222 + * @param array $paginationAppends
221 * @return mixed 223 * @return mixed
222 */ 224 */
223 - public function getBySearch($term) 225 + public function getBySearch($term, $count = 20, $paginationAppends = [])
224 { 226 {
225 $terms = explode(' ', $term); 227 $terms = explode(' ', $term);
226 - $books = $this->book->fullTextSearch(['name', 'description'], $terms); 228 + $books = $this->book->fullTextSearchQuery(['name', 'description'], $terms)
229 + ->paginate($count)->appends($paginationAppends);
227 $words = join('|', explode(' ', preg_quote(trim($term), '/'))); 230 $words = join('|', explode(' ', preg_quote(trim($term), '/')));
228 foreach ($books as $book) { 231 foreach ($books as $book) {
229 //highlight 232 //highlight
......
...@@ -125,12 +125,15 @@ class ChapterRepo ...@@ -125,12 +125,15 @@ class ChapterRepo
125 * Get chapters by the given search term. 125 * Get chapters by the given search term.
126 * @param $term 126 * @param $term
127 * @param array $whereTerms 127 * @param array $whereTerms
128 + * @param int $count
129 + * @param array $paginationAppends
128 * @return mixed 130 * @return mixed
129 */ 131 */
130 - public function getBySearch($term, $whereTerms = []) 132 + public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
131 { 133 {
132 $terms = explode(' ', $term); 134 $terms = explode(' ', $term);
133 - $chapters = $this->chapter->fullTextSearch(['name', 'description'], $terms, $whereTerms); 135 + $chapters = $this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)
136 + ->paginate($count)->appends($paginationAppends);
134 $words = join('|', explode(' ', preg_quote(trim($term), '/'))); 137 $words = join('|', explode(' ', preg_quote(trim($term), '/')));
135 foreach ($chapters as $chapter) { 138 foreach ($chapters as $chapter) {
136 //highlight 139 //highlight
......
...@@ -175,14 +175,17 @@ class PageRepo ...@@ -175,14 +175,17 @@ class PageRepo
175 /** 175 /**
176 * Gets pages by a search term. 176 * Gets pages by a search term.
177 * Highlights page content for showing in results. 177 * Highlights page content for showing in results.
178 - * @param string $term 178 + * @param string $term
179 * @param array $whereTerms 179 * @param array $whereTerms
180 + * @param int $count
181 + * @param array $paginationAppends
180 * @return mixed 182 * @return mixed
181 */ 183 */
182 - public function getBySearch($term, $whereTerms = []) 184 + public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
183 { 185 {
184 $terms = explode(' ', $term); 186 $terms = explode(' ', $term);
185 - $pages = $this->page->fullTextSearch(['name', 'text'], $terms, $whereTerms); 187 + $pages = $this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)
188 + ->paginate($count)->appends($paginationAppends);
186 189
187 // Add highlights to page text. 190 // Add highlights to page text.
188 $words = join('|', explode(' ', preg_quote(trim($term), '/'))); 191 $words = join('|', explode(' ', preg_quote(trim($term), '/')));
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
10 <p class="text-muted">{{ $chapter->getExcerpt() }}</p> 10 <p class="text-muted">{{ $chapter->getExcerpt() }}</p>
11 @endif 11 @endif
12 12
13 - @if(count($chapter->pages) > 0 && !isset($hidePages)) 13 + @if(!isset($hidePages) && count($chapter->pages) > 0)
14 <p class="text-muted chapter-toggle"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ count($chapter->pages) }} Pages</span></p> 14 <p class="text-muted chapter-toggle"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ count($chapter->pages) }} Pages</span></p>
15 <div class="inset-list"> 15 <div class="inset-list">
16 @foreach($chapter->pages as $page) 16 @foreach($chapter->pages as $page)
......
...@@ -16,10 +16,10 @@ ...@@ -16,10 +16,10 @@
16 Last updated {{ $page->updated_at->diffForHumans() }} @if($page->updatedBy)by {{$page->updatedBy->name}} @endif 16 Last updated {{ $page->updated_at->diffForHumans() }} @if($page->updatedBy)by {{$page->updatedBy->name}} @endif
17 </div> 17 </div>
18 <div class="col-md-8"> 18 <div class="col-md-8">
19 - <a class="text-book" href="{{ $page->book->getUrl() }}"><i class="zmdi zmdi-book"></i>{{ $page->book->getExcerpt(30) }}</a> 19 + <a class="text-book" href="{{ $page->book->getUrl() }}"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName(30) }}</a>
20 <br> 20 <br>
21 @if($page->chapter) 21 @if($page->chapter)
22 - <a class="text-chapter" href="{{ $page->chapter->getUrl() }}"><i class="zmdi zmdi-collection-bookmark"></i>{{ $page->chapter->getExcerpt(30) }}</a> 22 + <a class="text-chapter" href="{{ $page->chapter->getUrl() }}"><i class="zmdi zmdi-collection-bookmark"></i>{{ $page->chapter->getShortName(30) }}</a>
23 @else 23 @else
24 <i class="zmdi zmdi-collection-bookmark"></i> Page is not in a chapter 24 <i class="zmdi zmdi-collection-bookmark"></i> Page is not in a chapter
25 @endif 25 @endif
......
...@@ -6,41 +6,36 @@ ...@@ -6,41 +6,36 @@
6 6
7 <h1>Search Results&nbsp;&nbsp;&nbsp; <span class="text-muted">{{$searchTerm}}</span></h1> 7 <h1>Search Results&nbsp;&nbsp;&nbsp; <span class="text-muted">{{$searchTerm}}</span></h1>
8 8
9 + <p>
10 + <a href="/search/pages?term={{$searchTerm}}" class="text-page"><i class="zmdi zmdi-file-text"></i>View all matched pages</a>
11 +
12 + @if(count($chapters) > 0)
13 + &nbsp; &nbsp;&nbsp;
14 + <a href="/search/chapters?term={{$searchTerm}}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>View all matched chapters</a>
15 + @endif
16 +
17 + @if(count($books) > 0)
18 + &nbsp; &nbsp;&nbsp;
19 + <a href="/search/books?term={{$searchTerm}}" class="text-book"><i class="zmdi zmdi-book"></i>View all matched books</a>
20 + @endif
21 + </p>
9 <div class="row"> 22 <div class="row">
10 23
11 <div class="col-md-6"> 24 <div class="col-md-6">
12 - <h3>Matching Pages</h3> 25 + <h3><a href="/search/pages?term={{$searchTerm}}" class="no-color">Matching Pages</a></h3>
13 - <div class="page-list"> 26 + @include('partials/entity-list', ['entities' => $pages, 'style' => 'detailed'])
14 - @if(count($pages) > 0)
15 - @foreach($pages as $page)
16 - @include('pages/list-item', ['page' => $page, 'style' => 'detailed'])
17 - <hr>
18 - @endforeach
19 - @else
20 - <p class="text-muted">No pages matched this search</p>
21 - @endif
22 - </div>
23 </div> 27 </div>
24 28
25 <div class="col-md-5 col-md-offset-1"> 29 <div class="col-md-5 col-md-offset-1">
26 30
27 @if(count($books) > 0) 31 @if(count($books) > 0)
28 - <h3>Matching Books</h3> 32 + <h3><a href="/search/books?term={{$searchTerm}}" class="no-color">Matching Books</a></h3>
29 - <div class="page-list"> 33 + @include('partials/entity-list', ['entities' => $books])
30 - @foreach($books as $book)
31 - @include('books/list-item', ['book' => $book])
32 - <hr>
33 - @endforeach
34 - </div>
35 @endif 34 @endif
36 35
37 @if(count($chapters) > 0) 36 @if(count($chapters) > 0)
38 - <h3>Matching Chapters</h3> 37 + <h3><a href="/search/chapters?term={{$searchTerm}}" class="no-color">Matching Chapters</a></h3>
39 - <div class="page-list"> 38 + @include('partials/entity-list', ['entities' => $chapters])
40 - @foreach($chapters as $chapter)
41 - @include('chapters/list-item', ['chapter' => $chapter, 'hidePages' => true])
42 - @endforeach
43 - </div>
44 @endif 39 @endif
45 40
46 </div> 41 </div>
......
1 +@extends('base')
2 +
3 +@section('content')
4 +
5 + <div class="container">
6 + <div class="row">
7 +
8 + <div class="col-sm-7">
9 + <h1>{{ $title }} <small>{{$searchTerm}}</small></h1>
10 + @include('partials.entity-list', ['entities' => $entities, 'style' => 'detailed'])
11 + {!! $entities->links() !!}
12 + </div>
13 +
14 + <div class="col-sm-4 col-sm-offset-1"></div>
15 +
16 + </div>
17 + </div>
18 +@stop
...\ No newline at end of file ...\ No newline at end of file
1 +<?php
2 +
3 +use Illuminate\Support\Facades\DB;
4 +
5 +class EntitySearchTest extends TestCase
6 +{
7 +
8 + public function test_page_search()
9 + {
10 + $book = \BookStack\Book::all()->first();
11 + $page = $book->pages->first();
12 +
13 + $this->asAdmin()
14 + ->visit('/')
15 + ->type($page->name, 'term')
16 + ->press('header-search-box-button')
17 + ->see('Search Results')
18 + ->see($page->name)
19 + ->click($page->name)
20 + ->seePageIs($page->getUrl());
21 + }
22 +
23 + public function test_invalid_page_search()
24 + {
25 + $this->asAdmin()
26 + ->visit('/')
27 + ->type('<p>test</p>', 'term')
28 + ->press('header-search-box-button')
29 + ->see('Search Results')
30 + ->seeStatusCode(200);
31 + }
32 +
33 + public function test_empty_search_redirects_back()
34 + {
35 + $this->asAdmin()
36 + ->visit('/')
37 + ->visit('/search/all')
38 + ->seePageIs('/');
39 + }
40 +
41 + public function test_book_search()
42 + {
43 + $book = \BookStack\Book::all()->first();
44 + $page = $book->pages->last();
45 + $chapter = $book->chapters->last();
46 +
47 + $this->asAdmin()
48 + ->visit('/search/book/' . $book->id . '?term=' . urlencode($page->name))
49 + ->see($page->name)
50 +
51 + ->visit('/search/book/' . $book->id . '?term=' . urlencode($chapter->name))
52 + ->see($chapter->name);
53 + }
54 +
55 + public function test_empty_book_search_redirects_back()
56 + {
57 + $book = \BookStack\Book::all()->first();
58 + $this->asAdmin()
59 + ->visit('/books')
60 + ->visit('/search/book/' . $book->id . '?term=')
61 + ->seePageIs('/books');
62 + }
63 +
64 +
65 + public function test_pages_search_listing()
66 + {
67 + $page = \BookStack\Page::all()->last();
68 + $this->asAdmin()->visit('/search/pages?term=' . $page->name)
69 + ->see('Page Search Results')->see('.entity-list', $page->name);
70 + }
71 +
72 + public function test_chapters_search_listing()
73 + {
74 + $chapter = \BookStack\Chapter::all()->last();
75 + $this->asAdmin()->visit('/search/chapters?term=' . $chapter->name)
76 + ->see('Chapter Search Results')->seeInElement('.entity-list', $chapter->name);
77 + }
78 +
79 + public function test_books_search_listing()
80 + {
81 + $book = \BookStack\Book::all()->last();
82 + $this->asAdmin()->visit('/search/books?term=' . $book->name)
83 + ->see('Book Search Results')->see('.entity-list', $book->name);
84 + }
85 +}
...@@ -155,63 +155,6 @@ class EntityTest extends TestCase ...@@ -155,63 +155,6 @@ class EntityTest extends TestCase
155 return $book; 155 return $book;
156 } 156 }
157 157
158 - public function test_page_search()
159 - {
160 - $book = \BookStack\Book::all()->first();
161 - $page = $book->pages->first();
162 -
163 - $this->asAdmin()
164 - ->visit('/')
165 - ->type($page->name, 'term')
166 - ->press('header-search-box-button')
167 - ->see('Search Results')
168 - ->see($page->name)
169 - ->click($page->name)
170 - ->seePageIs($page->getUrl());
171 - }
172 -
173 - public function test_invalid_page_search()
174 - {
175 - $this->asAdmin()
176 - ->visit('/')
177 - ->type('<p>test</p>', 'term')
178 - ->press('header-search-box-button')
179 - ->see('Search Results')
180 - ->seeStatusCode(200);
181 - }
182 -
183 - public function test_empty_search_redirects_back()
184 - {
185 - $this->asAdmin()
186 - ->visit('/')
187 - ->visit('/search/all')
188 - ->seePageIs('/');
189 - }
190 -
191 - public function test_book_search()
192 - {
193 - $book = \BookStack\Book::all()->first();
194 - $page = $book->pages->last();
195 - $chapter = $book->chapters->last();
196 -
197 - $this->asAdmin()
198 - ->visit('/search/book/' . $book->id . '?term=' . urlencode($page->name))
199 - ->see($page->name)
200 -
201 - ->visit('/search/book/' . $book->id . '?term=' . urlencode($chapter->name))
202 - ->see($chapter->name);
203 - }
204 -
205 - public function test_empty_book_search_redirects_back()
206 - {
207 - $book = \BookStack\Book::all()->first();
208 - $this->asAdmin()
209 - ->visit('/books')
210 - ->visit('/search/book/' . $book->id . '?term=')
211 - ->seePageIs('/books');
212 - }
213 -
214 -
215 public function test_entities_viewable_after_creator_deletion() 158 public function test_entities_viewable_after_creator_deletion()
216 { 159 {
217 // Create required assets and revisions 160 // Create required assets and revisions
......