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:
@@ -12,31 +12,22 @@ use Illuminate\Database\Eloquent\Collection;
|
|||||||
|
|
||||||
class EntityHydrator
|
class EntityHydrator
|
||||||
{
|
{
|
||||||
/**
|
public function __construct(
|
||||||
* @var EntityTable[] $entities
|
protected EntityQueries $entityQueries,
|
||||||
*/
|
) {
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hydrate the entities of this hydrator to return a list of entities represented
|
* Hydrate the entities of this hydrator to return a list of entities represented
|
||||||
* in their original intended models.
|
* in their original intended models.
|
||||||
|
* @param EntityTable[] $entities
|
||||||
* @return Entity[]
|
* @return Entity[]
|
||||||
*/
|
*/
|
||||||
public function hydrate(): array
|
public function hydrate(array $entities, bool $loadTags = false, bool $loadParents = false): array
|
||||||
{
|
{
|
||||||
$hydrated = [];
|
$hydrated = [];
|
||||||
|
|
||||||
foreach ($this->entities as $entity) {
|
foreach ($entities as $entity) {
|
||||||
$data = $entity->getRawOriginal();
|
$data = $entity->getRawOriginal();
|
||||||
$instance = Entity::instanceFromType($entity->type);
|
$instance = Entity::instanceFromType($entity->type);
|
||||||
|
|
||||||
@@ -49,11 +40,11 @@ class EntityHydrator
|
|||||||
$hydrated[] = $instance;
|
$hydrated[] = $instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->loadTags) {
|
if ($loadTags) {
|
||||||
$this->loadTagsIntoModels($hydrated);
|
$this->loadTagsIntoModels($hydrated);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->loadParents) {
|
if ($loadParents) {
|
||||||
$this->loadParentsIntoModels($hydrated);
|
$this->loadParentsIntoModels($hydrated);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,10 +106,7 @@ class EntityHydrator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - Inject in?
|
$parentQuery = $this->entityQueries->visibleForList();
|
||||||
$queries = app()->make(EntityQueries::class);
|
|
||||||
|
|
||||||
$parentQuery = $queries->visibleForList();
|
|
||||||
$filtered = count($parentsByType['book']) > 0 || count($parentsByType['chapter']) > 0;
|
$filtered = count($parentsByType['book']) > 0 || count($parentsByType['chapter']) > 0;
|
||||||
$parentQuery = $parentQuery->where(function ($query) use ($parentsByType) {
|
$parentQuery = $parentQuery->where(function ($query) use ($parentsByType) {
|
||||||
foreach ($parentsByType as $type => $ids) {
|
foreach ($parentsByType as $type => $ids) {
|
||||||
@@ -132,7 +120,7 @@ class EntityHydrator
|
|||||||
});
|
});
|
||||||
|
|
||||||
$parentModels = $filtered ? $parentQuery->get()->all() : [];
|
$parentModels = $filtered ? $parentQuery->get()->all() : [];
|
||||||
$parents = (new EntityHydrator($parentModels))->hydrate();
|
$parents = $this->hydrate($parentModels);
|
||||||
$parentMap = [];
|
$parentMap = [];
|
||||||
foreach ($parents as $parent) {
|
foreach ($parents as $parent) {
|
||||||
$parentMap[$parent->type . ':' . $parent->id] = $parent;
|
$parentMap[$parent->type . ':' . $parent->id] = $parent;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace BookStack\Search;
|
namespace BookStack\Search;
|
||||||
|
|
||||||
use BookStack\Api\ApiEntityListFormatter;
|
use BookStack\Api\ApiEntityListFormatter;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Http\ApiController;
|
use BookStack\Http\ApiController;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class SearchApiController extends ApiController
|
class SearchApiController extends ApiController
|
||||||
@@ -31,11 +34,9 @@ class SearchApiController extends ApiController
|
|||||||
* between: bookshelf, book, chapter & page.
|
* between: bookshelf, book, chapter & page.
|
||||||
*
|
*
|
||||||
* The paging parameters and response format emulates a standard listing endpoint
|
* 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
|
* but standard sorting and filtering cannot be done on this endpoint.
|
||||||
* is provided this will only be taken as a suggestion. The results in the response
|
|
||||||
* may currently be up to 4x this value.
|
|
||||||
*/
|
*/
|
||||||
public function all(Request $request)
|
public function all(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$this->validate($request, $this->rules['all']);
|
$this->validate($request, $this->rules['all']);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use BookStack\Entities\Queries\QueryPopular;
|
|||||||
use BookStack\Entities\Tools\SiblingFetcher;
|
use BookStack\Entities\Tools\SiblingFetcher;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
class SearchController extends Controller
|
class SearchController extends Controller
|
||||||
{
|
{
|
||||||
@@ -23,20 +24,21 @@ class SearchController extends Controller
|
|||||||
{
|
{
|
||||||
$searchOpts = SearchOptions::fromRequest($request);
|
$searchOpts = SearchOptions::fromRequest($request);
|
||||||
$fullSearchString = $searchOpts->toString();
|
$fullSearchString = $searchOpts->toString();
|
||||||
$this->setPageTitle(trans('entities.search_for_term', ['term' => $fullSearchString]));
|
|
||||||
|
|
||||||
$page = intval($request->get('page', '0')) ?: 1;
|
$page = intval($request->get('page', '0')) ?: 1;
|
||||||
$nextPageLink = url('/search?term=' . urlencode($fullSearchString) . '&page=' . ($page + 1));
|
|
||||||
|
|
||||||
$results = $this->searchRunner->searchEntities($searchOpts, 'all', $page, 20);
|
$results = $this->searchRunner->searchEntities($searchOpts, 'all', $page, 20);
|
||||||
$formatter->format($results['results']->all(), $searchOpts);
|
$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', [
|
return view('search.all', [
|
||||||
'entities' => $results['results'],
|
'entities' => $results['results'],
|
||||||
'totalResults' => $results['total'],
|
'totalResults' => $results['total'],
|
||||||
|
'paginator' => $paginator,
|
||||||
'searchTerm' => $fullSearchString,
|
'searchTerm' => $fullSearchString,
|
||||||
'hasNextPage' => $results['has_more'],
|
|
||||||
'nextPageLink' => $nextPageLink,
|
|
||||||
'options' => $searchOpts,
|
'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)
|
public function searchSiblings(Request $request, SiblingFetcher $siblingFetcher)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use BookStack\Search\Options\TagSearchOption;
|
|||||||
use BookStack\Users\Models\User;
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Database\Connection;
|
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;
|
||||||
@@ -30,17 +29,15 @@ class SearchRunner
|
|||||||
protected EntityProvider $entityProvider,
|
protected EntityProvider $entityProvider,
|
||||||
protected PermissionApplicator $permissions,
|
protected PermissionApplicator $permissions,
|
||||||
protected EntityQueries $entityQueries,
|
protected EntityQueries $entityQueries,
|
||||||
|
protected EntityHydrator $entityHydrator,
|
||||||
) {
|
) {
|
||||||
$this->termAdjustmentCache = new WeakMap();
|
$this->termAdjustmentCache = new WeakMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search all entities in the system.
|
* 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
|
public function searchEntities(SearchOptions $searchOpts, string $entityType = 'all', int $page = 1, int $count = 20): array
|
||||||
{
|
{
|
||||||
@@ -58,14 +55,9 @@ class SearchRunner
|
|||||||
$total = $searchQuery->count();
|
$total = $searchQuery->count();
|
||||||
$results = $this->getPageOfDataFromQuery($searchQuery, $page, $count);
|
$results = $this->getPageOfDataFromQuery($searchQuery, $page, $count);
|
||||||
|
|
||||||
// TODO - Pagination?
|
|
||||||
$hasMore = ($total > ($page * $count));
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'total' => $total,
|
'total' => $total,
|
||||||
'count' => count($results),
|
'results' => $results->values(),
|
||||||
'has_more' => $hasMore,
|
|
||||||
'results' => $results->sortByDesc('score')->values(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,15 +71,8 @@ class SearchRunner
|
|||||||
$filterMap = $opts->filters->toValueMap();
|
$filterMap = $opts->filters->toValueMap();
|
||||||
$entityTypesToSearch = isset($filterMap['type']) ? explode('|', $filterMap['type']) : $entityTypes;
|
$entityTypesToSearch = isset($filterMap['type']) ? explode('|', $filterMap['type']) : $entityTypes;
|
||||||
|
|
||||||
$results = collect();
|
$filteredTypes = array_intersect($entityTypesToSearch, $entityTypes);
|
||||||
foreach ($entityTypesToSearch as $entityType) {
|
$results = $this->buildQuery($opts, $filteredTypes)->where('book_id', '=', $bookId)->take(20)->get();
|
||||||
if (!in_array($entityType, $entityTypes)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$search = $this->buildQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
|
|
||||||
$results = $results->merge($search);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $results->sortByDesc('score')->take(20);
|
return $results->sortByDesc('score')->take(20);
|
||||||
}
|
}
|
||||||
@@ -98,7 +83,7 @@ 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();
|
$pages = $this->buildQuery($opts, ['page'])->where('chapter_id', '=', $chapterId)->take(20)->get();
|
||||||
|
|
||||||
return $pages->sortByDesc('score');
|
return $pages->sortByDesc('score');
|
||||||
}
|
}
|
||||||
@@ -113,7 +98,7 @@ class SearchRunner
|
|||||||
->take($count)
|
->take($count)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$hydrated = (new EntityHydrator($entities->all(), true, true))->hydrate();
|
$hydrated = $this->entityHydrator->hydrate($entities->all(), true, true);
|
||||||
|
|
||||||
return collect($hydrated);
|
return collect($hydrated);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,11 +87,7 @@
|
|||||||
@include('entities.list', ['entities' => $entities, 'showPath' => true, 'showTags' => true])
|
@include('entities.list', ['entities' => $entities, 'showPath' => true, 'showTags' => true])
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($hasNextPage)
|
{{ $paginator->render() }}
|
||||||
<div class="text-right mt-m">
|
|
||||||
<a href="{{ $nextPageLink }}" class="button outline">{{ trans('entities.search_more') }}</a>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user