return $pages->sortBy('priority');
}
+ public function getExcerpt($length = 100)
+ {
+ return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description;
+ }
+
}
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();
+ }
+
}
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
--- /dev/null
+<?php
+
+namespace Oxbow\Http\Controllers;
+
+use Illuminate\Http\Request;
+
+use Oxbow\Http\Requests;
+use Oxbow\Http\Controllers\Controller;
+use Oxbow\Repos\BookRepo;
+use Oxbow\Repos\ChapterRepo;
+use Oxbow\Repos\PageRepo;
+
+class SearchController extends Controller
+{
+ protected $pageRepo;
+ protected $bookRepo;
+ protected $chapterRepo;
+
+ /**
+ * SearchController constructor.
+ * @param $pageRepo
+ * @param $bookRepo
+ * @param $chapterRepo
+ */
+ public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo)
+ {
+ $this->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]);
+ }
+
+
+}
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');
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', "<span class=\"highlight\">\$0</span>", $book->getExcerpt(100));
+ $book->searchSnippet = $result;
+ }
+ return $books;
+ }
+
}
\ No newline at end of file
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', "<span class=\"highlight\">\$0</span>", $chapter->getExcerpt(100));
+ $chapter->searchSnippet = $result;
+ }
+ return $chapters;
+ }
+
}
\ No newline at end of file
/**
* PageRepo constructor.
- * @param Page $page
+ * @param Page $page
* @param PageRevision $pageRevision
*/
public function __construct(Page $page, PageRevision $pageRevision)
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', "<span class=\"highlight\">\$0</span>", $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)
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());
$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();
}
/**
* 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;
/**
* 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;
public function remove($key)
{
$setting = $this->getSettingObjectByKey($key);
- if($setting) {
+ if ($setting) {
$setting->delete();
}
return true;
* @param $key
* @return mixed
*/
- private function getSettingObjectByKey($key) {
+ private function getSettingObjectByKey($key)
+ {
return $this->setting->where('setting_key', '=', $key)->first();
}
--- /dev/null
+<?php
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class AddSearchIndexes extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ DB::statement('ALTER TABLE pages ADD FULLTEXT search(name, text)');
+ DB::statement('ALTER TABLE books ADD FULLTEXT search(name, description)');
+ DB::statement('ALTER TABLE chapters ADD FULLTEXT search(name, description)');
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('pages', function(Blueprint $table) {
+ $table->dropIndex('search');
+ });
+ Schema::table('books', function(Blueprint $table) {
+ $table->dropIndex('search');
+ });
+ Schema::table('chapters', function(Blueprint $table) {
+ $table->dropIndex('search');
+ });
+ }
+}
}
}
+span.highlight {
+ //background-color: rgba($primary, 0.2);
+ font-weight: bold;
+ //padding: 2px 4px;
+}
+
/*
* Lists
*/
padding-right: 0;
}
}
- .search-box {
- padding-top: $-l *0.8;
- }
.avatar, .user-name {
display: inline-block;
}
}
}
+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;
<div class="col-md-3">
<a href="/" class="logo">{{ Setting::get('app-name', 'BookStack') }}</a>
</div>
- <div class="col-md-9">
+ <div class="col-md-3 text-right">
+ <form action="/search/all" method="GET" class="search-box">
+ <i class="zmdi zmdi-search"></i>
+ <input type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}">
+ </form>
+ </div>
+ <div class="col-md-6">
<div class="float right">
<div class="links text-center">
- <a href="/search"><i class="zmdi zmdi-search"></i></a>
<a href="/books"><i class="zmdi zmdi-book"></i>Books</a>
@if($currentUser->can('settings-update'))
<a href="/settings"><i class="zmdi zmdi-settings"></i>Settings</a>
+++ /dev/null
-@extends('base')
-
-@section('content')
-
-
- <div class="row">
-
- <div class="col-md-3 page-menu">
-
- </div>
-
- <div class="col-md-9 page-content">
- <h1>Search Results <span class="subheader">For '{{$searchTerm}}'</span></h1>
- <div class="page-list">
- @if(count($pages) > 0)
- @foreach($pages as $page)
- <a href="{{$page->getUrl() . '#' . $searchTerm}}">{{$page->name}}</a>
- @endforeach
- @else
- <p class="text-muted">No pages matched this search</p>
- @endif
- </div>
- </div>
-
- </div>
-
-
-
-
-@stop
\ No newline at end of file
--- /dev/null
+@extends('base')
+
+@section('content')
+
+ <div class="container">
+
+ <h1>Search Results <span class="text-muted">{{$searchTerm}}</span></h1>
+
+ <div class="row">
+
+ <div class="col-md-6">
+ <h3>Matching Pages</h3>
+ <div class="page-list">
+ @if(count($pages) > 0)
+ @foreach($pages as $page)
+ <div class="book-child">
+ <h3>
+ <a href="{{$page->getUrl() . '#' . $searchTerm}}" class="page">
+ <i class="zmdi zmdi-file-text"></i>{{$page->name}}
+ </a>
+ </h3>
+ <p class="text-muted">
+ {!! $page->searchSnippet !!}
+ </p>
+ <hr>
+ </div>
+ @endforeach
+ @else
+ <p class="text-muted">No pages matched this search</p>
+ @endif
+ </div>
+ </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)
+ <div class="book-child">
+ <h3>
+ <a href="{{$book->getUrl()}}" class="text-book">
+ <i class="zmdi zmdi-book"></i>{{$book->name}}
+ </a>
+ </h3>
+ <p class="text-muted">
+ {!! $book->searchSnippet !!}
+ </p>
+ <hr>
+ </div>
+ @endforeach
+ </div>
+ @endif
+
+ @if(count($chapters) > 0)
+ <h3>Matching Chapters</h3>
+ <div class="page-list">
+ @foreach($chapters as $chapter)
+ <div class="book-child">
+ <h3>
+ <a href="{{$chapter->getUrl()}}" class="text-chapter">
+ <i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->name}}
+ </a>
+ </h3>
+ <p class="text-muted">
+ {!! $chapter->searchSnippet !!}
+ </p>
+ <hr>
+ </div>
+ @endforeach
+ </div>
+ @endif
+
+ </div>
+
+
+ </div>
+
+
+ </div>
+
+
+
+
+@stop
\ No newline at end of file