mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
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:
parent
b4dec2a99c
commit
61577cf6bf
@ -98,7 +98,7 @@ abstract class Entity extends Model
|
||||
* @param string[] array $wheres
|
||||
* @return mixed
|
||||
*/
|
||||
public static function fullTextSearch($fieldsToSearch, $terms, $wheres = [])
|
||||
public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
|
||||
{
|
||||
$termString = '';
|
||||
foreach ($terms as $term) {
|
||||
@ -107,7 +107,7 @@ abstract class Entity extends Model
|
||||
$fields = implode(',', $fieldsToSearch);
|
||||
$termStringEscaped = \DB::connection()->getPdo()->quote($termString);
|
||||
$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
|
||||
foreach ($wheres as $whereTerm) {
|
||||
@ -115,10 +115,13 @@ abstract class Entity extends Model
|
||||
}
|
||||
|
||||
// Load in relations
|
||||
if (!static::isA('book')) $search = $search->with('book');
|
||||
if (static::isA('page')) $search = $search->with('chapter');
|
||||
if (static::isA('page')) {
|
||||
$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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -42,11 +42,77 @@ class SearchController extends Controller
|
||||
return redirect()->back();
|
||||
}
|
||||
$searchTerm = $request->get('term');
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm);
|
||||
$books = $this->bookRepo->getBySearch($searchTerm);
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm);
|
||||
$paginationAppends = $request->only('term');
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
|
||||
$books = $this->bookRepo->getBySearch($searchTerm, 10, $paginationAppends);
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends);
|
||||
$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
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -74,6 +74,9 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
|
||||
// Search
|
||||
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');
|
||||
|
||||
// Other Pages
|
||||
|
@ -218,12 +218,15 @@ class BookRepo
|
||||
/**
|
||||
* Get books by search term.
|
||||
* @param $term
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term)
|
||||
public function getBySearch($term, $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$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), '/')));
|
||||
foreach ($books as $book) {
|
||||
//highlight
|
||||
|
@ -125,12 +125,15 @@ class ChapterRepo
|
||||
* Get chapters by the given search term.
|
||||
* @param $term
|
||||
* @param array $whereTerms
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term, $whereTerms = [])
|
||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$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), '/')));
|
||||
foreach ($chapters as $chapter) {
|
||||
//highlight
|
||||
|
@ -175,14 +175,17 @@ class PageRepo
|
||||
/**
|
||||
* Gets pages by a search term.
|
||||
* Highlights page content for showing in results.
|
||||
* @param string $term
|
||||
* @param string $term
|
||||
* @param array $whereTerms
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term, $whereTerms = [])
|
||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$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.
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
|
@ -10,7 +10,7 @@
|
||||
<p class="text-muted">{{ $chapter->getExcerpt() }}</p>
|
||||
@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>
|
||||
<div class="inset-list">
|
||||
@foreach($chapter->pages as $page)
|
||||
|
@ -16,10 +16,10 @@
|
||||
Last updated {{ $page->updated_at->diffForHumans() }} @if($page->updatedBy)by {{$page->updatedBy->name}} @endif
|
||||
</div>
|
||||
<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>
|
||||
@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
|
||||
<i class="zmdi zmdi-collection-bookmark"></i> Page is not in a chapter
|
||||
@endif
|
||||
|
@ -6,41 +6,36 @@
|
||||
|
||||
<h1>Search Results <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)
|
||||
|
||||
<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)
|
||||
|
||||
<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="col-md-6">
|
||||
<h3>Matching Pages</h3>
|
||||
<div class="page-list">
|
||||
@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>
|
||||
<h3><a href="/search/pages?term={{$searchTerm}}" class="no-color">Matching Pages</a></h3>
|
||||
@include('partials/entity-list', ['entities' => $pages, 'style' => 'detailed'])
|
||||
</div>
|
||||
|
||||
<div class="col-md-5 col-md-offset-1">
|
||||
|
||||
@if(count($books) > 0)
|
||||
<h3>Matching Books</h3>
|
||||
<div class="page-list">
|
||||
@foreach($books as $book)
|
||||
@include('books/list-item', ['book' => $book])
|
||||
<hr>
|
||||
@endforeach
|
||||
</div>
|
||||
<h3><a href="/search/books?term={{$searchTerm}}" class="no-color">Matching Books</a></h3>
|
||||
@include('partials/entity-list', ['entities' => $books])
|
||||
@endif
|
||||
|
||||
@if(count($chapters) > 0)
|
||||
<h3>Matching Chapters</h3>
|
||||
<div class="page-list">
|
||||
@foreach($chapters as $chapter)
|
||||
@include('chapters/list-item', ['chapter' => $chapter, 'hidePages' => true])
|
||||
@endforeach
|
||||
</div>
|
||||
<h3><a href="/search/chapters?term={{$searchTerm}}" class="no-color">Matching Chapters</a></h3>
|
||||
@include('partials/entity-list', ['entities' => $chapters])
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
18
resources/views/search/entity-search-list.blade.php
Normal file
18
resources/views/search/entity-search-list.blade.php
Normal 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
|
85
tests/EntitySearchTest.php
Normal file
85
tests/EntitySearchTest.php
Normal 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);
|
||||
}
|
||||
}
|
@ -155,63 +155,6 @@ class EntityTest extends TestCase
|
||||
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()
|
||||
{
|
||||
// Create required assets and revisions
|
||||
|
Loading…
Reference in New Issue
Block a user