1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-04-19 18:22:16 +03:00

Refactored search runner a little to be neater

This commit is contained in:
Dan Brown 2021-11-08 11:04:27 +00:00
parent f2b1d2e1e7
commit e1b8fe45b0
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
2 changed files with 39 additions and 56 deletions

View File

@ -6,11 +6,12 @@ use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\User; use BookStack\Auth\User;
use BookStack\Entities\EntityProvider; use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class SearchRunner class SearchRunner
@ -20,11 +21,6 @@ class SearchRunner
*/ */
protected $entityProvider; protected $entityProvider;
/**
* @var Connection
*/
protected $db;
/** /**
* @var PermissionService * @var PermissionService
*/ */
@ -37,10 +33,9 @@ class SearchRunner
*/ */
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!=']; protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
public function __construct(EntityProvider $entityProvider, Connection $db, PermissionService $permissionService) public function __construct(EntityProvider $entityProvider, PermissionService $permissionService)
{ {
$this->entityProvider = $entityProvider; $this->entityProvider = $entityProvider;
$this->db = $db;
$this->permissionService = $permissionService; $this->permissionService = $permissionService;
} }
@ -69,16 +64,16 @@ class SearchRunner
continue; continue;
} }
$search = $this->searchEntityTable($searchOpts, $entityType, $page, $count, $action); $searchQuery = $this->buildQuery($searchOpts, $entityType, $action);
/** @var int $entityTotal */ $entityTotal = $searchQuery->count();
$entityTotal = $this->searchEntityTable($searchOpts, $entityType, $page, $count, $action, true); $searchResults = $this->getPageOfDataFromQuery($searchQuery, $page, $count);
if ($entityTotal > ($page * $count)) { if ($entityTotal > ($page * $count)) {
$hasMore = true; $hasMore = true;
} }
$total += $entityTotal; $total += $entityTotal;
$results = $results->merge($search); $results = $results->merge($searchResults);
} }
return [ return [
@ -103,7 +98,7 @@ class SearchRunner
if (!in_array($entityType, $entityTypes)) { if (!in_array($entityType, $entityTypes)) {
continue; continue;
} }
$search = $this->buildEntitySearchQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get(); $search = $this->buildQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
$results = $results->merge($search); $results = $results->merge($search);
} }
@ -116,49 +111,41 @@ class SearchRunner
public function searchChapter(int $chapterId, string $searchString): Collection public function searchChapter(int $chapterId, string $searchString): Collection
{ {
$opts = SearchOptions::fromString($searchString); $opts = SearchOptions::fromString($searchString);
$pages = $this->buildEntitySearchQuery($opts, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get(); $pages = $this->buildQuery($opts, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
return $pages->sortByDesc('score'); return $pages->sortByDesc('score');
} }
/** /**
* Search across a particular entity type. * Get a page of result data from the given query based on the provided page parameters.
* Setting getCount = true will return the total
* matching instead of the items themselves.
*
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
*/ */
protected function searchEntityTable(SearchOptions $searchOpts, string $entityType = 'page', int $page = 1, int $count = 20, string $action = 'view', bool $getCount = false) protected function getPageOfDataFromQuery(EloquentBuilder $query, int $page = 1, int $count = 20): EloquentCollection
{ {
$query = $this->buildEntitySearchQuery($searchOpts, $entityType, $action); return $query->clone()
if ($getCount) { ->skip(($page - 1) * $count)
return $query->count(); ->take($count)
} ->get();
$query = $query->skip(($page - 1) * $count)->take($count);
return $query->get();
} }
/** /**
* Create a search query for an entity. * Create a search query for an entity.
*/ */
protected function buildEntitySearchQuery(SearchOptions $searchOpts, string $entityType = 'page', string $action = 'view'): EloquentBuilder protected function buildQuery(SearchOptions $searchOpts, string $entityType = 'page', string $action = 'view'): EloquentBuilder
{ {
$entity = $this->entityProvider->get($entityType); $entity = $this->entityProvider->get($entityType);
$entitySelect = $entity->newQuery(); $entitySelect = $entity->newQuery();
// Handle normal search terms // Handle normal search terms
if (count($searchOpts->searches) > 0) { if (count($searchOpts->searches) > 0) {
$rawScoreSum = $this->db->raw('SUM(score) as score'); $rawScoreSum = DB::raw('SUM(score) as score');
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', $rawScoreSum); $subQuery = DB::table('search_terms')->select('entity_id', 'entity_type', $rawScoreSum);
$subQuery->where('entity_type', '=', $entity->getMorphClass()); $subQuery->where('entity_type', '=', $entity->getMorphClass());
$subQuery->where(function (Builder $query) use ($searchOpts) { $subQuery->where(function (Builder $query) use ($searchOpts) {
foreach ($searchOpts->searches as $inputTerm) { foreach ($searchOpts->searches as $inputTerm) {
$query->orWhere('term', 'like', $inputTerm . '%'); $query->orWhere('term', 'like', $inputTerm . '%');
} }
})->groupBy('entity_type', 'entity_id'); })->groupBy('entity_type', 'entity_id');
$entitySelect->join($this->db->raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) { $entitySelect->join(DB::raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) {
$join->on('id', '=', 'entity_id'); $join->on('id', '=', 'entity_id');
})->addSelect($entity->getTable() . '.*') })->addSelect($entity->getTable() . '.*')
->selectRaw('s.score') ->selectRaw('s.score')
@ -238,44 +225,36 @@ class SearchRunner
/** /**
* Custom entity search filters. * Custom entity search filters.
*/ */
protected function filterUpdatedAfter(EloquentBuilder $query, Entity $model, $input) protected function filterUpdatedAfter(EloquentBuilder $query, Entity $model, $input): void
{ {
try { try {
$date = date_create($input); $date = date_create($input);
} catch (\Exception $e) { $query->where('updated_at', '>=', $date);
return; } catch (\Exception $e) {}
}
$query->where('updated_at', '>=', $date);
} }
protected function filterUpdatedBefore(EloquentBuilder $query, Entity $model, $input) protected function filterUpdatedBefore(EloquentBuilder $query, Entity $model, $input): void
{ {
try { try {
$date = date_create($input); $date = date_create($input);
} catch (\Exception $e) { $query->where('updated_at', '<', $date);
return; } catch (\Exception $e) {}
}
$query->where('updated_at', '<', $date);
} }
protected function filterCreatedAfter(EloquentBuilder $query, Entity $model, $input) protected function filterCreatedAfter(EloquentBuilder $query, Entity $model, $input): void
{ {
try { try {
$date = date_create($input); $date = date_create($input);
} catch (\Exception $e) { $query->where('created_at', '>=', $date);
return; } catch (\Exception $e) {}
}
$query->where('created_at', '>=', $date);
} }
protected function filterCreatedBefore(EloquentBuilder $query, Entity $model, $input) protected function filterCreatedBefore(EloquentBuilder $query, Entity $model, $input)
{ {
try { try {
$date = date_create($input); $date = date_create($input);
} catch (\Exception $e) { $query->where('created_at', '<', $date);
return; } catch (\Exception $e) {}
}
$query->where('created_at', '<', $date);
} }
protected function filterCreatedBy(EloquentBuilder $query, Entity $model, $input) protected function filterCreatedBy(EloquentBuilder $query, Entity $model, $input)
@ -352,9 +331,9 @@ class SearchRunner
*/ */
protected function sortByLastCommented(EloquentBuilder $query, Entity $model) protected function sortByLastCommented(EloquentBuilder $query, Entity $model)
{ {
$commentsTable = $this->db->getTablePrefix() . 'comments'; $commentsTable = DB::getTablePrefix() . 'comments';
$morphClass = str_replace('\\', '\\\\', $model->getMorphClass()); $morphClass = str_replace('\\', '\\\\', $model->getMorphClass());
$commentQuery = $this->db->raw('(SELECT c1.entity_id, c1.entity_type, c1.created_at as last_commented FROM ' . $commentsTable . ' c1 LEFT JOIN ' . $commentsTable . ' c2 ON (c1.entity_id = c2.entity_id AND c1.entity_type = c2.entity_type AND c1.created_at < c2.created_at) WHERE c1.entity_type = \'' . $morphClass . '\' AND c2.created_at IS NULL) as comments'); $commentQuery = DB::raw('(SELECT c1.entity_id, c1.entity_type, c1.created_at as last_commented FROM ' . $commentsTable . ' c1 LEFT JOIN ' . $commentsTable . ' c2 ON (c1.entity_id = c2.entity_id AND c1.entity_type = c2.entity_type AND c1.created_at < c2.created_at) WHERE c1.entity_type = \'' . $morphClass . '\' AND c2.created_at IS NULL) as comments');
$query->join($commentQuery, $model->getTable() . '.id', '=', 'comments.entity_id')->orderBy('last_commented', 'desc'); $query->join($commentQuery, $model->getTable() . '.id', '=', 'comments.entity_id')->orderBy('last_commented', 'desc');
} }

View File

@ -5,6 +5,7 @@ namespace Database\Seeders;
use BookStack\Auth\Permissions\PermissionService; use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\Role; use BookStack\Auth\Role;
use BookStack\Auth\User; use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\SearchIndex; use BookStack\Entities\Tools\SearchIndex;
@ -25,12 +26,15 @@ class LargeContentSeeder extends Seeder
$editorRole = Role::getRole('editor'); $editorRole = Role::getRole('editor');
$editorUser->attachRole($editorRole); $editorUser->attachRole($editorRole);
$largeBook = \BookStack\Entities\Models\Book::factory()->create(['name' => 'Large book' . Str::random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]); /** @var Book $largeBook */
$largeBook = Book::factory()->create(['name' => 'Large book' . Str::random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$pages = Page::factory()->count(200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]); $pages = Page::factory()->count(200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$chapters = Chapter::factory()->count(50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]); $chapters = Chapter::factory()->count(50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$largeBook->pages()->saveMany($pages); $largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters); $largeBook->chapters()->saveMany($chapters);
app(PermissionService::class)->buildJointPermissions();
app(SearchIndex::class)->indexAllEntities(); app()->make(PermissionService::class)->buildJointPermissions();
app()->make(SearchIndex::class)->indexAllEntities();
} }
} }