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.
This commit is contained in:
Dan Brown 2016-02-21 12:53:58 +00:00
parent b4dec2a99c
commit 61577cf6bf
12 changed files with 222 additions and 100 deletions

View File

@ -98,7 +98,7 @@ abstract class Entity extends Model
* @param string[] array $wheres * @param string[] array $wheres
* @return mixed * @return mixed
*/ */
public static function fullTextSearch($fieldsToSearch, $terms, $wheres = []) public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
{ {
$termString = ''; $termString = '';
foreach ($terms as $term) { foreach ($terms as $term) {
@ -107,7 +107,7 @@ abstract class Entity extends Model
$fields = implode(',', $fieldsToSearch); $fields = implode(',', $fieldsToSearch);
$termStringEscaped = \DB::connection()->getPdo()->quote($termString); $termStringEscaped = \DB::connection()->getPdo()->quote($termString);
$search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance')); $search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance'));
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termStringEscaped]); $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
// Add additional where terms // Add additional where terms
foreach ($wheres as $whereTerm) { foreach ($wheres as $whereTerm) {
@ -115,10 +115,13 @@ abstract class Entity extends Model
} }
// Load in relations // Load in relations
if (!static::isA('book')) $search = $search->with('book'); if (static::isA('page')) {
if (static::isA('page')) $search = $search->with('chapter'); $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
} else if (static::isA('chapter')) {
$search = $search->with('book');
}
return $search->orderBy('title_relevance', 'desc')->get(); return $search->orderBy('title_relevance', 'desc');
} }
/** /**

View File

@ -42,11 +42,77 @@ class SearchController extends Controller
return redirect()->back(); return redirect()->back();
} }
$searchTerm = $request->get('term'); $searchTerm = $request->get('term');
$pages = $this->pageRepo->getBySearch($searchTerm); $paginationAppends = $request->only('term');
$books = $this->bookRepo->getBySearch($searchTerm); $pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
$chapters = $this->chapterRepo->getBySearch($searchTerm); $books = $this->bookRepo->getBySearch($searchTerm, 10, $paginationAppends);
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends);
$this->setPageTitle('Search For ' . $searchTerm); $this->setPageTitle('Search For ' . $searchTerm);
return view('search/all', ['pages' => $pages, 'books' => $books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); return view('search/all', [
'pages' => $pages,
'books' => $books,
'chapters' => $chapters,
'searchTerm' => $searchTerm
]);
}
/**
* Search only the pages in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchPages(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
$this->setPageTitle('Page Search For ' . $searchTerm);
return view('search/entity-search-list', [
'entities' => $pages,
'title' => 'Page Search Results',
'searchTerm' => $searchTerm
]);
}
/**
* Search only the chapters in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchChapters(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
$this->setPageTitle('Chapter Search For ' . $searchTerm);
return view('search/entity-search-list', [
'entities' => $chapters,
'title' => 'Chapter Search Results',
'searchTerm' => $searchTerm
]);
}
/**
* Search only the books in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchBooks(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends);
$this->setPageTitle('Book Search For ' . $searchTerm);
return view('search/entity-search-list', [
'entities' => $books,
'title' => 'Book Search Results',
'searchTerm' => $searchTerm
]);
} }
/** /**

View File

@ -74,6 +74,9 @@ Route::group(['middleware' => 'auth'], function () {
// Search // Search
Route::get('/search/all', 'SearchController@searchAll'); Route::get('/search/all', 'SearchController@searchAll');
Route::get('/search/pages', 'SearchController@searchPages');
Route::get('/search/books', 'SearchController@searchBooks');
Route::get('/search/chapters', 'SearchController@searchChapters');
Route::get('/search/book/{bookId}', 'SearchController@searchBook'); Route::get('/search/book/{bookId}', 'SearchController@searchBook');
// Other Pages // Other Pages

View File

@ -218,12 +218,15 @@ class BookRepo
/** /**
* Get books by search term. * Get books by search term.
* @param $term * @param $term
* @param int $count
* @param array $paginationAppends
* @return mixed * @return mixed
*/ */
public function getBySearch($term) public function getBySearch($term, $count = 20, $paginationAppends = [])
{ {
$terms = explode(' ', $term); $terms = explode(' ', $term);
$books = $this->book->fullTextSearch(['name', 'description'], $terms); $books = $this->book->fullTextSearchQuery(['name', 'description'], $terms)
->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/'))); $words = join('|', explode(' ', preg_quote(trim($term), '/')));
foreach ($books as $book) { foreach ($books as $book) {
//highlight //highlight

View File

@ -125,12 +125,15 @@ class ChapterRepo
* Get chapters by the given search term. * Get chapters by the given search term.
* @param $term * @param $term
* @param array $whereTerms * @param array $whereTerms
* @param int $count
* @param array $paginationAppends
* @return mixed * @return mixed
*/ */
public function getBySearch($term, $whereTerms = []) public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
{ {
$terms = explode(' ', $term); $terms = explode(' ', $term);
$chapters = $this->chapter->fullTextSearch(['name', 'description'], $terms, $whereTerms); $chapters = $this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)
->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/'))); $words = join('|', explode(' ', preg_quote(trim($term), '/')));
foreach ($chapters as $chapter) { foreach ($chapters as $chapter) {
//highlight //highlight

View File

@ -175,14 +175,17 @@ class PageRepo
/** /**
* Gets pages by a search term. * Gets pages by a search term.
* Highlights page content for showing in results. * Highlights page content for showing in results.
* @param string $term * @param string $term
* @param array $whereTerms * @param array $whereTerms
* @param int $count
* @param array $paginationAppends
* @return mixed * @return mixed
*/ */
public function getBySearch($term, $whereTerms = []) public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
{ {
$terms = explode(' ', $term); $terms = explode(' ', $term);
$pages = $this->page->fullTextSearch(['name', 'text'], $terms, $whereTerms); $pages = $this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)
->paginate($count)->appends($paginationAppends);
// Add highlights to page text. // Add highlights to page text.
$words = join('|', explode(' ', preg_quote(trim($term), '/'))); $words = join('|', explode(' ', preg_quote(trim($term), '/')));

View File

@ -10,7 +10,7 @@
<p class="text-muted">{{ $chapter->getExcerpt() }}</p> <p class="text-muted">{{ $chapter->getExcerpt() }}</p>
@endif @endif
@if(count($chapter->pages) > 0 && !isset($hidePages)) @if(!isset($hidePages) && count($chapter->pages) > 0)
<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> <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>
<div class="inset-list"> <div class="inset-list">
@foreach($chapter->pages as $page) @foreach($chapter->pages as $page)

View File

@ -16,10 +16,10 @@
Last updated {{ $page->updated_at->diffForHumans() }} @if($page->updatedBy)by {{$page->updatedBy->name}} @endif Last updated {{ $page->updated_at->diffForHumans() }} @if($page->updatedBy)by {{$page->updatedBy->name}} @endif
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<a class="text-book" href="{{ $page->book->getUrl() }}"><i class="zmdi zmdi-book"></i>{{ $page->book->getExcerpt(30) }}</a> <a class="text-book" href="{{ $page->book->getUrl() }}"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName(30) }}</a>
<br> <br>
@if($page->chapter) @if($page->chapter)
<a class="text-chapter" href="{{ $page->chapter->getUrl() }}"><i class="zmdi zmdi-collection-bookmark"></i>{{ $page->chapter->getExcerpt(30) }}</a> <a class="text-chapter" href="{{ $page->chapter->getUrl() }}"><i class="zmdi zmdi-collection-bookmark"></i>{{ $page->chapter->getShortName(30) }}</a>
@else @else
<i class="zmdi zmdi-collection-bookmark"></i> Page is not in a chapter <i class="zmdi zmdi-collection-bookmark"></i> Page is not in a chapter
@endif @endif

View File

@ -6,41 +6,36 @@
<h1>Search Results&nbsp;&nbsp;&nbsp; <span class="text-muted">{{$searchTerm}}</span></h1> <h1>Search Results&nbsp;&nbsp;&nbsp; <span class="text-muted">{{$searchTerm}}</span></h1>
<p>
<a href="/search/pages?term={{$searchTerm}}" class="text-page"><i class="zmdi zmdi-file-text"></i>View all matched pages</a>
@if(count($chapters) > 0)
&nbsp; &nbsp;&nbsp;
<a href="/search/chapters?term={{$searchTerm}}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>View all matched chapters</a>
@endif
@if(count($books) > 0)
&nbsp; &nbsp;&nbsp;
<a href="/search/books?term={{$searchTerm}}" class="text-book"><i class="zmdi zmdi-book"></i>View all matched books</a>
@endif
</p>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h3>Matching Pages</h3> <h3><a href="/search/pages?term={{$searchTerm}}" class="no-color">Matching Pages</a></h3>
<div class="page-list"> @include('partials/entity-list', ['entities' => $pages, 'style' => 'detailed'])
@if(count($pages) > 0)
@foreach($pages as $page)
@include('pages/list-item', ['page' => $page, 'style' => 'detailed'])
<hr>
@endforeach
@else
<p class="text-muted">No pages matched this search</p>
@endif
</div>
</div> </div>
<div class="col-md-5 col-md-offset-1"> <div class="col-md-5 col-md-offset-1">
@if(count($books) > 0) @if(count($books) > 0)
<h3>Matching Books</h3> <h3><a href="/search/books?term={{$searchTerm}}" class="no-color">Matching Books</a></h3>
<div class="page-list"> @include('partials/entity-list', ['entities' => $books])
@foreach($books as $book)
@include('books/list-item', ['book' => $book])
<hr>
@endforeach
</div>
@endif @endif
@if(count($chapters) > 0) @if(count($chapters) > 0)
<h3>Matching Chapters</h3> <h3><a href="/search/chapters?term={{$searchTerm}}" class="no-color">Matching Chapters</a></h3>
<div class="page-list"> @include('partials/entity-list', ['entities' => $chapters])
@foreach($chapters as $chapter)
@include('chapters/list-item', ['chapter' => $chapter, 'hidePages' => true])
@endforeach
</div>
@endif @endif
</div> </div>

View File

@ -0,0 +1,18 @@
@extends('base')
@section('content')
<div class="container">
<div class="row">
<div class="col-sm-7">
<h1>{{ $title }} <small>{{$searchTerm}}</small></h1>
@include('partials.entity-list', ['entities' => $entities, 'style' => 'detailed'])
{!! $entities->links() !!}
</div>
<div class="col-sm-4 col-sm-offset-1"></div>
</div>
</div>
@stop

View File

@ -0,0 +1,85 @@
<?php
use Illuminate\Support\Facades\DB;
class EntitySearchTest extends TestCase
{
public function test_page_search()
{
$book = \BookStack\Book::all()->first();
$page = $book->pages->first();
$this->asAdmin()
->visit('/')
->type($page->name, 'term')
->press('header-search-box-button')
->see('Search Results')
->see($page->name)
->click($page->name)
->seePageIs($page->getUrl());
}
public function test_invalid_page_search()
{
$this->asAdmin()
->visit('/')
->type('<p>test</p>', 'term')
->press('header-search-box-button')
->see('Search Results')
->seeStatusCode(200);
}
public function test_empty_search_redirects_back()
{
$this->asAdmin()
->visit('/')
->visit('/search/all')
->seePageIs('/');
}
public function test_book_search()
{
$book = \BookStack\Book::all()->first();
$page = $book->pages->last();
$chapter = $book->chapters->last();
$this->asAdmin()
->visit('/search/book/' . $book->id . '?term=' . urlencode($page->name))
->see($page->name)
->visit('/search/book/' . $book->id . '?term=' . urlencode($chapter->name))
->see($chapter->name);
}
public function test_empty_book_search_redirects_back()
{
$book = \BookStack\Book::all()->first();
$this->asAdmin()
->visit('/books')
->visit('/search/book/' . $book->id . '?term=')
->seePageIs('/books');
}
public function test_pages_search_listing()
{
$page = \BookStack\Page::all()->last();
$this->asAdmin()->visit('/search/pages?term=' . $page->name)
->see('Page Search Results')->see('.entity-list', $page->name);
}
public function test_chapters_search_listing()
{
$chapter = \BookStack\Chapter::all()->last();
$this->asAdmin()->visit('/search/chapters?term=' . $chapter->name)
->see('Chapter Search Results')->seeInElement('.entity-list', $chapter->name);
}
public function test_books_search_listing()
{
$book = \BookStack\Book::all()->last();
$this->asAdmin()->visit('/search/books?term=' . $book->name)
->see('Book Search Results')->see('.entity-list', $book->name);
}
}

View File

@ -155,63 +155,6 @@ class EntityTest extends TestCase
return $book; return $book;
} }
public function test_page_search()
{
$book = \BookStack\Book::all()->first();
$page = $book->pages->first();
$this->asAdmin()
->visit('/')
->type($page->name, 'term')
->press('header-search-box-button')
->see('Search Results')
->see($page->name)
->click($page->name)
->seePageIs($page->getUrl());
}
public function test_invalid_page_search()
{
$this->asAdmin()
->visit('/')
->type('<p>test</p>', 'term')
->press('header-search-box-button')
->see('Search Results')
->seeStatusCode(200);
}
public function test_empty_search_redirects_back()
{
$this->asAdmin()
->visit('/')
->visit('/search/all')
->seePageIs('/');
}
public function test_book_search()
{
$book = \BookStack\Book::all()->first();
$page = $book->pages->last();
$chapter = $book->chapters->last();
$this->asAdmin()
->visit('/search/book/' . $book->id . '?term=' . urlencode($page->name))
->see($page->name)
->visit('/search/book/' . $book->id . '?term=' . urlencode($chapter->name))
->see($chapter->name);
}
public function test_empty_book_search_redirects_back()
{
$book = \BookStack\Book::all()->first();
$this->asAdmin()
->visit('/books')
->visit('/search/book/' . $book->id . '?term=')
->seePageIs('/books');
}
public function test_entities_viewable_after_creator_deletion() public function test_entities_viewable_after_creator_deletion()
{ {
// Create required assets and revisions // Create required assets and revisions