]> BookStack Code Mirror - bookstack/commitdiff
Search: Added pagination, updated other search uses
authorDan Brown <redacted>
Tue, 28 Oct 2025 20:37:41 +0000 (20:37 +0000)
committerDan Brown <redacted>
Tue, 28 Oct 2025 20:37:41 +0000 (20:37 +0000)
Also updated hydrator to be created via injection.

app/Entities/Tools/EntityHydrator.php
app/Search/SearchApiController.php
app/Search/SearchController.php
app/Search/SearchRunner.php
resources/views/search/all.blade.php

index cb62674237c2f9ade99a0c15bff592f22f3b921c..7cabc7ecbec8c62a16e1aeb438485198b658e61a 100644 (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;
index cd4a14a3931c2869d654c53c710c0af51c7890bf..5de7a51103659195e7eacb683aaebfbb4bea6e8e 100644 (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']);
 
index 2fce6a3d53fb86e14e1b773f4126f0b1ba456fb2..9586beffba27e716f71e990b578c8bbeee717bb0 100644 (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)
     {
index 4b19aceac7af85596342c534c1561f910c31d22a..0128e38040da47d0f376d53e5963c767005d7409 100644 (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);
     }
index ad437604b1c47b0f46d57612593948b74c414395..48250816fefad756cff9feec3c16b5f456787567 100644 (file)
                         @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>