0
0
Fork 0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-05-03 07:49:57 +00:00

Improved relation loading during search

Relations now loaded during back-end query phase instead of being lazy
loaded one-by-one within views.

Reduced queries in testing from ~60 to ~20.

Need to check other areas list-item.php's "showPath" option is used to
ensure relations are properly loaded for those listings.
This commit is contained in:
Dan Brown 2021-11-08 15:24:49 +00:00
parent b3e1c7da73
commit bc472ca2d7
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
2 changed files with 37 additions and 17 deletions
app/Entities/Tools
resources/views/entities

View file

@ -5,11 +5,13 @@ namespace BookStack\Entities\Tools;
use BookStack\Auth\Permissions\PermissionService; 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\BookChild;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\SearchTerm; use BookStack\Entities\Models\SearchTerm;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -75,9 +77,10 @@ class SearchRunner
continue; continue;
} }
$searchQuery = $this->buildQuery($searchOpts, $entityType, $action); $entityModelInstance = $this->entityProvider->get($entityType);
$searchQuery = $this->buildQuery($searchOpts, $entityModelInstance, $action);
$entityTotal = $searchQuery->count(); $entityTotal = $searchQuery->count();
$searchResults = $this->getPageOfDataFromQuery($searchQuery, $page, $count); $searchResults = $this->getPageOfDataFromQuery($searchQuery, $entityModelInstance, $page, $count);
if ($entityTotal > ($page * $count)) { if ($entityTotal > ($page * $count)) {
$hasMore = true; $hasMore = true;
@ -109,7 +112,9 @@ class SearchRunner
if (!in_array($entityType, $entityTypes)) { if (!in_array($entityType, $entityTypes)) {
continue; continue;
} }
$search = $this->buildQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
$entityModelInstance = $this->entityProvider->get($entityType);
$search = $this->buildQuery($opts, $entityModelInstance)->where('book_id', '=', $bookId)->take(20)->get();
$results = $results->merge($search); $results = $results->merge($search);
} }
@ -122,7 +127,8 @@ 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->buildQuery($opts, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get(); $entityModelInstance = $this->entityProvider->get('page');
$pages = $this->buildQuery($opts, $entityModelInstance)->where('chapter_id', '=', $chapterId)->take(20)->get();
return $pages->sortByDesc('score'); return $pages->sortByDesc('score');
} }
@ -130,9 +136,24 @@ class SearchRunner
/** /**
* Get a page of result data from the given query based on the provided page parameters. * Get a page of result data from the given query based on the provided page parameters.
*/ */
protected function getPageOfDataFromQuery(EloquentBuilder $query, int $page = 1, int $count = 20): EloquentCollection protected function getPageOfDataFromQuery(EloquentBuilder $query, Entity $entityModelInstance, int $page = 1, int $count = 20): EloquentCollection
{ {
$relations = ['tags'];
if ($entityModelInstance instanceof BookChild) {
$relations['book'] = function(BelongsTo $query) {
$query->visible();
};
}
if ($entityModelInstance instanceof Page) {
$relations['chapter'] = function(BelongsTo $query) {
$query->visible();
};
}
return $query->clone() return $query->clone()
->with(array_filter($relations))
->skip(($page - 1) * $count) ->skip(($page - 1) * $count)
->take($count) ->take($count)
->get(); ->get();
@ -141,25 +162,24 @@ class SearchRunner
/** /**
* Create a search query for an entity. * Create a search query for an entity.
*/ */
protected function buildQuery(SearchOptions $searchOpts, string $entityType = 'page', string $action = 'view'): EloquentBuilder protected function buildQuery(SearchOptions $searchOpts, Entity $entityModelInstance, string $action = 'view'): EloquentBuilder
{ {
$entity = $this->entityProvider->get($entityType); $entityQuery = $entityModelInstance->newQuery();
$entityQuery = $entity->newQuery();
if ($entity instanceof Page) { if ($entityModelInstance instanceof Page) {
$entityQuery->select($entity::$listAttributes); $entityQuery->select($entityModelInstance::$listAttributes);
} else { } else {
$entityQuery->select(['*']); $entityQuery->select(['*']);
} }
// Handle normal search terms // Handle normal search terms
$this->applyTermSearch($entityQuery, $searchOpts, $entity); $this->applyTermSearch($entityQuery, $searchOpts, $entityModelInstance);
// Handle exact term matching // Handle exact term matching
foreach ($searchOpts->exacts as $inputTerm) { foreach ($searchOpts->exacts as $inputTerm) {
$entityQuery->where(function (EloquentBuilder $query) use ($inputTerm, $entity) { $entityQuery->where(function (EloquentBuilder $query) use ($inputTerm, $entityModelInstance) {
$query->where('name', 'like', '%' . $inputTerm . '%') $query->where('name', 'like', '%' . $inputTerm . '%')
->orWhere($entity->textField, 'like', '%' . $inputTerm . '%'); ->orWhere($entityModelInstance->textField, 'like', '%' . $inputTerm . '%');
}); });
} }
@ -172,11 +192,11 @@ class SearchRunner
foreach ($searchOpts->filters as $filterTerm => $filterValue) { foreach ($searchOpts->filters as $filterTerm => $filterValue) {
$functionName = Str::camel('filter_' . $filterTerm); $functionName = Str::camel('filter_' . $filterTerm);
if (method_exists($this, $functionName)) { if (method_exists($this, $functionName)) {
$this->$functionName($entityQuery, $entity, $filterValue); $this->$functionName($entityQuery, $entityModelInstance, $filterValue);
} }
} }
return $this->permissionService->enforceEntityRestrictions($entity, $entityQuery, $action); return $this->permissionService->enforceEntityRestrictions($entityModelInstance, $entityQuery, $action);
} }
/** /**

View file

@ -3,9 +3,9 @@
<div class="entity-item-snippet"> <div class="entity-item-snippet">
@if($showPath ?? false) @if($showPath ?? false)
@if($entity->book_id) @if($entity->relationLoaded('book') && $entity->book)
<span class="text-book">{{ $entity->book->getShortName(42) }}</span> <span class="text-book">{{ $entity->book->getShortName(42) }}</span>
@if($entity->chapter_id) @if($entity->relationLoaded('chapter') && $entity->chapter)
<span class="text-muted entity-list-item-path-sep">@icon('chevron-right')</span> <span class="text-chapter">{{ $entity->chapter->getShortName(42) }}</span> <span class="text-muted entity-list-item-path-sep">@icon('chevron-right')</span> <span class="text-chapter">{{ $entity->chapter->getShortName(42) }}</span>
@endif @endif
@endif @endif