Added Bookshelves to search system.

Also cleaned up and made search indexing system a little more efficient.
Closes #1023
This commit is contained in:
Dan Brown 2018-09-23 12:34:30 +01:00
parent eebfd8904e
commit 7b32aa163f
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
5 changed files with 36 additions and 24 deletions

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Services;
use BookStack\Book; use BookStack\Book;
use BookStack\Bookshelf;
use BookStack\Chapter; use BookStack\Chapter;
use BookStack\Entity; use BookStack\Entity;
use BookStack\Page; use BookStack\Page;
@ -13,11 +14,16 @@ use Illuminate\Support\Collection;
class SearchService class SearchService
{ {
protected $searchTerm; protected $searchTerm;
protected $bookshelf;
protected $book; protected $book;
protected $chapter; protected $chapter;
protected $page; protected $page;
protected $db; protected $db;
protected $permissionService; protected $permissionService;
/**
* @var Entity[]
*/
protected $entities; protected $entities;
/** /**
@ -29,20 +35,23 @@ class SearchService
/** /**
* SearchService constructor. * SearchService constructor.
* @param SearchTerm $searchTerm * @param SearchTerm $searchTerm
* @param Bookshelf $bookshelf
* @param Book $book * @param Book $book
* @param Chapter $chapter * @param Chapter $chapter
* @param Page $page * @param Page $page
* @param Connection $db * @param Connection $db
* @param PermissionService $permissionService * @param PermissionService $permissionService
*/ */
public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService) public function __construct(SearchTerm $searchTerm, Bookshelf $bookshelf, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService)
{ {
$this->searchTerm = $searchTerm; $this->searchTerm = $searchTerm;
$this->bookshelf = $bookshelf;
$this->book = $book; $this->book = $book;
$this->chapter = $chapter; $this->chapter = $chapter;
$this->page = $page; $this->page = $page;
$this->db = $db; $this->db = $db;
$this->entities = [ $this->entities = [
'bookshelf' => $this->bookshelf,
'page' => $this->page, 'page' => $this->page,
'chapter' => $this->chapter, 'chapter' => $this->chapter,
'book' => $this->book 'book' => $this->book
@ -65,6 +74,7 @@ class SearchService
* @param string $entityType * @param string $entityType
* @param int $page * @param int $page
* @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed. * @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
* @param string $action
* @return array[int, Collection]; * @return array[int, Collection];
*/ */
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view') public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view')
@ -370,20 +380,12 @@ class SearchService
{ {
$this->searchTerm->truncate(); $this->searchTerm->truncate();
// Chunk through all books foreach ($this->entities as $entityModel) {
$this->book->chunk(1000, function ($books) { $selectFields = ['id', 'name', $entityModel->textField];
$this->indexEntities($books); $entityModel->newQuery()->select($selectFields)->chunk(1000, function ($entities) {
}); $this->indexEntities($entities);
});
// Chunk through all chapters }
$this->chapter->chunk(1000, function ($chapters) {
$this->indexEntities($chapters);
});
// Chunk through all pages
$this->page->chunk(1000, function ($pages) {
$this->indexEntities($pages);
});
} }
/** /**

View File

@ -7,7 +7,8 @@ let data = {
type: { type: {
page: true, page: true,
chapter: true, chapter: true,
book: true book: true,
bookshelf: true,
}, },
exactTerms: [], exactTerms: [],
tagTerms: [], tagTerms: [],
@ -46,11 +47,7 @@ let methods = {
exactChange() { exactChange() {
let exactFilter = /"(.+?)"/g; let exactFilter = /"(.+?)"/g;
this.termString = this.termString.replace(exactFilter, ''); this.termString = this.termString.replace(exactFilter, '');
let matchesTerm = this.search.exactTerms.filter(term => { let matchesTerm = this.search.exactTerms.filter(term => term.trim() !== '').map(term => `"${term}"`).join(' ');
return term.trim() !== '';
}).map(term => {
return `"${term}"`
}).join(' ');
this.appendTerm(matchesTerm); this.appendTerm(matchesTerm);
}, },
@ -105,23 +102,24 @@ let methods = {
let match = searchString.match(typeFilter); let match = searchString.match(typeFilter);
let type = this.search.type; let type = this.search.type;
if (!match) { if (!match) {
type.page = type.book = type.chapter = true; type.page = type.book = type.chapter = type.bookshelf = true;
return; return;
} }
let splitTypes = match[1].replace(/ /g, '').split('|'); let splitTypes = match[1].replace(/ /g, '').split('|');
type.page = (splitTypes.indexOf('page') !== -1); type.page = (splitTypes.indexOf('page') !== -1);
type.chapter = (splitTypes.indexOf('chapter') !== -1); type.chapter = (splitTypes.indexOf('chapter') !== -1);
type.book = (splitTypes.indexOf('book') !== -1); type.book = (splitTypes.indexOf('book') !== -1);
type.bookshelf = (splitTypes.indexOf('bookshelf') !== -1);
}, },
typeChange() { typeChange() {
let typeFilter = /{\s?type:\s?(.*?)\s?}/; let typeFilter = /{\s?type:\s?(.*?)\s?}/;
let type = this.search.type; let type = this.search.type;
if (type.page === type.chapter && type.page === type.book) { if (type.page === type.chapter === type.book === type.bookshelf) {
this.termString = this.termString.replace(typeFilter, ''); this.termString = this.termString.replace(typeFilter, '');
return; return;
} }
let selectedTypes = Object.keys(type).filter(type => {return this.search.type[type];}).join('|'); let selectedTypes = Object.keys(type).filter(type => this.search.type[type]).join('|');
let typeTerm = '{type:'+selectedTypes+'}'; let typeTerm = '{type:'+selectedTypes+'}';
if (this.termString.match(typeFilter)) { if (this.termString.match(typeFilter)) {
this.termString = this.termString.replace(typeFilter, typeTerm); this.termString = this.termString.replace(typeFilter, typeTerm);

View File

@ -69,6 +69,7 @@ return [
/** /**
* Shelves * Shelves
*/ */
'shelf' => 'Shelf',
'shelves' => 'Shelves', 'shelves' => 'Shelves',
'shelves_long' => 'Bookshelves', 'shelves_long' => 'Bookshelves',
'shelves_empty' => 'No shelves have been created', 'shelves_empty' => 'No shelves have been created',

View File

@ -22,7 +22,9 @@
<div class="form-group"> <div class="form-group">
<label class="inline checkbox text-page"><input type="checkbox" v-on:change="typeChange" v-model="search.type.page" value="page">{{ trans('entities.page') }}</label> <label class="inline checkbox text-page"><input type="checkbox" v-on:change="typeChange" v-model="search.type.page" value="page">{{ trans('entities.page') }}</label>
<label class="inline checkbox text-chapter"><input type="checkbox" v-on:change="typeChange" v-model="search.type.chapter" value="chapter">{{ trans('entities.chapter') }}</label> <label class="inline checkbox text-chapter"><input type="checkbox" v-on:change="typeChange" v-model="search.type.chapter" value="chapter">{{ trans('entities.chapter') }}</label>
<br>
<label class="inline checkbox text-book"><input type="checkbox" v-on:change="typeChange" v-model="search.type.book" value="book">{{ trans('entities.book') }}</label> <label class="inline checkbox text-book"><input type="checkbox" v-on:change="typeChange" v-model="search.type.book" value="book">{{ trans('entities.book') }}</label>
<label class="inline checkbox text-bookshelf"><input type="checkbox" v-on:change="typeChange" v-model="search.type.bookshelf" value="bookshelf">{{ trans('entities.shelf') }}</label>
</div> </div>
<h6 class="text-muted">{{ trans('entities.search_exact_matches') }}</h6> <h6 class="text-muted">{{ trans('entities.search_exact_matches') }}</h6>

View File

@ -1,6 +1,7 @@
<?php namespace Tests; <?php namespace Tests;
use BookStack\Bookshelf;
use BookStack\Chapter; use BookStack\Chapter;
use BookStack\Page; use BookStack\Page;
@ -17,6 +18,14 @@ class EntitySearchTest extends TestCase
$search->assertSee($page->name); $search->assertSee($page->name);
} }
public function test_bookshelf_search()
{
$shelf = Bookshelf::first();
$search = $this->asEditor()->get('/search?term=' . urlencode(mb_substr($shelf->name, 0, 3)) . ' {type:bookshelf}');
$search->assertStatus(200);
$search->assertSee($shelf->name);
}
public function test_invalid_page_search() public function test_invalid_page_search()
{ {
$resp = $this->asEditor()->get('/search?term=' . urlencode('<p>test</p>')); $resp = $this->asEditor()->get('/search?term=' . urlencode('<p>test</p>'));