mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-11-04 13:31:45 +03:00
Search: Tested changes to single-table search
Updated filters to use single table where needed.
This commit is contained in:
@@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
namespace BookStack\Entities\Models;
|
namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
|
use BookStack\Activity\Models\Tag;
|
||||||
|
use BookStack\Activity\Models\View;
|
||||||
use BookStack\App\Model;
|
use BookStack\App\Model;
|
||||||
|
use BookStack\Permissions\Models\EntityPermission;
|
||||||
use BookStack\Permissions\Models\JointPermission;
|
use BookStack\Permissions\Models\JointPermission;
|
||||||
use BookStack\Permissions\PermissionApplicator;
|
use BookStack\Permissions\PermissionApplicator;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,6 +36,34 @@ class EntityTable extends Model
|
|||||||
*/
|
*/
|
||||||
public function jointPermissions(): HasMany
|
public function jointPermissions(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(JointPermission::class, 'entity_id')->whereColumn('entity_type', '=', 'entities.type');
|
return $this->hasMany(JointPermission::class, 'entity_id')
|
||||||
|
->whereColumn('entity_type', '=', 'entities.type');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Tags that have been assigned to entities.
|
||||||
|
*/
|
||||||
|
public function tags(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Tag::class, 'entity_id')
|
||||||
|
->whereColumn('entity_type', '=', 'entities.type');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the assigned permissions.
|
||||||
|
*/
|
||||||
|
public function permissions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(EntityPermission::class, 'entity_id')
|
||||||
|
->whereColumn('entity_type', '=', 'entities.type');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get View objects for this entity.
|
||||||
|
*/
|
||||||
|
public function views(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(View::class, 'viewable_id')
|
||||||
|
->whereColumn('viewable_type', '=', 'entities.type');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace BookStack\Entities\Queries;
|
|||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Models\EntityTable;
|
use BookStack\Entities\Models\EntityTable;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||||
use Illuminate\Database\Query\JoinClause;
|
use Illuminate\Database\Query\JoinClause;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
@@ -43,8 +44,14 @@ class EntityQueries
|
|||||||
public function visibleForList(): Builder
|
public function visibleForList(): Builder
|
||||||
{
|
{
|
||||||
$rawDescriptionField = DB::raw('COALESCE(description, text) as description');
|
$rawDescriptionField = DB::raw('COALESCE(description, text) as description');
|
||||||
|
$bookSlugSelect = function (QueryBuilder $query) {
|
||||||
|
return $query->select('slug')->from('entities as books')
|
||||||
|
->whereColumn('books.id', '=', 'entities.book_id')
|
||||||
|
->where('type', '=', 'book');
|
||||||
|
};
|
||||||
|
|
||||||
return EntityTable::query()->scopes('visible')
|
return EntityTable::query()->scopes('visible')
|
||||||
->select(['id', 'type', 'name', 'slug', 'book_id', 'chapter_id', 'created_at', 'updated_at', 'draft', $rawDescriptionField])
|
->select(['id', 'type', 'name', 'slug', 'book_id', 'chapter_id', 'created_at', 'updated_at', 'draft', 'book_slug' => $bookSlugSelect, $rawDescriptionField])
|
||||||
->leftJoin('entity_container_data', function (JoinClause $join) {
|
->leftJoin('entity_container_data', function (JoinClause $join) {
|
||||||
$join->on('entity_container_data.entity_id', '=', 'entities.id')
|
$join->on('entity_container_data.entity_id', '=', 'entities.id')
|
||||||
->on('entity_container_data.entity_type', '=', 'entities.type');
|
->on('entity_container_data.entity_type', '=', 'entities.type');
|
||||||
|
|||||||
@@ -129,11 +129,11 @@ class EntityHydrator
|
|||||||
foreach ($entities as $entity) {
|
foreach ($entities as $entity) {
|
||||||
if ($entity instanceof Page || $entity instanceof Chapter) {
|
if ($entity instanceof Page || $entity instanceof Chapter) {
|
||||||
$key = 'book:' . $entity->getRawAttribute('book_id');
|
$key = 'book:' . $entity->getRawAttribute('book_id');
|
||||||
$entity->setAttribute('book', $parentMap[$key] ?? null);
|
$entity->setRelation('book', $parentMap[$key] ?? null);
|
||||||
}
|
}
|
||||||
if ($entity instanceof Page) {
|
if ($entity instanceof Page) {
|
||||||
$key = 'chapter:' . $entity->getRawAttribute('chapter_id');
|
$key = 'chapter:' . $entity->getRawAttribute('chapter_id');
|
||||||
$entity->setAttribute('chapter', $parentMap[$key] ?? null);
|
$entity->setRelation('chapter', $parentMap[$key] ?? null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ class SearchRunner
|
|||||||
$entityTypesToSearch = isset($filterMap['type']) ? explode('|', $filterMap['type']) : $entityTypes;
|
$entityTypesToSearch = isset($filterMap['type']) ? explode('|', $filterMap['type']) : $entityTypes;
|
||||||
|
|
||||||
$filteredTypes = array_intersect($entityTypesToSearch, $entityTypes);
|
$filteredTypes = array_intersect($entityTypesToSearch, $entityTypes);
|
||||||
$results = $this->buildQuery($opts, $filteredTypes)->where('book_id', '=', $bookId)->take(20)->get();
|
$query = $this->buildQuery($opts, $filteredTypes)->where('book_id', '=', $bookId);
|
||||||
|
|
||||||
return $results->sortByDesc('score')->take(20);
|
return $this->getPageOfDataFromQuery($query, 1, 20)->sortByDesc('score');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,9 +83,9 @@ 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();
|
$query = $this->buildQuery($opts, ['page'])->where('chapter_id', '=', $chapterId);
|
||||||
|
|
||||||
return $pages->sortByDesc('score');
|
return $this->getPageOfDataFromQuery($query, 1, 20)->sortByDesc('score');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,7 +120,8 @@ class SearchRunner
|
|||||||
$filter = function (EloquentBuilder $query) use ($exact) {
|
$filter = function (EloquentBuilder $query) use ($exact) {
|
||||||
$inputTerm = str_replace('\\', '\\\\', $exact->value);
|
$inputTerm = str_replace('\\', '\\\\', $exact->value);
|
||||||
$query->where('name', 'like', '%' . $inputTerm . '%')
|
$query->where('name', 'like', '%' . $inputTerm . '%')
|
||||||
->orWhere('description', 'like', '%' . $inputTerm . '%');
|
->orWhere('description', 'like', '%' . $inputTerm . '%')
|
||||||
|
->orWhere('text', 'like', '%' . $inputTerm . '%');
|
||||||
};
|
};
|
||||||
|
|
||||||
$exact->negated ? $entityQuery->whereNot($filter) : $entityQuery->where($filter);
|
$exact->negated ? $entityQuery->whereNot($filter) : $entityQuery->where($filter);
|
||||||
@@ -301,7 +302,7 @@ class SearchRunner
|
|||||||
$option->negated ? $query->whereDoesntHave('tags', $filter) : $query->whereHas('tags', $filter);
|
$option->negated ? $query->whereDoesntHave('tags', $filter) : $query->whereHas('tags', $filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function applyNegatableWhere(EloquentBuilder $query, bool $negated, string $column, string $operator, mixed $value): void
|
protected function applyNegatableWhere(EloquentBuilder $query, bool $negated, string|callable $column, string|null $operator, mixed $value): void
|
||||||
{
|
{
|
||||||
if ($negated) {
|
if ($negated) {
|
||||||
$query->whereNot($column, $operator, $value);
|
$query->whereNot($column, $operator, $value);
|
||||||
@@ -376,7 +377,10 @@ class SearchRunner
|
|||||||
|
|
||||||
protected function filterInBody(EloquentBuilder $query, string $input, bool $negated)
|
protected function filterInBody(EloquentBuilder $query, string $input, bool $negated)
|
||||||
{
|
{
|
||||||
$this->applyNegatableWhere($query, $negated, 'description', 'like', '%' . $input . '%');
|
$this->applyNegatableWhere($query, $negated, function (EloquentBuilder $query) use ($input) {
|
||||||
|
$query->where('description', 'like', '%' . $input . '%')
|
||||||
|
->orWhere('text', 'like', '%' . $input . '%');
|
||||||
|
}, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function filterIsRestricted(EloquentBuilder $query, string $input, bool $negated)
|
protected function filterIsRestricted(EloquentBuilder $query, string $input, bool $negated)
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ class SearchApiTest extends TestCase
|
|||||||
$this->permissions->disableEntityInheritedPermissions($book);
|
$this->permissions->disableEntityInheritedPermissions($book);
|
||||||
|
|
||||||
$resp = $this->getJson($this->baseEndpoint . '?query=superextrauniquevalue');
|
$resp = $this->getJson($this->baseEndpoint . '?query=superextrauniquevalue');
|
||||||
|
$resp->assertOk();
|
||||||
$resp->assertJsonPath('data.0.id', $page->id);
|
$resp->assertJsonPath('data.0.id', $page->id);
|
||||||
$resp->assertJsonMissingPath('data.0.book.name');
|
$resp->assertJsonMissingPath('data.0.book.name');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ class EntitySearchTest extends TestCase
|
|||||||
$search->assertSeeText($shelf->name, true);
|
$search->assertSeeText($shelf->name, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_search_shows_pagination()
|
||||||
|
{
|
||||||
|
$search = $this->asEditor()->get('/search?term=a');
|
||||||
|
$this->withHtml($search)->assertLinkExists('/search?term=a&page=2', '2');
|
||||||
|
}
|
||||||
|
|
||||||
public function test_invalid_page_search()
|
public function test_invalid_page_search()
|
||||||
{
|
{
|
||||||
$resp = $this->asEditor()->get('/search?term=' . urlencode('<p>test</p>'));
|
$resp = $this->asEditor()->get('/search?term=' . urlencode('<p>test</p>'));
|
||||||
|
|||||||
Reference in New Issue
Block a user