1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-11-04 13:31:45 +03:00

Search: Added pagination, updated other search uses

Also updated hydrator to be created via injection.
This commit is contained in:
Dan Brown
2025-10-28 20:37:41 +00:00
parent f0303de2e5
commit 3fd25bd03e
5 changed files with 31 additions and 59 deletions

View File

@@ -12,31 +12,22 @@ use Illuminate\Database\Eloquent\Collection;
class EntityHydrator
{
/**
* @var EntityTable[] $entities
*/
protected array $entities;
protected bool $loadTags = false;
protected bool $loadParents = false;
public function __construct(array $entities, bool $loadTags = false, bool $loadParents = false)
{
$this->entities = $entities;
$this->loadTags = $loadTags;
$this->loadParents = $loadParents;
public function __construct(
protected EntityQueries $entityQueries,
) {
}
/**
* Hydrate the entities of this hydrator to return a list of entities represented
* in their original intended models.
* @param EntityTable[] $entities
* @return Entity[]
*/
public function hydrate(): array
public function hydrate(array $entities, bool $loadTags = false, bool $loadParents = false): array
{
$hydrated = [];
foreach ($this->entities as $entity) {
foreach ($entities as $entity) {
$data = $entity->getRawOriginal();
$instance = Entity::instanceFromType($entity->type);
@@ -49,11 +40,11 @@ class EntityHydrator
$hydrated[] = $instance;
}
if ($this->loadTags) {
if ($loadTags) {
$this->loadTagsIntoModels($hydrated);
}
if ($this->loadParents) {
if ($loadParents) {
$this->loadParentsIntoModels($hydrated);
}
@@ -115,10 +106,7 @@ class EntityHydrator
}
}
// TODO - Inject in?
$queries = app()->make(EntityQueries::class);
$parentQuery = $queries->visibleForList();
$parentQuery = $this->entityQueries->visibleForList();
$filtered = count($parentsByType['book']) > 0 || count($parentsByType['chapter']) > 0;
$parentQuery = $parentQuery->where(function ($query) use ($parentsByType) {
foreach ($parentsByType as $type => $ids) {
@@ -132,7 +120,7 @@ class EntityHydrator
});
$parentModels = $filtered ? $parentQuery->get()->all() : [];
$parents = (new EntityHydrator($parentModels))->hydrate();
$parents = $this->hydrate($parentModels);
$parentMap = [];
foreach ($parents as $parent) {
$parentMap[$parent->type . ':' . $parent->id] = $parent;

View File

@@ -1,10 +1,13 @@
<?php
declare(strict_types=1);
namespace BookStack\Search;
use BookStack\Api\ApiEntityListFormatter;
use BookStack\Entities\Models\Entity;
use BookStack\Http\ApiController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SearchApiController extends ApiController
@@ -31,11 +34,9 @@ class SearchApiController extends ApiController
* between: bookshelf, book, chapter & page.
*
* The paging parameters and response format emulates a standard listing endpoint
* but standard sorting and filtering cannot be done on this endpoint. If a count value
* is provided this will only be taken as a suggestion. The results in the response
* may currently be up to 4x this value.
* but standard sorting and filtering cannot be done on this endpoint.
*/
public function all(Request $request)
public function all(Request $request): JsonResponse
{
$this->validate($request, $this->rules['all']);

View File

@@ -7,6 +7,7 @@ use BookStack\Entities\Queries\QueryPopular;
use BookStack\Entities\Tools\SiblingFetcher;
use BookStack\Http\Controller;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
class SearchController extends Controller
{
@@ -23,20 +24,21 @@ class SearchController extends Controller
{
$searchOpts = SearchOptions::fromRequest($request);
$fullSearchString = $searchOpts->toString();
$this->setPageTitle(trans('entities.search_for_term', ['term' => $fullSearchString]));
$page = intval($request->get('page', '0')) ?: 1;
$nextPageLink = url('/search?term=' . urlencode($fullSearchString) . '&page=' . ($page + 1));
$results = $this->searchRunner->searchEntities($searchOpts, 'all', $page, 20);
$formatter->format($results['results']->all(), $searchOpts);
$paginator = new LengthAwarePaginator($results['results'], $results['total'], 20, $page);
$paginator->setPath('/search');
$paginator->appends($request->except('page'));
$this->setPageTitle(trans('entities.search_for_term', ['term' => $fullSearchString]));
return view('search.all', [
'entities' => $results['results'],
'totalResults' => $results['total'],
'paginator' => $paginator,
'searchTerm' => $fullSearchString,
'hasNextPage' => $results['has_more'],
'nextPageLink' => $nextPageLink,
'options' => $searchOpts,
]);
}
@@ -128,7 +130,7 @@ class SearchController extends Controller
}
/**
* Search siblings items in the system.
* Search sibling items in the system.
*/
public function searchSiblings(Request $request, SiblingFetcher $siblingFetcher)
{

View File

@@ -11,7 +11,6 @@ use BookStack\Search\Options\TagSearchOption;
use BookStack\Users\Models\User;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
@@ -30,17 +29,15 @@ class SearchRunner
protected EntityProvider $entityProvider,
protected PermissionApplicator $permissions,
protected EntityQueries $entityQueries,
protected EntityHydrator $entityHydrator,
) {
$this->termAdjustmentCache = new WeakMap();
}
/**
* Search all entities in the system.
* The provided count is for each entity to search,
* Total returned could be larger and not guaranteed.
* // TODO - Update this comment
*
* @return array{total: int, count: int, has_more: bool, results: Collection<Entity>}
* @return array{total: int, results: Collection<Entity>}
*/
public function searchEntities(SearchOptions $searchOpts, string $entityType = 'all', int $page = 1, int $count = 20): array
{
@@ -58,14 +55,9 @@ class SearchRunner
$total = $searchQuery->count();
$results = $this->getPageOfDataFromQuery($searchQuery, $page, $count);
// TODO - Pagination?
$hasMore = ($total > ($page * $count));
return [
'total' => $total,
'count' => count($results),
'has_more' => $hasMore,
'results' => $results->sortByDesc('score')->values(),
'results' => $results->values(),
];
}
@@ -79,15 +71,8 @@ class SearchRunner
$filterMap = $opts->filters->toValueMap();
$entityTypesToSearch = isset($filterMap['type']) ? explode('|', $filterMap['type']) : $entityTypes;
$results = collect();
foreach ($entityTypesToSearch as $entityType) {
if (!in_array($entityType, $entityTypes)) {
continue;
}
$search = $this->buildQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
$results = $results->merge($search);
}
$filteredTypes = array_intersect($entityTypesToSearch, $entityTypes);
$results = $this->buildQuery($opts, $filteredTypes)->where('book_id', '=', $bookId)->take(20)->get();
return $results->sortByDesc('score')->take(20);
}
@@ -98,7 +83,7 @@ class SearchRunner
public function searchChapter(int $chapterId, string $searchString): Collection
{
$opts = SearchOptions::fromString($searchString);
$pages = $this->buildQuery($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');
}
@@ -113,7 +98,7 @@ class SearchRunner
->take($count)
->get();
$hydrated = (new EntityHydrator($entities->all(), true, true))->hydrate();
$hydrated = $this->entityHydrator->hydrate($entities->all(), true, true);
return collect($hydrated);
}

View File

@@ -87,11 +87,7 @@
@include('entities.list', ['entities' => $entities, 'showPath' => true, 'showTags' => true])
</div>
@if($hasNextPage)
<div class="text-right mt-m">
<a href="{{ $nextPageLink }}" class="button outline">{{ trans('entities.search_more') }}</a>
</div>
@endif
{{ $paginator->render() }}
</div>
</div>
</div>