Dan Brown

Started implementation of new search system

...@@ -56,4 +56,13 @@ class Book extends Entity ...@@ -56,4 +56,13 @@ class Book extends Entity
56 return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description; 56 return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
57 } 57 }
58 58
59 + /**
60 + * Return a generalised, common raw query that can be 'unioned' across entities.
61 + * @return string
62 + */
63 + public function entityRawQuery()
64 + {
65 + return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
66 + }
67 +
59 } 68 }
......
...@@ -51,4 +51,13 @@ class Chapter extends Entity ...@@ -51,4 +51,13 @@ class Chapter extends Entity
51 return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description; 51 return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
52 } 52 }
53 53
54 + /**
55 + * Return a generalised, common raw query that can be 'unioned' across entities.
56 + * @return string
57 + */
58 + public function entityRawQuery()
59 + {
60 + return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
61 + }
62 +
54 } 63 }
......
1 +<?php
2 +
3 +namespace BookStack\Console\Commands;
4 +
5 +use BookStack\Services\SearchService;
6 +use Illuminate\Console\Command;
7 +
8 +class RegenerateSearch extends Command
9 +{
10 + /**
11 + * The name and signature of the console command.
12 + *
13 + * @var string
14 + */
15 + protected $signature = 'bookstack:regenerate-search';
16 +
17 + /**
18 + * The console command description.
19 + *
20 + * @var string
21 + */
22 + protected $description = 'Command description';
23 +
24 + protected $searchService;
25 +
26 + /**
27 + * Create a new command instance.
28 + *
29 + * @param SearchService $searchService
30 + */
31 + public function __construct(SearchService $searchService)
32 + {
33 + parent::__construct();
34 + $this->searchService = $searchService;
35 + }
36 +
37 + /**
38 + * Execute the console command.
39 + *
40 + * @return mixed
41 + */
42 + public function handle()
43 + {
44 + $this->searchService->indexAllEntities();
45 + }
46 +}
1 -<?php 1 +<?php namespace BookStack\Console;
2 -
3 -namespace BookStack\Console;
4 2
5 use Illuminate\Console\Scheduling\Schedule; 3 use Illuminate\Console\Scheduling\Schedule;
6 use Illuminate\Foundation\Console\Kernel as ConsoleKernel; 4 use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
...@@ -13,10 +11,11 @@ class Kernel extends ConsoleKernel ...@@ -13,10 +11,11 @@ class Kernel extends ConsoleKernel
13 * @var array 11 * @var array
14 */ 12 */
15 protected $commands = [ 13 protected $commands = [
16 - \BookStack\Console\Commands\ClearViews::class, 14 + Commands\ClearViews::class,
17 - \BookStack\Console\Commands\ClearActivity::class, 15 + Commands\ClearActivity::class,
18 - \BookStack\Console\Commands\ClearRevisions::class, 16 + Commands\ClearRevisions::class,
19 - \BookStack\Console\Commands\RegeneratePermissions::class, 17 + Commands\RegeneratePermissions::class,
18 + Commands\RegenerateSearch::class
20 ]; 19 ];
21 20
22 /** 21 /**
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 class Entity extends Ownable 4 class Entity extends Ownable
5 { 5 {
6 6
7 - protected $fieldsToSearch = ['name', 'description']; 7 + protected $textField = 'description';
8 8
9 /** 9 /**
10 * Compares this entity to another given entity. 10 * Compares this entity to another given entity.
...@@ -66,6 +66,15 @@ class Entity extends Ownable ...@@ -66,6 +66,15 @@ class Entity extends Ownable
66 } 66 }
67 67
68 /** 68 /**
69 + * Get the related search terms.
70 + * @return \Illuminate\Database\Eloquent\Relations\MorphMany
71 + */
72 + public function searchTerms()
73 + {
74 + return $this->morphMany(SearchTerm::class, 'entity');
75 + }
76 +
77 + /**
69 * Get this entities restrictions. 78 * Get this entities restrictions.
70 */ 79 */
71 public function permissions() 80 public function permissions()
...@@ -153,10 +162,25 @@ class Entity extends Ownable ...@@ -153,10 +162,25 @@ class Entity extends Ownable
153 } 162 }
154 163
155 /** 164 /**
165 + * Get the body text of this entity.
166 + * @return mixed
167 + */
168 + public function getText()
169 + {
170 + return $this->{$this->textField};
171 + }
172 +
173 + /**
174 + * Return a generalised, common raw query that can be 'unioned' across entities.
175 + * @return string
176 + */
177 + public function entityRawQuery(){return '';}
178 +
179 + /**
156 * Perform a full-text search on this entity. 180 * Perform a full-text search on this entity.
157 - * @param string[] $fieldsToSearch
158 * @param string[] $terms 181 * @param string[] $terms
159 * @param string[] array $wheres 182 * @param string[] array $wheres
183 + * TODO - REMOVE
160 * @return mixed 184 * @return mixed
161 */ 185 */
162 public function fullTextSearchQuery($terms, $wheres = []) 186 public function fullTextSearchQuery($terms, $wheres = [])
...@@ -178,21 +202,21 @@ class Entity extends Ownable ...@@ -178,21 +202,21 @@ class Entity extends Ownable
178 } 202 }
179 203
180 $isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0; 204 $isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0;
181 - 205 + $fieldsToSearch = ['name', $this->textField];
182 206
183 // Perform fulltext search if relevant terms exist. 207 // Perform fulltext search if relevant terms exist.
184 if ($isFuzzy) { 208 if ($isFuzzy) {
185 $termString = implode(' ', $fuzzyTerms); 209 $termString = implode(' ', $fuzzyTerms);
186 - $fields = implode(',', $this->fieldsToSearch); 210 +
187 $search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]); 211 $search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
188 - $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]); 212 + $search = $search->whereRaw('MATCH(' . implode(',', $fieldsToSearch ). ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
189 } 213 }
190 214
191 // Ensure at least one exact term matches if in search 215 // Ensure at least one exact term matches if in search
192 if (count($exactTerms) > 0) { 216 if (count($exactTerms) > 0) {
193 - $search = $search->where(function ($query) use ($exactTerms) { 217 + $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) {
194 foreach ($exactTerms as $exactTerm) { 218 foreach ($exactTerms as $exactTerm) {
195 - foreach ($this->fieldsToSearch as $field) { 219 + foreach ($fieldsToSearch as $field) {
196 $query->orWhere($field, 'like', $exactTerm); 220 $query->orWhere($field, 'like', $exactTerm);
197 } 221 }
198 } 222 }
......
1 <?php namespace BookStack\Http\Controllers; 1 <?php namespace BookStack\Http\Controllers;
2 2
3 use BookStack\Repos\EntityRepo; 3 use BookStack\Repos\EntityRepo;
4 +use BookStack\Services\SearchService;
4 use BookStack\Services\ViewService; 5 use BookStack\Services\ViewService;
5 use Illuminate\Http\Request; 6 use Illuminate\Http\Request;
6 7
...@@ -8,16 +9,19 @@ class SearchController extends Controller ...@@ -8,16 +9,19 @@ class SearchController extends Controller
8 { 9 {
9 protected $entityRepo; 10 protected $entityRepo;
10 protected $viewService; 11 protected $viewService;
12 + protected $searchService;
11 13
12 /** 14 /**
13 * SearchController constructor. 15 * SearchController constructor.
14 * @param EntityRepo $entityRepo 16 * @param EntityRepo $entityRepo
15 * @param ViewService $viewService 17 * @param ViewService $viewService
18 + * @param SearchService $searchService
16 */ 19 */
17 - public function __construct(EntityRepo $entityRepo, ViewService $viewService) 20 + public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
18 { 21 {
19 $this->entityRepo = $entityRepo; 22 $this->entityRepo = $entityRepo;
20 $this->viewService = $viewService; 23 $this->viewService = $viewService;
24 + $this->searchService = $searchService;
21 parent::__construct(); 25 parent::__construct();
22 } 26 }
23 27
...@@ -33,15 +37,13 @@ class SearchController extends Controller ...@@ -33,15 +37,13 @@ class SearchController extends Controller
33 return redirect()->back(); 37 return redirect()->back();
34 } 38 }
35 $searchTerm = $request->get('term'); 39 $searchTerm = $request->get('term');
36 - $paginationAppends = $request->only('term'); 40 +// $paginationAppends = $request->only('term'); TODO - Check pagination
37 - $pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
38 - $books = $this->entityRepo->getBySearch('book', $searchTerm, [], 10, $paginationAppends);
39 - $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 10, $paginationAppends);
40 $this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm])); 41 $this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
42 +
43 + $entities = $this->searchService->searchEntities($searchTerm);
44 +
41 return view('search/all', [ 45 return view('search/all', [
42 - 'pages' => $pages, 46 + 'entities' => $entities,
43 - 'books' => $books,
44 - 'chapters' => $chapters,
45 'searchTerm' => $searchTerm 47 'searchTerm' => $searchTerm
46 ]); 48 ]);
47 } 49 }
......
...@@ -8,8 +8,7 @@ class Page extends Entity ...@@ -8,8 +8,7 @@ class Page extends Entity
8 protected $simpleAttributes = ['name', 'id', 'slug']; 8 protected $simpleAttributes = ['name', 'id', 'slug'];
9 9
10 protected $with = ['book']; 10 protected $with = ['book'];
11 - 11 + protected $textField = 'text';
12 - protected $fieldsToSearch = ['name', 'text'];
13 12
14 /** 13 /**
15 * Converts this page into a simplified array. 14 * Converts this page into a simplified array.
...@@ -96,4 +95,14 @@ class Page extends Entity ...@@ -96,4 +95,14 @@ class Page extends Entity
96 return mb_convert_encoding($text, 'UTF-8'); 95 return mb_convert_encoding($text, 'UTF-8');
97 } 96 }
98 97
98 + /**
99 + * Return a generalised, common raw query that can be 'unioned' across entities.
100 + * @param bool $withContent
101 + * @return string
102 + */
103 + public function entityRawQuery($withContent = false)
104 + { $htmlQuery = $withContent ? 'html' : "'' as html";
105 + return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
106 + }
107 +
99 } 108 }
......
...@@ -8,6 +8,7 @@ use BookStack\Page; ...@@ -8,6 +8,7 @@ use BookStack\Page;
8 use BookStack\PageRevision; 8 use BookStack\PageRevision;
9 use BookStack\Services\AttachmentService; 9 use BookStack\Services\AttachmentService;
10 use BookStack\Services\PermissionService; 10 use BookStack\Services\PermissionService;
11 +use BookStack\Services\SearchService;
11 use BookStack\Services\ViewService; 12 use BookStack\Services\ViewService;
12 use Carbon\Carbon; 13 use Carbon\Carbon;
13 use DOMDocument; 14 use DOMDocument;
...@@ -59,13 +60,18 @@ class EntityRepo ...@@ -59,13 +60,18 @@ class EntityRepo
59 protected $tagRepo; 60 protected $tagRepo;
60 61
61 /** 62 /**
63 + * @var SearchService
64 + */
65 + protected $searchService;
66 +
67 + /**
62 * Acceptable operators to be used in a query 68 * Acceptable operators to be used in a query
63 * @var array 69 * @var array
64 */ 70 */
65 protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!=']; 71 protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
66 72
67 /** 73 /**
68 - * EntityService constructor. 74 + * EntityRepo constructor.
69 * @param Book $book 75 * @param Book $book
70 * @param Chapter $chapter 76 * @param Chapter $chapter
71 * @param Page $page 77 * @param Page $page
...@@ -73,10 +79,12 @@ class EntityRepo ...@@ -73,10 +79,12 @@ class EntityRepo
73 * @param ViewService $viewService 79 * @param ViewService $viewService
74 * @param PermissionService $permissionService 80 * @param PermissionService $permissionService
75 * @param TagRepo $tagRepo 81 * @param TagRepo $tagRepo
82 + * @param SearchService $searchService
76 */ 83 */
77 public function __construct( 84 public function __construct(
78 Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision, 85 Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision,
79 - ViewService $viewService, PermissionService $permissionService, TagRepo $tagRepo 86 + ViewService $viewService, PermissionService $permissionService,
87 + TagRepo $tagRepo, SearchService $searchService
80 ) 88 )
81 { 89 {
82 $this->book = $book; 90 $this->book = $book;
...@@ -91,6 +99,7 @@ class EntityRepo ...@@ -91,6 +99,7 @@ class EntityRepo
91 $this->viewService = $viewService; 99 $this->viewService = $viewService;
92 $this->permissionService = $permissionService; 100 $this->permissionService = $permissionService;
93 $this->tagRepo = $tagRepo; 101 $this->tagRepo = $tagRepo;
102 + $this->searchService = $searchService;
94 } 103 }
95 104
96 /** 105 /**
...@@ -608,6 +617,7 @@ class EntityRepo ...@@ -608,6 +617,7 @@ class EntityRepo
608 $entity->updated_by = user()->id; 617 $entity->updated_by = user()->id;
609 $isChapter ? $book->chapters()->save($entity) : $entity->save(); 618 $isChapter ? $book->chapters()->save($entity) : $entity->save();
610 $this->permissionService->buildJointPermissionsForEntity($entity); 619 $this->permissionService->buildJointPermissionsForEntity($entity);
620 + $this->searchService->indexEntity($entity);
611 return $entity; 621 return $entity;
612 } 622 }
613 623
...@@ -628,6 +638,7 @@ class EntityRepo ...@@ -628,6 +638,7 @@ class EntityRepo
628 $entityModel->updated_by = user()->id; 638 $entityModel->updated_by = user()->id;
629 $entityModel->save(); 639 $entityModel->save();
630 $this->permissionService->buildJointPermissionsForEntity($entityModel); 640 $this->permissionService->buildJointPermissionsForEntity($entityModel);
641 + $this->searchService->indexEntity($entityModel);
631 return $entityModel; 642 return $entityModel;
632 } 643 }
633 644
...@@ -961,6 +972,8 @@ class EntityRepo ...@@ -961,6 +972,8 @@ class EntityRepo
961 $this->savePageRevision($page, $input['summary']); 972 $this->savePageRevision($page, $input['summary']);
962 } 973 }
963 974
975 + $this->searchService->indexEntity($page);
976 +
964 return $page; 977 return $page;
965 } 978 }
966 979
...@@ -1064,6 +1077,7 @@ class EntityRepo ...@@ -1064,6 +1077,7 @@ class EntityRepo
1064 $page->text = strip_tags($page->html); 1077 $page->text = strip_tags($page->html);
1065 $page->updated_by = user()->id; 1078 $page->updated_by = user()->id;
1066 $page->save(); 1079 $page->save();
1080 + $this->searchService->indexEntity($page);
1067 return $page; 1081 return $page;
1068 } 1082 }
1069 1083
...@@ -1156,6 +1170,7 @@ class EntityRepo ...@@ -1156,6 +1170,7 @@ class EntityRepo
1156 $book->views()->delete(); 1170 $book->views()->delete();
1157 $book->permissions()->delete(); 1171 $book->permissions()->delete();
1158 $this->permissionService->deleteJointPermissionsForEntity($book); 1172 $this->permissionService->deleteJointPermissionsForEntity($book);
1173 + $this->searchService->deleteEntityTerms($book);
1159 $book->delete(); 1174 $book->delete();
1160 } 1175 }
1161 1176
...@@ -1175,6 +1190,7 @@ class EntityRepo ...@@ -1175,6 +1190,7 @@ class EntityRepo
1175 $chapter->views()->delete(); 1190 $chapter->views()->delete();
1176 $chapter->permissions()->delete(); 1191 $chapter->permissions()->delete();
1177 $this->permissionService->deleteJointPermissionsForEntity($chapter); 1192 $this->permissionService->deleteJointPermissionsForEntity($chapter);
1193 + $this->searchService->deleteEntityTerms($chapter);
1178 $chapter->delete(); 1194 $chapter->delete();
1179 } 1195 }
1180 1196
...@@ -1190,6 +1206,7 @@ class EntityRepo ...@@ -1190,6 +1206,7 @@ class EntityRepo
1190 $page->revisions()->delete(); 1206 $page->revisions()->delete();
1191 $page->permissions()->delete(); 1207 $page->permissions()->delete();
1192 $this->permissionService->deleteJointPermissionsForEntity($page); 1208 $this->permissionService->deleteJointPermissionsForEntity($page);
1209 + $this->searchService->deleteEntityTerms($page);
1193 1210
1194 // Delete Attached Files 1211 // Delete Attached Files
1195 $attachmentService = app(AttachmentService::class); 1212 $attachmentService = app(AttachmentService::class);
......
1 +<?php namespace BookStack;
2 +
3 +use Illuminate\Database\Eloquent\Model;
4 +
5 +class SearchTerm extends Model
6 +{
7 +
8 + protected $fillable = ['term', 'entity_id', 'entity_type', 'score'];
9 + public $timestamps = false;
10 +
11 + /**
12 + * Get the entity that this term belongs to
13 + * @return \Illuminate\Database\Eloquent\Relations\MorphTo
14 + */
15 + public function entity()
16 + {
17 + return $this->morphTo('entity');
18 + }
19 +
20 +}
...@@ -479,8 +479,7 @@ class PermissionService ...@@ -479,8 +479,7 @@ class PermissionService
479 * @return \Illuminate\Database\Query\Builder 479 * @return \Illuminate\Database\Query\Builder
480 */ 480 */
481 public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) { 481 public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
482 - $pageContentSelect = $fetchPageContent ? 'html' : "''"; 482 + $pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
483 - $pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, {$pageContentSelect} as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
484 $query->where('draft', '=', 0); 483 $query->where('draft', '=', 0);
485 if (!$filterDrafts) { 484 if (!$filterDrafts) {
486 $query->orWhere(function($query) { 485 $query->orWhere(function($query) {
...@@ -488,7 +487,7 @@ class PermissionService ...@@ -488,7 +487,7 @@ class PermissionService
488 }); 487 });
489 } 488 }
490 }); 489 });
491 - $chapterSelect = $this->db->table('chapters')->selectRaw("'BookStack\\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft")->where('book_id', '=', $book_id); 490 + $chapterSelect = $this->db->table('chapters')->selectRaw($this->chapter->entityRawQuery())->where('book_id', '=', $book_id);
492 $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U")) 491 $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
493 ->mergeBindings($pageSelect)->mergeBindings($chapterSelect); 492 ->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
494 493
...@@ -540,7 +539,7 @@ class PermissionService ...@@ -540,7 +539,7 @@ class PermissionService
540 } 539 }
541 540
542 /** 541 /**
543 - * Filter items that have entities set a a polymorphic relation. 542 + * Filter items that have entities set as a polymorphic relation.
544 * @param $query 543 * @param $query
545 * @param string $tableName 544 * @param string $tableName
546 * @param string $entityIdColumn 545 * @param string $entityIdColumn
......
1 +<?php namespace BookStack\Services;
2 +
3 +use BookStack\Book;
4 +use BookStack\Chapter;
5 +use BookStack\Entity;
6 +use BookStack\Page;
7 +use BookStack\SearchTerm;
8 +use Illuminate\Database\Connection;
9 +use Illuminate\Database\Query\JoinClause;
10 +
11 +class SearchService
12 +{
13 +
14 + protected $searchTerm;
15 + protected $book;
16 + protected $chapter;
17 + protected $page;
18 + protected $db;
19 +
20 + /**
21 + * SearchService constructor.
22 + * @param SearchTerm $searchTerm
23 + * @param Book $book
24 + * @param Chapter $chapter
25 + * @param Page $page
26 + * @param Connection $db
27 + */
28 + public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db)
29 + {
30 + $this->searchTerm = $searchTerm;
31 + $this->book = $book;
32 + $this->chapter = $chapter;
33 + $this->page = $page;
34 + $this->db = $db;
35 + }
36 +
37 + public function searchEntities($searchString, $entityType = 'all')
38 + {
39 + // TODO - Add Tag Searches
40 + // TODO - Add advanced custom column searches
41 + // TODO - Add exact match searches ("")
42 +
43 + $termArray = explode(' ', $searchString);
44 +
45 + $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
46 + $subQuery->where(function($query) use ($termArray) {
47 + foreach ($termArray as $inputTerm) {
48 + $query->orWhere('term', 'like', $inputTerm .'%');
49 + }
50 + });
51 +
52 + $subQuery = $subQuery->groupBy('entity_type', 'entity_id');
53 + $pageSelect = $this->db->table('pages as e')->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) {
54 + $join->on('e.id', '=', 's.entity_id');
55 + })->selectRaw('e.*, s.score')->orderBy('score', 'desc');
56 + $pageSelect->mergeBindings($subQuery);
57 + dd($pageSelect->toSql());
58 + // TODO - Continue from here
59 + }
60 +
61 + /**
62 + * Index the given entity.
63 + * @param Entity $entity
64 + */
65 + public function indexEntity(Entity $entity)
66 + {
67 + $this->deleteEntityTerms($entity);
68 + $nameTerms = $this->generateTermArrayFromText($entity->name, 5);
69 + $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
70 + $terms = array_merge($nameTerms, $bodyTerms);
71 + $entity->searchTerms()->createMany($terms);
72 + }
73 +
74 + /**
75 + * Index multiple Entities at once
76 + * @param Entity[] $entities
77 + */
78 + protected function indexEntities($entities) {
79 + $terms = [];
80 + foreach ($entities as $entity) {
81 + $nameTerms = $this->generateTermArrayFromText($entity->name, 5);
82 + $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
83 + foreach (array_merge($nameTerms, $bodyTerms) as $term) {
84 + $term['entity_id'] = $entity->id;
85 + $term['entity_type'] = $entity->getMorphClass();
86 + $terms[] = $term;
87 + }
88 + }
89 + $this->searchTerm->insert($terms);
90 + }
91 +
92 + /**
93 + * Delete and re-index the terms for all entities in the system.
94 + */
95 + public function indexAllEntities()
96 + {
97 + $this->searchTerm->truncate();
98 +
99 + // Chunk through all books
100 + $this->book->chunk(500, function ($books) {
101 + $this->indexEntities($books);
102 + });
103 +
104 + // Chunk through all chapters
105 + $this->chapter->chunk(500, function ($chapters) {
106 + $this->indexEntities($chapters);
107 + });
108 +
109 + // Chunk through all pages
110 + $this->page->chunk(500, function ($pages) {
111 + $this->indexEntities($pages);
112 + });
113 + }
114 +
115 + /**
116 + * Delete related Entity search terms.
117 + * @param Entity $entity
118 + */
119 + public function deleteEntityTerms(Entity $entity)
120 + {
121 + $entity->searchTerms()->delete();
122 + }
123 +
124 + /**
125 + * Create a scored term array from the given text.
126 + * @param $text
127 + * @param float|int $scoreAdjustment
128 + * @return array
129 + */
130 + protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
131 + {
132 + $tokenMap = []; // {TextToken => OccurrenceCount}
133 + $splitText = explode(' ', $text);
134 + foreach ($splitText as $token) {
135 + if ($token === '') continue;
136 + if (!isset($tokenMap[$token])) $tokenMap[$token] = 0;
137 + $tokenMap[$token]++;
138 + }
139 +
140 + $terms = [];
141 + foreach ($tokenMap as $token => $count) {
142 + $terms[] = [
143 + 'term' => $token,
144 + 'score' => $count * $scoreAdjustment
145 + ];
146 + }
147 + return $terms;
148 + }
149 +
150 +}
...\ No newline at end of file ...\ No newline at end of file
1 +<?php
2 +
3 +use Illuminate\Support\Facades\Schema;
4 +use Illuminate\Database\Schema\Blueprint;
5 +use Illuminate\Database\Migrations\Migration;
6 +
7 +class CreateSearchIndexTable extends Migration
8 +{
9 + /**
10 + * Run the migrations.
11 + *
12 + * @return void
13 + */
14 + public function up()
15 + {
16 + Schema::create('search_terms', function (Blueprint $table) {
17 + $table->increments('id');
18 + $table->string('term', 200);
19 + $table->string('entity_type', 100);
20 + $table->integer('entity_id');
21 + $table->integer('score');
22 +
23 + $table->index('term');
24 + $table->index('entity_type');
25 + $table->index(['entity_type', 'entity_id']);
26 + $table->index('score');
27 + });
28 +
29 + // TODO - Drop old fulltext indexes
30 +
31 + app(\BookStack\Services\SearchService::class)->indexAllEntities();
32 + }
33 +
34 + /**
35 + * Reverse the migrations.
36 + *
37 + * @return void
38 + */
39 + public function down()
40 + {
41 + Schema::dropIfExists('search_terms');
42 + }
43 +}
...@@ -20,36 +20,30 @@ ...@@ -20,36 +20,30 @@
20 <h1>{{ trans('entities.search_results') }}</h1> 20 <h1>{{ trans('entities.search_results') }}</h1>
21 21
22 <p> 22 <p>
23 - @if(count($pages) > 0) 23 + {{--TODO - Remove these pages--}}
24 - <a href="{{ baseUrl("/search/pages?term={$searchTerm}") }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.search_view_pages') }}</a> 24 + Remove these links (Commented out)
25 - @endif 25 + {{--@if(count($pages) > 0)--}}
26 - 26 + {{--<a href="{{ baseUrl("/search/pages?term={$searchTerm}") }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.search_view_pages') }}</a>--}}
27 - @if(count($chapters) > 0) 27 + {{--@endif--}}
28 - &nbsp; &nbsp;&nbsp; 28 +
29 - <a href="{{ baseUrl("/search/chapters?term={$searchTerm}") }}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>{{ trans('entities.search_view_chapters') }}</a> 29 + {{--@if(count($chapters) > 0)--}}
30 - @endif 30 + {{--&nbsp; &nbsp;&nbsp;--}}
31 - 31 + {{--<a href="{{ baseUrl("/search/chapters?term={$searchTerm}") }}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>{{ trans('entities.search_view_chapters') }}</a>--}}
32 - @if(count($books) > 0) 32 + {{--@endif--}}
33 - &nbsp; &nbsp;&nbsp; 33 +
34 - <a href="{{ baseUrl("/search/books?term={$searchTerm}") }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.search_view_books') }}</a> 34 + {{--@if(count($books) > 0)--}}
35 - @endif 35 + {{--&nbsp; &nbsp;&nbsp;--}}
36 + {{--<a href="{{ baseUrl("/search/books?term={$searchTerm}") }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.search_view_books') }}</a>--}}
37 + {{--@endif--}}
36 </p> 38 </p>
37 39
38 <div class="row"> 40 <div class="row">
39 <div class="col-md-6"> 41 <div class="col-md-6">
40 <h3><a href="{{ baseUrl("/search/pages?term={$searchTerm}") }}" class="no-color">{{ trans('entities.pages') }}</a></h3> 42 <h3><a href="{{ baseUrl("/search/pages?term={$searchTerm}") }}" class="no-color">{{ trans('entities.pages') }}</a></h3>
41 - @include('partials/entity-list', ['entities' => $pages, 'style' => 'detailed']) 43 + @include('partials/entity-list', ['entities' => $entities, 'style' => 'detailed'])
42 </div> 44 </div>
43 <div class="col-md-5 col-md-offset-1"> 45 <div class="col-md-5 col-md-offset-1">
44 - @if(count($books) > 0) 46 + Sidebar filter controls
45 - <h3><a href="{{ baseUrl("/search/books?term={$searchTerm}") }}" class="no-color">{{ trans('entities.books') }}</a></h3>
46 - @include('partials/entity-list', ['entities' => $books])
47 - @endif
48 -
49 - @if(count($chapters) > 0)
50 - <h3><a href="{{ baseUrl("/search/chapters?term={$searchTerm}") }}" class="no-color">{{ trans('entities.chapters') }}</a></h3>
51 - @include('partials/entity-list', ['entities' => $chapters])
52 - @endif
53 </div> 47 </div>
54 </div> 48 </div>
55 49
......