mirror of
https://github.com/AnimeThemes/animethemes-server.git
synced 2026-07-11 01:24:46 +02:00
feat(search): add Typesense support (#1146)
This commit is contained in:
+11
-10
@@ -87,16 +87,6 @@ DUMP_DISK_ROOT=
|
||||
DUMP_URL=http://localhost
|
||||
DUMP_PATH=/dump
|
||||
|
||||
# elastic client
|
||||
ELASTIC_CONNECTION=default
|
||||
ELASTIC_HOST=http://localhost:9200
|
||||
|
||||
# elastic driver
|
||||
ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS=false
|
||||
|
||||
# elastic migrations
|
||||
ELASTIC_MIGRATIONS_TABLE=elastic_migrations
|
||||
|
||||
# external profile
|
||||
USER_MAX_PROFILES=5
|
||||
|
||||
@@ -240,9 +230,20 @@ SCOUT_DRIVER=null
|
||||
SCOUT_PREFIX=
|
||||
SCOUT_QUEUE=false
|
||||
SCOUT_IDENTIFY=false
|
||||
|
||||
ALGOLIA_APP_ID=
|
||||
ALGOLIA_SECRET=
|
||||
|
||||
ELASTIC_CONNECTION=default
|
||||
ELASTIC_HOST=http://localhost:9200
|
||||
ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS=false
|
||||
ELASTIC_MIGRATIONS_TABLE=elastic_migrations
|
||||
|
||||
TYPESENSE_HOST=localhost
|
||||
TYPESENSE_PORT=8108
|
||||
TYPESENSE_PROTOCOL=http
|
||||
TYPESENSE_API_KEY=
|
||||
|
||||
# secure headers
|
||||
ENABLE_CLEAR_SITE_DATA=false
|
||||
ENABLE_HSTS=false
|
||||
|
||||
+11
-10
@@ -88,16 +88,6 @@ DUMP_DISK_ROOT=
|
||||
DUMP_URL=http://localhost
|
||||
DUMP_PATH=/dump
|
||||
|
||||
# elastic client
|
||||
ELASTIC_CONNECTION=default
|
||||
ELASTIC_HOST=http://elasticsearch:9200
|
||||
|
||||
# elastic driver
|
||||
ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS=false
|
||||
|
||||
# elastic migrations
|
||||
ELASTIC_MIGRATIONS_TABLE=elastic_migrations
|
||||
|
||||
# ffmpeg
|
||||
FFMPEG_BINARIES=
|
||||
FFPROBE_BINARIES=
|
||||
@@ -238,9 +228,20 @@ SCOUT_DRIVER=elastic
|
||||
SCOUT_PREFIX=
|
||||
SCOUT_QUEUE=false
|
||||
SCOUT_IDENTIFY=false
|
||||
|
||||
ALGOLIA_APP_ID=
|
||||
ALGOLIA_SECRET=
|
||||
|
||||
ELASTIC_CONNECTION=default
|
||||
ELASTIC_HOST=http://elasticsearch:9200
|
||||
ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS=false
|
||||
ELASTIC_MIGRATIONS_TABLE=elastic_migrations
|
||||
|
||||
TYPESENSE_HOST=localhost
|
||||
TYPESENSE_PORT=8108
|
||||
TYPESENSE_PROTOCOL=http
|
||||
TYPESENSE_API_KEY=
|
||||
|
||||
# secure headers
|
||||
ENABLE_CLEAR_SITE_DATA=false
|
||||
ENABLE_HSTS=false
|
||||
|
||||
@@ -25,7 +25,7 @@ This project is powered by [**Laravel**](https://laravel.com/), a PHP framework
|
||||
- [Extra Configuration](#extra-configuration)
|
||||
- [Feature Flags](#feature-flags)
|
||||
- [Users](#users)
|
||||
- [Elasticsearch](#elasticsearch)
|
||||
- [Search](#search)
|
||||
- [Local Storage](#local-storage)
|
||||
- [Contributing](#contributing)
|
||||
- [Resources](#resources)
|
||||
@@ -113,17 +113,19 @@ $user = User::factory()->create(['name' => 'User Name', 'email' => 'example@exam
|
||||
$user->assignRole('Admin');
|
||||
```
|
||||
|
||||
### Elasticsearch
|
||||
### Search
|
||||
|
||||
If we want to enable scout, we need to configure Elasticsearch.
|
||||
If we want to enable scout, we need to configure a search engine (either Elasticsearch or Typesense).
|
||||
|
||||
If we have installed Elasticsearch, migrate and import models into our indices using:
|
||||
Change the `SCOUT_DRIVER` variable in `.env` to either "elastic" or "typesense". Add additional configuration like host and port.
|
||||
|
||||
Migrate and import models into our indices using:
|
||||
|
||||
```sh
|
||||
# Run the elastic migrations
|
||||
# Elasticsearch: run the elastic migrations
|
||||
php artisan elastic:migrate
|
||||
|
||||
# Import Models with a seeder
|
||||
# Elasticsearch and Typesense: Import Models with a seeder
|
||||
php artisan db:seed --class="Database\Seeders\Scout\ImportModelsSeeder"
|
||||
```
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ use App\GraphQL\Criteria\Sort\SortCriteria;
|
||||
use App\GraphQL\Schema\Enums\SortableColumns;
|
||||
use App\GraphQL\Schema\Types\BaseType;
|
||||
use App\Rules\GraphQL\Argument\FirstArgumentRule;
|
||||
use App\Search\Criteria;
|
||||
use App\Search\Search;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Search;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use Illuminate\Contracts\Pagination\Paginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -46,16 +46,17 @@ class IndexAction
|
||||
{
|
||||
$criteria = new Criteria(Arr::get($args, 'search'));
|
||||
|
||||
$searchBuilder = Search::search($builder->getModel(), $criteria)
|
||||
->passToEloquentBuilder(function (Builder $builder) use ($args, $type, $resolveInfo): void {
|
||||
$this->withAggregates($builder, $args, $this->getSelection($resolveInfo), $type);
|
||||
$searchBuilder = Search::getSearch($builder->getModel(), $criteria);
|
||||
|
||||
$this->filter($builder, $args, $type);
|
||||
$eloquentCallback = function (Builder $builder) use ($args, $type, $resolveInfo): void {
|
||||
$this->withAggregates($builder, $args, $this->getSelection($resolveInfo), $type);
|
||||
|
||||
$this->sort($builder, $args, $type);
|
||||
$this->filter($builder, $args, $type);
|
||||
|
||||
$this->constrainEagerLoads($builder, $resolveInfo, $type);
|
||||
});
|
||||
$this->sort($builder, $args, $type);
|
||||
|
||||
$this->constrainEagerLoads($builder, $resolveInfo, $type);
|
||||
};
|
||||
|
||||
// Note: First for searching must not be too high.
|
||||
$first = min(100, Arr::get($args, 'first'));
|
||||
@@ -65,8 +66,6 @@ class IndexAction
|
||||
'first' => ['required', 'integer', 'min:1', new FirstArgumentRule()],
|
||||
])->validate();
|
||||
|
||||
$searchBuilder->withPagination($first, $page);
|
||||
|
||||
$sorts = Arr::get($args, SortArgument::ARGUMENT, []);
|
||||
$criteria = Arr::get(new SortableColumns($type)->getAttributes(), 'criteria');
|
||||
|
||||
@@ -93,8 +92,6 @@ class IndexAction
|
||||
}
|
||||
}
|
||||
|
||||
$searchBuilder->withSort($sortsRaw);
|
||||
|
||||
return $searchBuilder->execute();
|
||||
return $searchBuilder->search($eloquentCallback, $first, $page, $sortsRaw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,10 @@ use App\Http\Api\Query\Query;
|
||||
use App\Http\Api\Schema\EloquentSchema;
|
||||
use App\Http\Api\Schema\Schema;
|
||||
use App\Http\Api\Scope\ScopeParser;
|
||||
use App\Scout\Elasticsearch\Elasticsearch;
|
||||
use App\Scout\Search;
|
||||
use Illuminate\Contracts\Pagination\Paginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use RuntimeException;
|
||||
|
||||
@@ -64,10 +62,12 @@ class IndexAction
|
||||
Schema $schema,
|
||||
PaginationStrategy $paginationStrategy = PaginationStrategy::OFFSET
|
||||
): Collection|Paginator {
|
||||
$search = $this->getSearch();
|
||||
if ($query->hasSearchCriteria() && $schema instanceof EloquentSchema) {
|
||||
$search = Search::getSearch($schema->model(), $query->getSearchCriteria());
|
||||
|
||||
if ($search instanceof Search && $search->shouldSearch($query) && $schema instanceof EloquentSchema) {
|
||||
return $search->search($query, $schema, $paginationStrategy);
|
||||
if ($search->shouldSearch()) {
|
||||
return $search->searchViaJSONAPI($query, $schema, $paginationStrategy);
|
||||
}
|
||||
}
|
||||
|
||||
// Let developer know why search can't be performed
|
||||
@@ -75,12 +75,4 @@ class IndexAction
|
||||
$term = $query->getSearchCriteria()?->getTerm();
|
||||
throw new RuntimeException("Can't search for term '$term' with driver '$driver' and type '{$schema->type()}'");
|
||||
}
|
||||
|
||||
protected function getSearch(): ?Search
|
||||
{
|
||||
return match (Config::get('scout.driver')) {
|
||||
'elastic' => App::make(Elasticsearch::class),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ class OptimizeImageAction
|
||||
protected function handleFFmpeg(): ?string
|
||||
{
|
||||
$filesToDelete = [];
|
||||
$imagePath = null;
|
||||
|
||||
try {
|
||||
Storage::disk('local')->put(
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contracts\Search;
|
||||
|
||||
use App\Search\Criteria;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Pagination\Paginator;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
interface SearchDriver
|
||||
{
|
||||
/**
|
||||
* Build the query.
|
||||
*/
|
||||
public static function search(Model $model, Criteria $criteria): SearchDriver;
|
||||
|
||||
/**
|
||||
* Paginate the results.
|
||||
*/
|
||||
public function withPagination(int $perPage, int $page): static;
|
||||
|
||||
/**
|
||||
* @param array<string, array{direction: string, isString: bool, relation: ?string}> $sorts
|
||||
*/
|
||||
public function withSort(array $sorts): static;
|
||||
|
||||
/**
|
||||
* Run a callback through the Eloquent query.
|
||||
*/
|
||||
public function passToEloquentBuilder(Closure $callback): SearchDriver;
|
||||
|
||||
/**
|
||||
* Execute the search and get the resulting models.
|
||||
*/
|
||||
public function execute(): Paginator;
|
||||
|
||||
/**
|
||||
* Get the keys of the retrieved models.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function keys(): array;
|
||||
}
|
||||
@@ -9,8 +9,8 @@ use App\Contracts\Models\Nameable;
|
||||
use App\Enums\Http\Api\Filter\ComparisonOperator;
|
||||
use App\Filament\Resources\BaseResource;
|
||||
use App\Models\Auth\User;
|
||||
use App\Search\Criteria;
|
||||
use App\Search\Search;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Search;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Laravel\Scout\Searchable;
|
||||
@@ -73,8 +73,8 @@ class BelongsTo extends Select
|
||||
return $this
|
||||
->getSearchResultsUsing(
|
||||
fn (string $search) => collect(
|
||||
Search::search($modelClass, new Criteria($this->escapeReservedChars($search)))
|
||||
->execute()
|
||||
Search::getSearch($modelClass, new Criteria($this->escapeReservedChars($search)))
|
||||
->search()
|
||||
->items()
|
||||
)
|
||||
->mapWithKeys(fn (Model $model): array => [$model->getKey() => static::getSearchLabelWithBlade($model, $this->withSubtitle)])
|
||||
|
||||
@@ -6,8 +6,8 @@ namespace App\Filament\Components\Fields;
|
||||
|
||||
use App\Contracts\Models\Nameable;
|
||||
use App\Filament\RelationManagers\BaseRelationManager;
|
||||
use App\Search\Criteria;
|
||||
use App\Search\Search;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Search;
|
||||
use Filament\Forms\Components\Select as ComponentsSelect;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -29,8 +29,8 @@ class Select extends ComponentsSelect
|
||||
->getOptionLabelUsing(fn ($state): string => is_null($state) ? '' : BelongsTo::getSearchLabelWithBlade($modelClass::query()->find($state)))
|
||||
->getSearchResultsUsing(
|
||||
fn (string $search) => collect(
|
||||
Search::search($modelClass, new Criteria($this->escapeReservedChars($search)))
|
||||
->passToEloquentBuilder(function (Builder $query) use ($loadRelation, $livewire): void {
|
||||
Search::getSearch($modelClass, new Criteria($this->escapeReservedChars($search)))
|
||||
->search(function (Builder $query) use ($loadRelation, $livewire): void {
|
||||
$query->with($loadRelation ?? []);
|
||||
|
||||
if (! ($livewire instanceof BaseRelationManager)
|
||||
@@ -41,7 +41,6 @@ class Select extends ComponentsSelect
|
||||
// This is necessary to prevent already attached records from being returned on search.
|
||||
$query->whereDoesntHave($livewire->getTable()->getInverseRelationship(), fn (Builder $query) => $query->whereKey($livewire->getOwnerRecord()->getKey()));
|
||||
})
|
||||
->execute()
|
||||
->items()
|
||||
)
|
||||
->mapWithKeys(fn (Model $model): array => [$model->getKey() => BelongsTo::getSearchLabelWithBlade($model)])
|
||||
|
||||
@@ -9,8 +9,8 @@ use App\Contracts\Models\Nameable;
|
||||
use App\Enums\Http\Api\Filter\ComparisonOperator;
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\User\Submission\SubmissionVirtual;
|
||||
use App\Search\Criteria;
|
||||
use App\Search\Search;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Search;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -89,8 +89,8 @@ class SubmissionBelongsTo extends BelongsTo
|
||||
return $this
|
||||
->getSearchResultsUsing(
|
||||
fn (string $search) => collect(
|
||||
Search::search($modelClass, new Criteria($this->escapeReservedChars($search)))
|
||||
->execute()
|
||||
Search::getSearch($modelClass, new Criteria($this->escapeReservedChars($search)))
|
||||
->search()
|
||||
->items()
|
||||
)
|
||||
->mapWithKeys(fn (Model $model): array => [$model->getKey() => static::getSearchLabelWithBlade($model, $this->withSubtitle)])
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Providers;
|
||||
|
||||
use App\Filament\Resources\BaseResource;
|
||||
use App\Search\Criteria;
|
||||
use App\Search\Search;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Search;
|
||||
use Elastic\ScoutDriverPlus\Searchable;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\GlobalSearch\GlobalSearchResult;
|
||||
@@ -34,9 +34,8 @@ class GlobalSearchScoutProvider implements GlobalSearchProvider
|
||||
}
|
||||
|
||||
$resourceResults = collect(
|
||||
Search::search($modelClass, new Criteria($this->escapeReservedChars($query)))
|
||||
->passToEloquentBuilder(fn (Builder $builder) => $builder->with($resource::getEloquentQuery()->getEagerLoads()))
|
||||
->execute()
|
||||
Search::getSearch($modelClass, new Criteria($this->escapeReservedChars($query)))
|
||||
->search(fn (Builder $builder) => $builder->with($resource::getEloquentQuery()->getEagerLoads()))
|
||||
->items()
|
||||
)
|
||||
->map(function (Model $record) use ($resource): ?GlobalSearchResult {
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\Base;
|
||||
|
||||
use App\Filament\Actions\Base\CreateAction;
|
||||
use App\Search\Criteria;
|
||||
use App\Search\Search;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Search;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -37,8 +37,11 @@ abstract class BaseListResources extends ListRecords
|
||||
$model = new $modelClass;
|
||||
|
||||
if (filled($search = $this->getTableSearch())) {
|
||||
$keys = Search::search($modelClass, new Criteria($this->escapeReservedChars($search)))
|
||||
->keys();
|
||||
$keys = Search::getSearch($modelClass, new Criteria($this->escapeReservedChars($search)))
|
||||
->search()
|
||||
->getCollection()
|
||||
->map(fn (Model $model) => $model->getKey())
|
||||
->toArray();
|
||||
|
||||
$query
|
||||
->whereIn($model->getKeyName(), $keys)
|
||||
|
||||
@@ -82,7 +82,9 @@ class ThemeResource extends BaseResource
|
||||
*/
|
||||
public static function getRecordTitle(?Model $record): ?string
|
||||
{
|
||||
return $record?->getName();
|
||||
return $record instanceof AnimeTheme
|
||||
? $record->getName()
|
||||
: null;
|
||||
}
|
||||
|
||||
public static function canGloballySearch(): bool
|
||||
|
||||
@@ -26,8 +26,8 @@ use App\Models\Wiki\Series;
|
||||
use App\Models\Wiki\Song;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Models\Wiki\Video;
|
||||
use App\Search\Criteria;
|
||||
use App\Search\Search;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Search;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -131,10 +131,13 @@ class SearchQuery extends BaseQuery
|
||||
$page = Arr::get($args, 'page', 1);
|
||||
$first = Number::clamp(Arr::get($args, 'first'), 1, 15);
|
||||
|
||||
$searchBuilder = Search::search($modelClass, new Criteria($term))
|
||||
->passToEloquentBuilder(fn (Builder $builder) => $this->constrainEagerLoads($builder, $resolveInfo, $type, $fieldName))
|
||||
->withPagination($first, $page);
|
||||
$searchBuilder = Search::getSearch($modelClass, new Criteria($term))
|
||||
->search(
|
||||
fn (Builder $builder) => $this->constrainEagerLoads($builder, $resolveInfo, $type, $fieldName),
|
||||
$first,
|
||||
$page,
|
||||
);
|
||||
|
||||
return collect($searchBuilder->execute()->items());
|
||||
return collect($searchBuilder->items());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Api\Parser;
|
||||
|
||||
use App\Search\Criteria;
|
||||
use App\Scout\Criteria;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class SearchParser extends Parser
|
||||
|
||||
@@ -17,7 +17,7 @@ use App\Http\Api\Parser\IncludeParser;
|
||||
use App\Http\Api\Parser\PagingParser;
|
||||
use App\Http\Api\Parser\SearchParser;
|
||||
use App\Http\Api\Parser\SortParser;
|
||||
use App\Search\Criteria as SearchCriteria;
|
||||
use App\Scout\Criteria as SearchCriteria;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class Query
|
||||
|
||||
@@ -15,6 +15,8 @@ use App\Models\Auth\User;
|
||||
use App\Models\BaseModel;
|
||||
use App\Models\List\External\ExternalEntry;
|
||||
use App\Models\List\External\ExternalToken;
|
||||
use App\Scout\Elasticsearch\Models\List\ExternalProfileElasticModel;
|
||||
use App\Scout\Typesense\Models\List\ExternalProfileTypesenseModel;
|
||||
use Database\Factories\List\ExternalProfileFactory;
|
||||
use Elastic\ScoutDriverPlus\Searchable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
@@ -30,6 +32,7 @@ use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Uri;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @property int $profile_id
|
||||
@@ -122,11 +125,16 @@ class ExternalProfile extends BaseModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Only get the attributes as an array to prevent recursive toArray() calls.
|
||||
* Get the indexable data array for the model.
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return $this->attributesToArray();
|
||||
return match ($driver = Config::get('scout.driver')) {
|
||||
'collection',
|
||||
'elastic' => ExternalProfileElasticModel::toSearchableArray($this),
|
||||
'typesense' => ExternalProfileTypesenseModel::toSearchableArray($this),
|
||||
default => throw new RuntimeException("Unsupported {$driver} search driver configured."),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,8 @@ use App\Models\BaseModel;
|
||||
use App\Models\List\Playlist\PlaylistTrack;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Pivots\Morph\Imageable;
|
||||
use App\Scout\Elasticsearch\Models\List\PlaylistElasticModel;
|
||||
use App\Scout\Typesense\Models\List\PlaylistTypesenseModel;
|
||||
use Database\Factories\List\PlaylistFactory;
|
||||
use Elastic\ScoutDriverPlus\Searchable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
@@ -27,6 +29,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @property string|null $description
|
||||
@@ -151,13 +155,16 @@ class Playlist extends BaseModel implements HasAggregateLikes, HasHashids, HasIm
|
||||
}
|
||||
|
||||
/**
|
||||
* Only get the attributes as an array to prevent recursive toArray() calls.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
* Get the indexable data array for the model.
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return $this->attributesToArray();
|
||||
return match ($driver = Config::get('scout.driver')) {
|
||||
'collection',
|
||||
'elastic' => PlaylistElasticModel::toSearchableArray($this),
|
||||
'typesense' => PlaylistTypesenseModel::toSearchableArray($this),
|
||||
default => throw new RuntimeException("Unsupported {$driver} search driver configured."),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,8 @@ use App\Pivots\Morph\Imageable;
|
||||
use App\Pivots\Morph\Resourceable;
|
||||
use App\Pivots\Wiki\AnimeSeries;
|
||||
use App\Pivots\Wiki\AnimeStudio;
|
||||
use App\Scout\Elasticsearch\Models\Wiki\AnimeElasticModel;
|
||||
use App\Scout\Typesense\Models\Wiki\AnimeTypesenseModel;
|
||||
use Database\Factories\Wiki\AnimeFactory;
|
||||
use Deprecated;
|
||||
use Elastic\ScoutDriverPlus\Searchable;
|
||||
@@ -40,8 +42,10 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use OwenIt\Auditing\Auditable as HasAudits;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @property int $anime_id
|
||||
@@ -157,10 +161,12 @@ class Anime extends BaseModel implements Auditable, HasImages, HasResources, Has
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
$array = $this->attributesToArray();
|
||||
$array['synonyms'] = $this->synonyms->map(fn (Synonym $synonym) => $synonym->text)->all();
|
||||
|
||||
return $array;
|
||||
return match ($driver = Config::get('scout.driver')) {
|
||||
'collection',
|
||||
'elastic' => AnimeElasticModel::toSearchableArray($this),
|
||||
'typesense' => AnimeTypesenseModel::toSearchableArray($this),
|
||||
default => throw new RuntimeException("Unsupported {$driver} search driver configured."),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,12 +10,16 @@ use App\Contracts\Models\SoftDeletable;
|
||||
use App\Enums\Models\Wiki\AnimeSynonymType;
|
||||
use App\Models\BaseModel;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Scout\Elasticsearch\Models\Wiki\Anime\AnimeSynonymElasticModel;
|
||||
use App\Scout\Typesense\Models\Wiki\Anime\AnimeSynonymTypesenseModel;
|
||||
use Elastic\ScoutDriverPlus\Searchable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use OwenIt\Auditing\Auditable as HasAudits;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @property Anime $anime
|
||||
@@ -71,6 +75,19 @@ class AnimeSynonym extends BaseModel implements Auditable, SoftDeletable
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the indexable data array for the model.
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return match ($driver = Config::get('scout.driver')) {
|
||||
'collection',
|
||||
'elastic' => AnimeSynonymElasticModel::toSearchableArray($this),
|
||||
'typesense' => AnimeSynonymTypesenseModel::toSearchableArray($this),
|
||||
default => throw new RuntimeException("Unsupported {$driver} search driver configured."),
|
||||
};
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->text;
|
||||
|
||||
@@ -22,6 +22,8 @@ use App\Models\Wiki\Group;
|
||||
use App\Models\Wiki\Song;
|
||||
use App\Observers\Wiki\Anime\AnimeThemeObserver;
|
||||
use App\Scopes\WithoutInsertSongScope;
|
||||
use App\Scout\Elasticsearch\Models\Wiki\Anime\AnimeThemeElasticModel;
|
||||
use App\Scout\Typesense\Models\Wiki\Anime\AnimeThemeTypesenseModel;
|
||||
use Database\Factories\Wiki\Anime\AnimeThemeFactory;
|
||||
use Elastic\ScoutDriverPlus\Searchable;
|
||||
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
|
||||
@@ -32,10 +34,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Stringable;
|
||||
use OwenIt\Auditing\Auditable as HasAudits;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @property Anime $anime
|
||||
@@ -147,14 +151,12 @@ class AnimeTheme extends BaseModel implements Auditable, InteractsWithSchema, So
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
$array = $this->attributesToArray();
|
||||
$array['anime'] = $this->anime->toSearchableArray();
|
||||
|
||||
if ($this->song !== null) {
|
||||
$array['song'] = $this->song->toSearchableArray();
|
||||
}
|
||||
|
||||
return $array;
|
||||
return match ($driver = Config::get('scout.driver')) {
|
||||
'collection',
|
||||
'elastic' => AnimeThemeElasticModel::toSearchableArray($this),
|
||||
'typesense' => AnimeThemeTypesenseModel::toSearchableArray($this),
|
||||
default => throw new RuntimeException("Unsupported {$driver} search driver configured."),
|
||||
};
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
|
||||
@@ -30,6 +30,8 @@ use App\Models\Wiki\Song;
|
||||
use App\Models\Wiki\Video;
|
||||
use App\Pivots\Morph\Resourceable;
|
||||
use App\Pivots\Wiki\AnimeThemeEntryVideo;
|
||||
use App\Scout\Elasticsearch\Models\Wiki\Anime\Theme\AnimeThemeEntryElasticModel;
|
||||
use App\Scout\Typesense\Models\Wiki\Anime\Theme\AnimeThemeEntryTypesenseModel;
|
||||
use Database\Factories\Wiki\Anime\Theme\AnimeThemeEntryFactory;
|
||||
use Elastic\ScoutDriverPlus\Searchable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
@@ -40,10 +42,12 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Stringable;
|
||||
use OwenIt\Auditing\Auditable as HasAudits;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
use RuntimeException;
|
||||
use Znck\Eloquent\Relations\BelongsToThrough;
|
||||
use Znck\Eloquent\Traits\BelongsToThrough as ZnckBelongsToThrough;
|
||||
|
||||
@@ -157,14 +161,12 @@ class AnimeThemeEntry extends BaseModel implements Auditable, HasAggregateLikes,
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
$array = $this->attributesToArray();
|
||||
|
||||
$array['theme'] = $this->animetheme->toSearchableArray();
|
||||
|
||||
// Overwrite version with readable format "v{#}"
|
||||
$array['version'] = Str::of(strval($this->version))->prepend('v')->__toString();
|
||||
|
||||
return $array;
|
||||
return match ($driver = Config::get('scout.driver')) {
|
||||
'collection',
|
||||
'elastic' => AnimeThemeEntryElasticModel::toSearchableArray($this),
|
||||
'typesense' => AnimeThemeEntryTypesenseModel::toSearchableArray($this),
|
||||
default => throw new RuntimeException("Unsupported {$driver} search driver configured."),
|
||||
};
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
|
||||
+10
-13
@@ -24,6 +24,8 @@ use App\Pivots\Morph\Imageable;
|
||||
use App\Pivots\Morph\Resourceable;
|
||||
use App\Pivots\Wiki\ArtistMember;
|
||||
use App\Pivots\Wiki\ArtistSong;
|
||||
use App\Scout\Elasticsearch\Models\Wiki\ArtistElasticModel;
|
||||
use App\Scout\Typesense\Models\Wiki\ArtistTypesenseModel;
|
||||
use Database\Factories\Wiki\ArtistFactory;
|
||||
use Deprecated;
|
||||
use Elastic\ScoutDriverPlus\Searchable;
|
||||
@@ -36,8 +38,10 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use OwenIt\Auditing\Auditable as HasAudits;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
use RuntimeException;
|
||||
use Staudenmeir\EloquentHasManyDeep\HasManyDeep;
|
||||
use Staudenmeir\EloquentHasManyDeep\HasRelationships;
|
||||
|
||||
@@ -149,19 +153,12 @@ class Artist extends BaseModel implements Auditable, HasImages, HasResources, Ha
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
$array = $this->attributesToArray();
|
||||
|
||||
$array['as'] = $this->performances->map(fn (Performance $performance) => $performance->as)
|
||||
->toBase()
|
||||
->concat($this->memberships->map(fn (Membership $membership) => $membership->as))
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$array['synonyms'] = $this->synonyms->map(fn (Synonym $synonym) => $synonym->text)->all();
|
||||
|
||||
return $array;
|
||||
return match ($driver = Config::get('scout.driver')) {
|
||||
'collection',
|
||||
'elastic' => ArtistElasticModel::toSearchableArray($this),
|
||||
'typesense' => ArtistTypesenseModel::toSearchableArray($this),
|
||||
default => throw new RuntimeException("Unsupported {$driver} search driver configured."),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,8 @@ use App\Events\Wiki\Series\SeriesUpdated;
|
||||
use App\Http\Resources\Pivot\Wiki\Resource\AnimeSeriesJsonResource;
|
||||
use App\Models\BaseModel;
|
||||
use App\Pivots\Wiki\AnimeSeries;
|
||||
use App\Scout\Elasticsearch\Models\Wiki\SeriesElasticModel;
|
||||
use App\Scout\Typesense\Models\Wiki\SeriesTypesenseModel;
|
||||
use Database\Factories\Wiki\SeriesFactory;
|
||||
use Elastic\ScoutDriverPlus\Searchable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
@@ -21,8 +23,10 @@ use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use OwenIt\Auditing\Auditable as HasAudits;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @property Collection<int, Anime> $anime
|
||||
@@ -100,12 +104,12 @@ class Series extends BaseModel implements Auditable, SoftDeletable
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
$array = $this->attributesToArray();
|
||||
$array['anime'] = $this->anime->map(
|
||||
fn (Anime $anime): array => $anime->toSearchableArray()
|
||||
)->toArray();
|
||||
|
||||
return $array;
|
||||
return match ($driver = Config::get('scout.driver')) {
|
||||
'collection',
|
||||
'elastic' => SeriesElasticModel::toSearchableArray($this),
|
||||
'typesense' => SeriesTypesenseModel::toSearchableArray($this),
|
||||
default => throw new RuntimeException("Unsupported {$driver} search driver configured."),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,8 @@ use App\Models\Wiki\Anime\AnimeTheme;
|
||||
use App\Models\Wiki\Song\Performance;
|
||||
use App\Pivots\Morph\Resourceable;
|
||||
use App\Pivots\Wiki\ArtistSong;
|
||||
use App\Scout\Elasticsearch\Models\Wiki\SongElasticModel;
|
||||
use App\Scout\Typesense\Models\Wiki\SongTypesenseModel;
|
||||
use Database\Factories\Wiki\SongFactory;
|
||||
use Deprecated;
|
||||
use Elastic\ScoutDriverPlus\Searchable;
|
||||
@@ -28,8 +30,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use OwenIt\Auditing\Auditable as HasAudits;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @property Collection<int, AnimeTheme> $animethemes
|
||||
@@ -104,6 +108,19 @@ class Song extends BaseModel implements Auditable, HasResources, SoftDeletable
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the indexable data array for the model.
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return match ($driver = Config::get('scout.driver')) {
|
||||
'collection',
|
||||
'elastic' => SongElasticModel::toSearchableArray($this),
|
||||
'typesense' => SongTypesenseModel::toSearchableArray($this),
|
||||
default => throw new RuntimeException("Unsupported {$driver} search driver configured."),
|
||||
};
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
if (blank($this->title)) {
|
||||
|
||||
@@ -18,6 +18,8 @@ use App\Models\BaseModel;
|
||||
use App\Pivots\Morph\Imageable;
|
||||
use App\Pivots\Morph\Resourceable;
|
||||
use App\Pivots\Wiki\AnimeStudio;
|
||||
use App\Scout\Elasticsearch\Models\Wiki\StudioElasticModel;
|
||||
use App\Scout\Typesense\Models\Wiki\StudioTypesenseModel;
|
||||
use Database\Factories\Wiki\StudioFactory;
|
||||
use Elastic\ScoutDriverPlus\Searchable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
@@ -25,8 +27,10 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use OwenIt\Auditing\Auditable as HasAudits;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @property Collection<int, Anime> $anime
|
||||
@@ -93,6 +97,19 @@ class Studio extends BaseModel implements Auditable, HasImages, HasResources, So
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the indexable data array for the model.
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return match ($driver = Config::get('scout.driver')) {
|
||||
'collection',
|
||||
'elastic' => StudioElasticModel::toSearchableArray($this),
|
||||
'typesense' => StudioTypesenseModel::toSearchableArray($this),
|
||||
default => throw new RuntimeException("Unsupported {$driver} search driver configured."),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the route key for the model.
|
||||
*
|
||||
|
||||
@@ -15,14 +15,18 @@ use App\Events\Wiki\Synonym\SynonymDeleted;
|
||||
use App\Events\Wiki\Synonym\SynonymRestored;
|
||||
use App\Events\Wiki\Synonym\SynonymUpdated;
|
||||
use App\Models\BaseModel;
|
||||
use App\Scout\Elasticsearch\Models\Wiki\SynonymElasticModel;
|
||||
use App\Scout\Typesense\Models\Wiki\SynonymTypesenseModel;
|
||||
use Database\Factories\Wiki\SynonymFactory;
|
||||
use Elastic\ScoutDriverPlus\Searchable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use OwenIt\Auditing\Auditable as HasAudits;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @property int $synonym_id
|
||||
@@ -94,6 +98,19 @@ class Synonym extends BaseModel implements Auditable, SoftDeletable
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the indexable data array for the model.
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return match ($driver = Config::get('scout.driver')) {
|
||||
'collection',
|
||||
'elastic' => SynonymElasticModel::toSearchableArray($this),
|
||||
'typesense' => SynonymTypesenseModel::toSearchableArray($this),
|
||||
default => throw new RuntimeException("Unsupported {$driver} search driver configured."),
|
||||
};
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->text;
|
||||
|
||||
@@ -23,6 +23,8 @@ use App\Models\List\Playlist\PlaylistTrack;
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use App\Models\Wiki\Video\VideoScript;
|
||||
use App\Pivots\Wiki\AnimeThemeEntryVideo;
|
||||
use App\Scout\Elasticsearch\Models\Wiki\VideoElasticModel;
|
||||
use App\Scout\Typesense\Models\Wiki\VideoTypesenseModel;
|
||||
use Database\Factories\Wiki\VideoFactory;
|
||||
use Elastic\ScoutDriverPlus\Searchable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
@@ -35,8 +37,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use OwenIt\Auditing\Auditable as HasAudits;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @property Collection<int, AnimeThemeEntry> $animethemeentries
|
||||
@@ -247,18 +251,15 @@ class Video extends BaseModel implements Auditable, SoftDeletable, Streamable
|
||||
|
||||
/**
|
||||
* Get the indexable data array for the model.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
$array = $this->attributesToArray();
|
||||
|
||||
$array['entries'] = $this->animethemeentries->map(
|
||||
fn (AnimeThemeEntry $entry): array => $entry->toSearchableArray()
|
||||
)->toArray();
|
||||
|
||||
return $array;
|
||||
return match ($driver = Config::get('scout.driver')) {
|
||||
'collection',
|
||||
'elastic' => VideoElasticModel::toSearchableArray($this),
|
||||
'typesense' => VideoTypesenseModel::toSearchableArray($this),
|
||||
default => throw new RuntimeException("Unsupported {$driver} search driver configured."),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,6 +43,7 @@ use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\ParallelTesting;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
|
||||
use Psr\Http\Message\MessageInterface;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -60,7 +61,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
Artisan::call('db:seed', ['--class' => ProhibitionSeeder::class]);
|
||||
});
|
||||
|
||||
Http::globalRequestMiddleware(fn (Request $request) => $request->withHeader('User-Agent', 'AnimeThemes'));
|
||||
Http::globalRequestMiddleware(fn (Request $request): MessageInterface => $request->withHeader('User-Agent', 'AnimeThemes'));
|
||||
|
||||
DB::listen(function (QueryExecuted $query): void {
|
||||
if (app()->isLocal()) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Collection;
|
||||
|
||||
use App\Enums\Http\Api\Paging\PaginationStrategy;
|
||||
use App\Http\Api\Query\Query;
|
||||
use App\Http\Api\Schema\EloquentSchema;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Search;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Pagination\Paginator;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class CollectionSearch extends Search
|
||||
{
|
||||
public function __construct(protected Model $model, Criteria $criteria)
|
||||
{
|
||||
parent::__construct($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the search.
|
||||
*/
|
||||
public function searchViaJSONAPI(
|
||||
Query $query,
|
||||
EloquentSchema $schema,
|
||||
PaginationStrategy $paginationStrategy
|
||||
): Collection|Paginator {
|
||||
// TODO
|
||||
return $this->search();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure(EloquentBuilder): void $callback
|
||||
* @param array<string, array{direction: string, isString: bool, relation: ?string}> $sorts
|
||||
*/
|
||||
public function search(?Closure $callback = null, int $perPage = 15, int $page = 1, array $sorts = []): LengthAwarePaginator
|
||||
{
|
||||
/** @phpstan-ignore-next-line */
|
||||
return $this->model::search($this->criteria->getTerm())
|
||||
->query($callback)
|
||||
->paginate($perPage, page: $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the search be performed?
|
||||
*/
|
||||
public function shouldSearch(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Search;
|
||||
namespace App\Scout;
|
||||
|
||||
readonly class Criteria
|
||||
{
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Elasticsearch\Api\Query;
|
||||
|
||||
use App\Search\Criteria;
|
||||
use App\Scout\Criteria;
|
||||
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
|
||||
|
||||
abstract class ElasticQuery
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Scout\Elasticsearch\Api\Query\List;
|
||||
|
||||
use App\Models\List\Playlist;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Elasticsearch\Api\Query\ElasticQuery;
|
||||
use App\Search\Criteria;
|
||||
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
|
||||
use Elastic\ScoutDriverPlus\Support\Query;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Scout\Elasticsearch\Api\Query\Wiki\Anime;
|
||||
|
||||
use App\Models\Wiki\Anime\AnimeSynonym;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Elasticsearch\Api\Query\ElasticQuery;
|
||||
use App\Search\Criteria;
|
||||
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
|
||||
use Elastic\ScoutDriverPlus\Support\Query;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Scout\Elasticsearch\Api\Query\Wiki\Anime\Theme;
|
||||
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Elasticsearch\Api\Query\ElasticQuery;
|
||||
use App\Search\Criteria;
|
||||
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
|
||||
use Elastic\ScoutDriverPlus\Support\Query;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Scout\Elasticsearch\Api\Query\Wiki\Anime;
|
||||
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Elasticsearch\Api\Query\ElasticQuery;
|
||||
use App\Search\Criteria;
|
||||
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
|
||||
use Elastic\ScoutDriverPlus\Support\Query;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Scout\Elasticsearch\Api\Query\Wiki;
|
||||
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Elasticsearch\Api\Query\ElasticQuery;
|
||||
use App\Search\Criteria;
|
||||
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
|
||||
use Elastic\ScoutDriverPlus\Support\Query;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Scout\Elasticsearch\Api\Query\Wiki;
|
||||
|
||||
use App\Models\Wiki\Artist;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Elasticsearch\Api\Query\ElasticQuery;
|
||||
use App\Search\Criteria;
|
||||
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
|
||||
use Elastic\ScoutDriverPlus\Support\Query;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Scout\Elasticsearch\Api\Query\Wiki;
|
||||
|
||||
use App\Models\Wiki\Series;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Elasticsearch\Api\Query\ElasticQuery;
|
||||
use App\Search\Criteria;
|
||||
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
|
||||
use Elastic\ScoutDriverPlus\Support\Query;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Scout\Elasticsearch\Api\Query\Wiki;
|
||||
|
||||
use App\Models\Wiki\Song;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Elasticsearch\Api\Query\ElasticQuery;
|
||||
use App\Search\Criteria;
|
||||
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
|
||||
use Elastic\ScoutDriverPlus\Support\Query;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Scout\Elasticsearch\Api\Query\Wiki;
|
||||
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Elasticsearch\Api\Query\ElasticQuery;
|
||||
use App\Search\Criteria;
|
||||
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
|
||||
use Elastic\ScoutDriverPlus\Support\Query;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Scout\Elasticsearch\Api\Query\Wiki;
|
||||
|
||||
use App\Models\Wiki\Synonym;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Elasticsearch\Api\Query\ElasticQuery;
|
||||
use App\Search\Criteria;
|
||||
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
|
||||
use Elastic\ScoutDriverPlus\Support\Query;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Scout\Elasticsearch\Api\Query\Wiki;
|
||||
|
||||
use App\Models\Wiki\Video;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Elasticsearch\Api\Query\ElasticQuery;
|
||||
use App\Search\Criteria;
|
||||
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
|
||||
use Elastic\ScoutDriverPlus\Support\Query;
|
||||
|
||||
|
||||
@@ -10,36 +10,68 @@ use App\Enums\Http\Api\Paging\PaginationStrategy;
|
||||
use App\Http\Api\Query\Query;
|
||||
use App\Http\Api\Schema\EloquentSchema;
|
||||
use App\Http\Api\Scope\ScopeParser;
|
||||
use App\Models\List\Playlist;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Anime\AnimeSynonym;
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use App\Models\Wiki\Artist;
|
||||
use App\Models\Wiki\Series;
|
||||
use App\Models\Wiki\Song;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Models\Wiki\Synonym;
|
||||
use App\Models\Wiki\Video;
|
||||
use App\Scout\Criteria as SearchCriteria;
|
||||
use App\Scout\Elasticsearch\Api\Criteria\Filter\Criteria;
|
||||
use App\Scout\Elasticsearch\Api\Parser\FilterParser;
|
||||
use App\Scout\Elasticsearch\Api\Parser\PagingParser;
|
||||
use App\Scout\Elasticsearch\Api\Parser\SortParser;
|
||||
use App\Scout\Elasticsearch\Api\Query\ElasticQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\List\PlaylistQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\Anime\SynonymQuery as AnimeSynonymQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\Anime\Theme\EntryQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\Anime\ThemeQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\AnimeQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\ArtistQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\SeriesQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\SongQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\StudioQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\SynonymQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\VideoQuery;
|
||||
use App\Scout\Elasticsearch\Api\Schema\Schema;
|
||||
use App\Scout\Search;
|
||||
use App\Search\Drivers\ElasticsearchDriver;
|
||||
use App\Search\Search as SearchSearch;
|
||||
use Closure;
|
||||
use Elastic\Client\ClientBuilderInterface;
|
||||
use Elastic\ScoutDriverPlus\Builders\BoolQueryBuilder;
|
||||
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
|
||||
use Elastic\ScoutDriverPlus\Exceptions\QueryBuilderValidationException;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Pagination\Paginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
|
||||
class Elasticsearch extends Search
|
||||
{
|
||||
use AggregatesFields;
|
||||
use ConstrainsEagerLoads;
|
||||
|
||||
protected SearchParametersBuilder $elasticBuilder;
|
||||
|
||||
/**
|
||||
* Is the ES instance reachable?
|
||||
*/
|
||||
protected bool $alive;
|
||||
|
||||
public function __construct(ClientBuilderInterface $builder)
|
||||
public function __construct(ClientBuilderInterface $builder, SearchCriteria $criteria)
|
||||
{
|
||||
parent::__construct($criteria);
|
||||
|
||||
try {
|
||||
$this->alive = $builder->default()->ping()->asBool();
|
||||
} catch (Exception $exception) {
|
||||
@@ -59,15 +91,15 @@ class Elasticsearch extends Search
|
||||
/**
|
||||
* Should the search be performed?
|
||||
*/
|
||||
public function shouldSearch(Query $query): bool
|
||||
public function shouldSearch(): bool
|
||||
{
|
||||
return $query->hasSearchCriteria() && $this->isAlive();
|
||||
return $this->isAlive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the search.
|
||||
*/
|
||||
public function search(
|
||||
public function searchViaJSONAPI(
|
||||
Query $query,
|
||||
EloquentSchema $schema,
|
||||
PaginationStrategy $paginationStrategy
|
||||
@@ -75,17 +107,16 @@ class Elasticsearch extends Search
|
||||
$elasticSchema = $this->elasticSchema($schema);
|
||||
|
||||
// initialize builder for matches
|
||||
/** @var ElasticsearchDriver $searchBuilder */
|
||||
$searchBuilder = SearchSearch::search($elasticSchema->model(), $query->getSearchCriteria());
|
||||
$builder = $searchBuilder->builder;
|
||||
$searchBuilder = static::elasticQuery($elasticSchema->model()::class)->build($this->criteria);
|
||||
$this->elasticBuilder = $searchBuilder;
|
||||
|
||||
// load aggregate fields
|
||||
$builder->refineModels(function (Builder $searchModelBuilder) use ($query, $schema): void {
|
||||
$this->elasticBuilder->refineModels(function (EloquentBuilder $searchModelBuilder) use ($query, $schema): void {
|
||||
$this->withAggregates($searchModelBuilder, $query, $schema);
|
||||
});
|
||||
|
||||
// eager load relations with constraints
|
||||
$builder = $builder->load($this->constrainEagerLoads($query, $schema));
|
||||
$this->elasticBuilder = $this->elasticBuilder->load($this->constrainEagerLoads($query, $schema));
|
||||
|
||||
// apply filters
|
||||
$filterBuilder = new BoolQueryBuilder();
|
||||
@@ -101,7 +132,7 @@ class Elasticsearch extends Search
|
||||
}
|
||||
}
|
||||
try {
|
||||
$builder->postFilter($filterBuilder);
|
||||
$this->elasticBuilder->postFilter($filterBuilder);
|
||||
} catch (QueryBuilderValidationException) {
|
||||
// There doesn't appear to be a way to check if any filters have been set in the filter builder
|
||||
}
|
||||
@@ -119,7 +150,7 @@ class Elasticsearch extends Search
|
||||
}
|
||||
}
|
||||
if ($sorts !== []) {
|
||||
$builder->sortRaw($sorts);
|
||||
$this->elasticBuilder->sortRaw($sorts);
|
||||
}
|
||||
|
||||
// paginate
|
||||
@@ -127,8 +158,67 @@ class Elasticsearch extends Search
|
||||
$elasticPaginationCriteria = PagingParser::parse($paginationCriteria);
|
||||
|
||||
return $elasticPaginationCriteria instanceof Api\Criteria\Paging\Criteria
|
||||
? $elasticPaginationCriteria->paginate($builder)
|
||||
: $builder->execute()->models();
|
||||
? $elasticPaginationCriteria->paginate($this->elasticBuilder)
|
||||
: $this->elasticBuilder->execute()->models();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure(EloquentBuilder): void $callback
|
||||
* @param array<string, array{direction: string, isString: bool, relation: ?string}> $sorts
|
||||
*/
|
||||
public function search(?Closure $callback = null, int $perPage = 15, int $page = 1, array $sorts = []): LengthAwarePaginator
|
||||
{
|
||||
// Resolve callback.
|
||||
$this->elasticBuilder->refineModels($callback);
|
||||
|
||||
// Resolve sorting.
|
||||
$sortRaw = [];
|
||||
foreach ($sorts as $column => $data) {
|
||||
$nested = Arr::get($data, 'direction');
|
||||
if (Arr::get($data, 'isString') === true) {
|
||||
if ($relation = Arr::get($data, 'relation')) {
|
||||
$column = $relation.'.'.$column.'_keyword';
|
||||
|
||||
$nested = [
|
||||
'order' => $nested,
|
||||
'nested' => [
|
||||
'path' => $relation,
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$column .= '.keyword';
|
||||
}
|
||||
}
|
||||
|
||||
$sortRaw[$column] = $nested;
|
||||
}
|
||||
|
||||
if ($sortRaw !== []) {
|
||||
$this->elasticBuilder->sortRaw($sortRaw);
|
||||
}
|
||||
|
||||
return $this->elasticBuilder->paginate($perPage, page: $page)->onlyModels();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Model> $model
|
||||
*/
|
||||
public static function elasticQuery(string $model): ElasticQuery
|
||||
{
|
||||
return match ($model) {
|
||||
Playlist::class => new PlaylistQuery(),
|
||||
AnimeThemeEntry::class => new EntryQuery(),
|
||||
AnimeSynonym::class => new AnimeSynonymQuery(),
|
||||
AnimeTheme::class => new ThemeQuery(),
|
||||
Anime::class => new AnimeQuery(),
|
||||
Artist::class => new ArtistQuery(),
|
||||
Series::class => new SeriesQuery(),
|
||||
Song::class => new SongQuery(),
|
||||
Studio::class => new StudioQuery(),
|
||||
Synonym::class => new SynonymQuery(),
|
||||
Video::class => new VideoQuery(),
|
||||
default => throw new RuntimeException("No ElasticQuery defined for model: {$model}"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Elasticsearch\Models\List;
|
||||
|
||||
use App\Models\List\ExternalProfile;
|
||||
|
||||
class ExternalProfileElasticModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(ExternalProfile $profile): array
|
||||
{
|
||||
return $profile->attributesToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Elasticsearch\Models\List;
|
||||
|
||||
use App\Models\List\Playlist;
|
||||
|
||||
class PlaylistElasticModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Playlist $playlist): array
|
||||
{
|
||||
return $playlist->attributesToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Elasticsearch\Models\Wiki\Anime;
|
||||
|
||||
use App\Models\Wiki\Anime\AnimeSynonym;
|
||||
|
||||
class AnimeSynonymElasticModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(AnimeSynonym $synonym): array
|
||||
{
|
||||
return $synonym->attributesToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Elasticsearch\Models\Wiki\Anime;
|
||||
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
|
||||
class AnimeThemeElasticModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(AnimeTheme $theme): array
|
||||
{
|
||||
$array = [
|
||||
...$theme->attributesToArray(),
|
||||
'anime' => $theme->anime->attributesToArray(),
|
||||
];
|
||||
|
||||
if ($theme->song !== null) {
|
||||
$array['song'] = $theme->song->toSearchableArray();
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Elasticsearch\Models\Wiki\Anime\Theme;
|
||||
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AnimeThemeEntryElasticModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(AnimeThemeEntry $entry): array
|
||||
{
|
||||
return [
|
||||
...$entry->attributesToArray(),
|
||||
'theme' => $entry->animetheme->toSearchableArray(),
|
||||
'version' => Str::of(strval($entry->version))->prepend('v')->__toString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Elasticsearch\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Synonym;
|
||||
|
||||
class AnimeElasticModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Anime $anime): array
|
||||
{
|
||||
return [
|
||||
...$anime->attributesToArray(),
|
||||
'synonyms' => $anime->synonyms->map(fn (Synonym $synonym) => $synonym->text)->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Elasticsearch\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Artist;
|
||||
use App\Models\Wiki\Song\Membership;
|
||||
use App\Models\Wiki\Song\Performance;
|
||||
use App\Models\Wiki\Synonym;
|
||||
|
||||
class ArtistElasticModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Artist $artist): array
|
||||
{
|
||||
return [
|
||||
...$artist->attributesToArray(),
|
||||
'synonyms' => $artist->synonyms->map(fn (Synonym $synonym) => $synonym->text)->all(),
|
||||
'as' => $artist->performances->map(fn (Performance $performance) => $performance->as)
|
||||
->toBase()
|
||||
->concat($artist->memberships->map(fn (Membership $membership) => $membership->as))
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Elasticsearch\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Series;
|
||||
|
||||
class SeriesElasticModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Series $series): array
|
||||
{
|
||||
return [
|
||||
...$series->attributesToArray(),
|
||||
'anime' => $series->anime->map(
|
||||
fn (Anime $anime): array => $anime->toSearchableArray()
|
||||
)->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Elasticsearch\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Song;
|
||||
|
||||
class SongElasticModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Song $song): array
|
||||
{
|
||||
return $song->attributesToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Elasticsearch\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Studio;
|
||||
|
||||
class StudioElasticModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Studio $studio): array
|
||||
{
|
||||
return $studio->attributesToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Elasticsearch\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Synonym;
|
||||
|
||||
class SynonymElasticModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Synonym $synonym): array
|
||||
{
|
||||
return $synonym->attributesToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Elasticsearch\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use App\Models\Wiki\Video;
|
||||
|
||||
class VideoElasticModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Video $video): array
|
||||
{
|
||||
return [
|
||||
...$video->attributesToArray(),
|
||||
'entries' => $video->animethemeentries->map(
|
||||
fn (AnimeThemeEntry $entry): array => $entry->toSearchableArray()
|
||||
)->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+35
-2
@@ -7,22 +7,55 @@ namespace App\Scout;
|
||||
use App\Enums\Http\Api\Paging\PaginationStrategy;
|
||||
use App\Http\Api\Query\Query;
|
||||
use App\Http\Api\Schema\EloquentSchema;
|
||||
use App\Scout\Collection\CollectionSearch;
|
||||
use App\Scout\Elasticsearch\Elasticsearch;
|
||||
use App\Scout\Typesense\Typesense;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Pagination\Paginator;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use RuntimeException;
|
||||
|
||||
abstract class Search
|
||||
{
|
||||
public function __construct(protected Criteria $criteria) {}
|
||||
|
||||
/**
|
||||
* @param class-string<Model>|Model $model
|
||||
*/
|
||||
public static function getSearch(Model|string $model, Criteria $criteria): Search
|
||||
{
|
||||
$model = $model instanceof Model ? $model : new $model;
|
||||
|
||||
return match ($driver = Config::get('scout.driver')) {
|
||||
'collection' => App::make(CollectionSearch::class, ['model' => $model, 'criteria' => $criteria]),
|
||||
'elastic' => App::make(Elasticsearch::class, ['criteria' => $criteria]),
|
||||
'typesense' => App::make(Typesense::class, ['model' => $model, 'criteria' => $criteria]),
|
||||
default => throw new RuntimeException("Unsupported {$driver} search driver configured."),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the search be performed?
|
||||
*/
|
||||
abstract public function shouldSearch(Query $query): bool;
|
||||
abstract public function shouldSearch(): bool;
|
||||
|
||||
/**
|
||||
* Perform the search.
|
||||
*/
|
||||
abstract public function search(
|
||||
abstract public function searchViaJSONAPI(
|
||||
Query $query,
|
||||
EloquentSchema $schema,
|
||||
PaginationStrategy $paginationStrategy
|
||||
): Collection|Paginator;
|
||||
|
||||
/**
|
||||
* @param Closure(EloquentBuilder): mixed $callback
|
||||
* @param array<string, array{direction: string, isString: bool, relation: ?string}> $sorts
|
||||
*/
|
||||
abstract public function search(?Closure $callback = null, int $perPage = 15, int $page = 1, array $sorts = []): LengthAwarePaginator;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Typesense\Models\List;
|
||||
|
||||
use App\Models\List\ExternalProfile;
|
||||
|
||||
class ExternalProfileTypesenseModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(ExternalProfile $profile): array
|
||||
{
|
||||
return [
|
||||
...$profile->attributesToArray(),
|
||||
'id' => (string) $profile->getKey(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Typesense\Models\List;
|
||||
|
||||
use App\Models\List\Playlist;
|
||||
|
||||
class PlaylistTypesenseModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Playlist $playlist): array
|
||||
{
|
||||
return [
|
||||
...$playlist->attributesToArray(),
|
||||
'id' => (string) $playlist->getKey(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Typesense\Models\Wiki\Anime;
|
||||
|
||||
use App\Models\Wiki\Anime\AnimeSynonym;
|
||||
|
||||
class AnimeSynonymTypesenseModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(AnimeSynonym $synonym): array
|
||||
{
|
||||
return [
|
||||
...$synonym->attributesToArray(),
|
||||
'id' => (string) $synonym->getKey(),
|
||||
'created_at' => $synonym->created_at?->timestamp,
|
||||
'updated_at' => $synonym->updated_at?->timestamp,
|
||||
'deleted_at' => $synonym->deleted_at?->timestamp,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Typesense\Models\Wiki\Anime;
|
||||
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
|
||||
class AnimeThemeTypesenseModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(AnimeTheme $theme): array
|
||||
{
|
||||
return [
|
||||
'id' => (string) $theme->getKey(),
|
||||
'created_at' => $theme->created_at?->timestamp,
|
||||
|
||||
'type_sequence' => $theme->type->localize().($theme->sequence ?? 1),
|
||||
'type' => $theme->type->localize(),
|
||||
'sequence' => (string) ($theme->sequence ?? 1),
|
||||
|
||||
'anime' => $theme->anime->toSearchableArray(),
|
||||
'song' => $theme->song?->toSearchableArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Typesense\Models\Wiki\Anime\Theme;
|
||||
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AnimeThemeEntryTypesenseModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(AnimeThemeEntry $entry): array
|
||||
{
|
||||
return [
|
||||
...$entry->attributesToArray(),
|
||||
'id' => (string) $entry->getKey(),
|
||||
'created_at' => $entry->created_at?->timestamp,
|
||||
'animetheme' => $entry->animetheme->toSearchableArray(),
|
||||
'version' => $version = Str::of(strval($entry->version))->prepend('v')->__toString(),
|
||||
'type_sequence_version' => $entry->animetheme->type->localize().(($entry->animetheme->sequence ?? 1)).$version,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Typesense\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Synonym;
|
||||
|
||||
class AnimeTypesenseModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Anime $anime): array
|
||||
{
|
||||
return [
|
||||
...$anime->attributesToArray(),
|
||||
'id' => (string) $anime->getKey(),
|
||||
'season' => $anime->season?->localize(),
|
||||
'created_at' => $anime->created_at?->timestamp,
|
||||
'updated_at' => $anime->updated_at?->timestamp,
|
||||
'deleted_at' => $anime->deleted_at?->timestamp,
|
||||
'synonyms' => $anime->synonyms->map(fn (Synonym $synonym) => $synonym->text)->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Typesense\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Artist;
|
||||
use App\Models\Wiki\Song\Membership;
|
||||
use App\Models\Wiki\Song\Performance;
|
||||
use App\Models\Wiki\Synonym;
|
||||
|
||||
class ArtistTypesenseModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Artist $artist): array
|
||||
{
|
||||
return [
|
||||
'id' => (string) $artist->getKey(),
|
||||
'name' => $artist->name,
|
||||
'created_at' => $artist->created_at?->timestamp,
|
||||
'updated_at' => $artist->updated_at?->timestamp,
|
||||
'deleted_at' => $artist->deleted_at?->timestamp,
|
||||
'synonyms' => $synonyms = $artist->synonyms->map(fn (Synonym $synonym) => $synonym->text)->all(),
|
||||
'as' => $as = $artist->performances->map(fn (Performance $performance) => $performance->as)
|
||||
->toBase()
|
||||
->concat($artist->memberships->map(fn (Membership $membership) => $membership->as))
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all(),
|
||||
'search_text' => implode(' ', [
|
||||
$artist->name,
|
||||
...$synonyms,
|
||||
// ...$as,
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Typesense\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Series;
|
||||
|
||||
class SeriesTypesenseModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Series $series): array
|
||||
{
|
||||
return [
|
||||
...$series->attributesToArray(),
|
||||
'id' => (string) $series->getKey(),
|
||||
'created_at' => $series->created_at?->timestamp,
|
||||
'anime' => $series->anime->map(
|
||||
fn (Anime $anime): array => $anime->toSearchableArray()
|
||||
)->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Typesense\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Song;
|
||||
|
||||
class SongTypesenseModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Song $song): array
|
||||
{
|
||||
return [
|
||||
...$song->attributesToArray(),
|
||||
'id' => (string) $song->getKey(),
|
||||
'created_at' => $song->created_at?->timestamp,
|
||||
'updated_at' => $song->updated_at?->timestamp,
|
||||
'deleted_at' => $song->deleted_at?->timestamp,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Typesense\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Studio;
|
||||
|
||||
class StudioTypesenseModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Studio $studio): array
|
||||
{
|
||||
return [
|
||||
...$studio->attributesToArray(),
|
||||
'id' => (string) $studio->getKey(),
|
||||
'created_at' => $studio->created_at?->timestamp,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Typesense\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Synonym;
|
||||
|
||||
class SynonymTypesenseModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Synonym $synonym): array
|
||||
{
|
||||
return [
|
||||
...$synonym->attributesToArray(),
|
||||
'id' => (string) $synonym->getKey(),
|
||||
'created_at' => $synonym->created_at?->timestamp,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Typesense\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use App\Models\Wiki\Video;
|
||||
|
||||
class VideoTypesenseModel
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function toSearchableArray(Video $video): array
|
||||
{
|
||||
return [
|
||||
...$video->attributesToArray(),
|
||||
'id' => (string) $video->getKey(),
|
||||
'created_at' => $video->created_at?->timestamp,
|
||||
'updated_at' => $video->updated_at?->timestamp,
|
||||
'deleted_at' => $video->deleted_at?->timestamp,
|
||||
'entries' => $video->animethemeentries->map(
|
||||
fn (AnimeThemeEntry $entry): array => $entry->toSearchableArray()
|
||||
)->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scout\Typesense;
|
||||
|
||||
use App\Enums\Http\Api\Paging\PaginationStrategy;
|
||||
use App\Http\Api\Query\Query;
|
||||
use App\Http\Api\Schema\EloquentSchema;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Search;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Pagination\Paginator;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Typesense extends Search
|
||||
{
|
||||
public function __construct(protected Model $model, Criteria $criteria)
|
||||
{
|
||||
parent::__construct($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the search.
|
||||
*/
|
||||
public function searchViaJSONAPI(
|
||||
Query $query,
|
||||
EloquentSchema $schema,
|
||||
PaginationStrategy $paginationStrategy
|
||||
): Collection|Paginator {
|
||||
// TODO
|
||||
return $this->search();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure(EloquentBuilder): void $callback
|
||||
* @param array<string, array{direction: string, isString: bool, relation: ?string}> $sorts
|
||||
*/
|
||||
public function search(?Closure $callback = null, int $perPage = 15, int $page = 1, array $sorts = []): LengthAwarePaginator
|
||||
{
|
||||
/** @phpstan-ignore-next-line */
|
||||
return $this->model::search($this->criteria->getTerm())
|
||||
->query($callback)
|
||||
->paginate($perPage, page: $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the search be performed?
|
||||
*/
|
||||
public function shouldSearch(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Search\Drivers;
|
||||
|
||||
use App\Contracts\Search\SearchDriver;
|
||||
use App\Search\Criteria;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Pagination\Paginator;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Scout\Builder;
|
||||
|
||||
class CollectionDriver implements SearchDriver
|
||||
{
|
||||
protected Builder $builder;
|
||||
|
||||
protected int $perPage = 15;
|
||||
protected int $page = 1;
|
||||
|
||||
protected function __construct(protected Model $model) {}
|
||||
|
||||
public static function search(Model $model, Criteria $criteria): SearchDriver
|
||||
{
|
||||
$builder = new self($model);
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
$builder->builder = $model::search($criteria->getTerm());
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
public function withPagination(int $perPage = 15, int $page = 1): static
|
||||
{
|
||||
$this->perPage = $perPage;
|
||||
$this->page = $page;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{direction: string, isString: bool, relation: ?string}> $sorts
|
||||
*/
|
||||
public function withSort(array $sorts): static
|
||||
{
|
||||
foreach ($sorts as $column => $data) {
|
||||
$this->builder->orderBy($column, Arr::get($data, 'direction'));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a callback through the Eloquent query.
|
||||
*
|
||||
* @param Closure(EloquentBuilder): void $callback
|
||||
*/
|
||||
public function passToEloquentBuilder(Closure $callback): SearchDriver
|
||||
{
|
||||
$this->builder->query($callback);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the search and get the resulting models.
|
||||
*/
|
||||
public function execute(): Paginator
|
||||
{
|
||||
return $this->builder
|
||||
->paginate($this->perPage, page: $this->page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the keys of the retrieved models.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function keys(): array
|
||||
{
|
||||
return Arr::pluck($this->execute()->items(), fn (Model $model) => $model->getKey());
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Search\Drivers;
|
||||
|
||||
use App\Contracts\Search\SearchDriver;
|
||||
use App\Models\List\Playlist;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Anime\AnimeSynonym;
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use App\Models\Wiki\Artist;
|
||||
use App\Models\Wiki\Series;
|
||||
use App\Models\Wiki\Song;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Models\Wiki\Synonym;
|
||||
use App\Models\Wiki\Video;
|
||||
use App\Scout\Elasticsearch\Api\Query\ElasticQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\List\PlaylistQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\Anime\SynonymQuery as AnimeSynonymQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\Anime\Theme\EntryQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\Anime\ThemeQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\AnimeQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\ArtistQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\SeriesQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\SongQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\StudioQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\SynonymQuery;
|
||||
use App\Scout\Elasticsearch\Api\Query\Wiki\VideoQuery;
|
||||
use App\Search\Criteria;
|
||||
use Closure;
|
||||
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
|
||||
use Elastic\ScoutDriverPlus\Paginator;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
use RuntimeException;
|
||||
|
||||
class ElasticsearchDriver implements SearchDriver
|
||||
{
|
||||
public SearchParametersBuilder $builder;
|
||||
|
||||
protected int $perPage = 15;
|
||||
protected int $page = 1;
|
||||
|
||||
protected function __construct(protected Model $model) {}
|
||||
|
||||
public static function search(Model $model, Criteria $criteria): SearchDriver
|
||||
{
|
||||
$builder = new self($model);
|
||||
|
||||
$builder->builder = static::elasticQuery($model::class)
|
||||
->build($criteria);
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
public function withPagination(int $perPage, int $page = 1): static
|
||||
{
|
||||
$this->perPage = $perPage;
|
||||
$this->page = $page;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{direction: string, isString: bool, relation: ?string}> $sorts
|
||||
*/
|
||||
public function withSort(array $sorts): static
|
||||
{
|
||||
$sortRaw = [];
|
||||
foreach ($sorts as $column => $data) {
|
||||
$nested = Arr::get($data, 'direction');
|
||||
if (Arr::get($data, 'isString') === true) {
|
||||
if ($relation = Arr::get($data, 'relation')) {
|
||||
$column = $relation.'.'.$column.'_keyword';
|
||||
|
||||
$nested = [
|
||||
'order' => $nested,
|
||||
'nested' => [
|
||||
'path' => $relation,
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$column .= '.keyword';
|
||||
}
|
||||
}
|
||||
|
||||
$sortRaw[$column] = $nested;
|
||||
}
|
||||
|
||||
if ($sortRaw !== []) {
|
||||
$this->builder->sortRaw($sortRaw);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a callback through the Eloquent query.
|
||||
*
|
||||
* @param Closure(EloquentBuilder): void $callback
|
||||
*/
|
||||
public function passToEloquentBuilder(Closure $callback): static
|
||||
{
|
||||
$this->builder->refineModels($callback);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the search and get the resulting models.
|
||||
*/
|
||||
public function execute(): Paginator
|
||||
{
|
||||
return $this->builder->paginate($this->perPage, page: $this->page)->onlyModels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the keys of the retrieved models.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function keys(): array
|
||||
{
|
||||
return $this
|
||||
->execute()
|
||||
->getCollection()
|
||||
->map(fn (Model $model) => $model->getKey())
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Model> $model
|
||||
*/
|
||||
public static function elasticQuery(string $model): ElasticQuery
|
||||
{
|
||||
return match ($model) {
|
||||
Playlist::class => new PlaylistQuery(),
|
||||
AnimeThemeEntry::class => new EntryQuery(),
|
||||
AnimeSynonym::class => new AnimeSynonymQuery(),
|
||||
AnimeTheme::class => new ThemeQuery(),
|
||||
Anime::class => new AnimeQuery(),
|
||||
Artist::class => new ArtistQuery(),
|
||||
Series::class => new SeriesQuery(),
|
||||
Song::class => new SongQuery(),
|
||||
Studio::class => new StudioQuery(),
|
||||
Synonym::class => new SynonymQuery(),
|
||||
Video::class => new VideoQuery(),
|
||||
default => throw new RuntimeException("No ElasticQuery defined for model: {$model}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Search;
|
||||
|
||||
use App\Contracts\Search\SearchDriver;
|
||||
use App\Search\Drivers\CollectionDriver;
|
||||
use App\Search\Drivers\ElasticsearchDriver;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use RuntimeException;
|
||||
|
||||
class Search
|
||||
{
|
||||
/**
|
||||
* @param class-string<Model>|Model $model
|
||||
*/
|
||||
public static function search(Model|string $model, Criteria $criteria): SearchDriver
|
||||
{
|
||||
$model = $model instanceof Model ? $model : new $model;
|
||||
|
||||
return match ($driver = Config::get('scout.driver')) {
|
||||
'elastic' => ElasticsearchDriver::search($model, $criteria),
|
||||
'collection' => CollectionDriver::search($model, $criteria),
|
||||
default => throw new RuntimeException("Unsupported {$driver} search driver configured."),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@
|
||||
"staudenmeir/laravel-adjacency-list": "^1.26.0",
|
||||
"symfony/http-client": "^6.4.34",
|
||||
"symfony/mailgun-mailer": "^6.4.24",
|
||||
"typesense/typesense-php": "^6.0",
|
||||
"vinkla/hashids": "^14.0"
|
||||
},
|
||||
"require-dev": {
|
||||
|
||||
Generated
+345
-1
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "a746bf2d1aad42b8f36655341a25a2ef",
|
||||
"content-hash": "e7e00007562758a8482a3967c7bfbf5c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "ajcastro/eager-load-pivot-relations",
|
||||
@@ -1221,6 +1221,72 @@
|
||||
],
|
||||
"time": "2026-03-20T21:10:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "clue/stream-filter",
|
||||
"version": "v1.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/clue/stream-filter.git",
|
||||
"reference": "049509fef80032cb3f051595029ab75b49a3c2f7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7",
|
||||
"reference": "049509fef80032cb3f051595029ab75b49a3c2f7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Clue\\StreamFilter\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"description": "A simple and modern approach to stream filtering in PHP",
|
||||
"homepage": "https://github.com/clue/stream-filter",
|
||||
"keywords": [
|
||||
"bucket brigade",
|
||||
"callback",
|
||||
"filter",
|
||||
"php_user_filter",
|
||||
"stream",
|
||||
"stream_filter_append",
|
||||
"stream_filter_register"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/clue/stream-filter/issues",
|
||||
"source": "https://github.com/clue/stream-filter/tree/v1.7.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://clue.engineering/support",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/clue",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-12-20T15:40:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "danharrin/date-format-converter",
|
||||
"version": "v0.3.1",
|
||||
@@ -6637,6 +6703,73 @@
|
||||
},
|
||||
"time": "2025-09-24T15:06:41+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/client-common",
|
||||
"version": "2.7.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-http/client-common.git",
|
||||
"reference": "dcc6de29c90dd74faab55f71b79d89409c4bf0c1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-http/client-common/zipball/dcc6de29c90dd74faab55f71b79d89409c4bf0c1",
|
||||
"reference": "dcc6de29c90dd74faab55f71b79d89409c4bf0c1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0",
|
||||
"php-http/httplug": "^2.0",
|
||||
"php-http/message": "^1.6",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"psr/http-message": "^1.0 || ^2.0",
|
||||
"symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/polyfill-php80": "^1.17"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/instantiator": "^1.1",
|
||||
"guzzlehttp/psr7": "^1.4",
|
||||
"nyholm/psr7": "^1.2",
|
||||
"phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-json": "To detect JSON responses with the ContentTypePlugin",
|
||||
"ext-libxml": "To detect XML responses with the ContentTypePlugin",
|
||||
"php-http/cache-plugin": "PSR-6 Cache plugin",
|
||||
"php-http/logger-plugin": "PSR-3 Logger plugin",
|
||||
"php-http/stopwatch-plugin": "Symfony Stopwatch plugin"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Http\\Client\\Common\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Márk Sági-Kazár",
|
||||
"email": "mark.sagikazar@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Common HTTP Client implementations and tools for HTTPlug",
|
||||
"homepage": "http://httplug.io",
|
||||
"keywords": [
|
||||
"client",
|
||||
"common",
|
||||
"http",
|
||||
"httplug"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-http/client-common/issues",
|
||||
"source": "https://github.com/php-http/client-common/tree/2.7.3"
|
||||
},
|
||||
"time": "2025-11-29T19:12:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/discovery",
|
||||
"version": "1.20.0",
|
||||
@@ -6773,6 +6906,75 @@
|
||||
},
|
||||
"time": "2024-09-23T11:39:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/message",
|
||||
"version": "1.16.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-http/message.git",
|
||||
"reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a",
|
||||
"reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"clue/stream-filter": "^1.5",
|
||||
"php": "^7.2 || ^8.0",
|
||||
"psr/http-message": "^1.1 || ^2.0"
|
||||
},
|
||||
"provide": {
|
||||
"php-http/message-factory-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ergebnis/composer-normalize": "^2.6",
|
||||
"ext-zlib": "*",
|
||||
"guzzlehttp/psr7": "^1.0 || ^2.0",
|
||||
"laminas/laminas-diactoros": "^2.0 || ^3.0",
|
||||
"php-http/message-factory": "^1.0.2",
|
||||
"phpspec/phpspec": "^5.1 || ^6.3 || ^7.1",
|
||||
"slim/slim": "^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-zlib": "Used with compressor/decompressor streams",
|
||||
"guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories",
|
||||
"laminas/laminas-diactoros": "Used with Diactoros Factories",
|
||||
"slim/slim": "Used with Slim Framework PSR-7 implementation"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/filters.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Márk Sági-Kazár",
|
||||
"email": "mark.sagikazar@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "HTTP Message related tools",
|
||||
"homepage": "http://php-http.org",
|
||||
"keywords": [
|
||||
"http",
|
||||
"message",
|
||||
"psr-7"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-http/message/issues",
|
||||
"source": "https://github.com/php-http/message/tree/1.16.2"
|
||||
},
|
||||
"time": "2024-10-02T11:34:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/promise",
|
||||
"version": "1.3.1",
|
||||
@@ -10155,6 +10357,77 @@
|
||||
],
|
||||
"time": "2026-03-06T13:17:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
"version": "v8.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/options-resolver.git",
|
||||
"reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
|
||||
"reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"symfony/deprecation-contracts": "^2.5|^3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\OptionsResolver\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides an improved replacement for the array_replace PHP function",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"config",
|
||||
"configuration",
|
||||
"options"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/options-resolver/tree/v8.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-12T15:55:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.33.0",
|
||||
@@ -11973,6 +12246,77 @@
|
||||
},
|
||||
"time": "2025-12-02T11:56:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "typesense/typesense-php",
|
||||
"version": "v6.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/typesense/typesense-php.git",
|
||||
"reference": "e113f90449e159316a433a5509205939f271c5e2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/typesense/typesense-php/zipball/e113f90449e159316a433a5509205939f271c5e2",
|
||||
"reference": "e113f90449e159316a433a5509205939f271c5e2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"monolog/monolog": "^2.1 || ^3.0 || ^3.3",
|
||||
"nyholm/psr7": "^1.3",
|
||||
"php": ">=7.4",
|
||||
"php-http/client-common": "^1.0 || ^2.3",
|
||||
"php-http/discovery": "^1.0",
|
||||
"php-http/httplug": "^1.0 || ^2.2",
|
||||
"psr/http-client-implementation": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"psr/http-message": "^1.0 || ^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.6",
|
||||
"phpunit/phpunit": "^11.2",
|
||||
"squizlabs/php_codesniffer": "3.*",
|
||||
"symfony/http-client": "^5.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Typesense\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Typesense",
|
||||
"email": "contact@typesense.org",
|
||||
"homepage": "https://typesense.org",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Abdullah Al-Faqeir",
|
||||
"email": "abdullah@devloops.net",
|
||||
"homepage": "https://www.devloops.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "PHP client for Typesense Search Server: https://github.com/typesense/typesense",
|
||||
"homepage": "https://github.com/typesense/typesense-php",
|
||||
"support": {
|
||||
"docs": "https://typesense.org/api",
|
||||
"issues": "https://github.com/typesense/typesense-php/issues",
|
||||
"source": "https://github.com/typesense/typesense-php"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/typesense",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-22T20:51:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ueberdosis/tiptap-php",
|
||||
"version": "2.1.0",
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\List\ExternalProfile;
|
||||
use App\Models\List\Playlist;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Anime\AnimeSynonym;
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use App\Models\Wiki\Artist;
|
||||
use App\Models\Wiki\Series;
|
||||
use App\Models\Wiki\Song;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Models\Wiki\Synonym;
|
||||
use App\Models\Wiki\Video;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
@@ -141,4 +154,411 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Typesense Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Typesense settings. Typesense is an open
|
||||
| source search engine using minimal configuration. Below, you will
|
||||
| state the host, key, and schema configuration for the instance.
|
||||
|
|
||||
*/
|
||||
|
||||
'typesense' => [
|
||||
'client-settings' => [
|
||||
'api_key' => env('TYPESENSE_API_KEY', 'xyz'),
|
||||
'nodes' => [
|
||||
[
|
||||
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||
'port' => env('TYPESENSE_PORT', '8108'),
|
||||
'path' => env('TYPESENSE_PATH', ''),
|
||||
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||
],
|
||||
],
|
||||
'nearest_node' => [
|
||||
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||
'port' => env('TYPESENSE_PORT', '8108'),
|
||||
'path' => env('TYPESENSE_PATH', ''),
|
||||
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||
],
|
||||
'connection_timeout_seconds' => env('TYPESENSE_CONNECTION_TIMEOUT_SECONDS', 2),
|
||||
'healthcheck_interval_seconds' => env('TYPESENSE_HEALTHCHECK_INTERVAL_SECONDS', 30),
|
||||
'num_retries' => env('TYPESENSE_NUM_RETRIES', 3),
|
||||
'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1),
|
||||
],
|
||||
'model-settings' => [
|
||||
ExternalProfile::class => [
|
||||
'collection-schema' => [
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'id',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'name',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
],
|
||||
'search-parameters' => [
|
||||
'query_by' => 'name',
|
||||
],
|
||||
],
|
||||
Playlist::class => [
|
||||
'collection-schema' => [
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'id',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'name',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
],
|
||||
'search-parameters' => [
|
||||
'query_by' => 'name',
|
||||
],
|
||||
],
|
||||
Anime::class => [
|
||||
'collection-schema' => [
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'id',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'name',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'season',
|
||||
'type' => 'string',
|
||||
'optional' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'year',
|
||||
'type' => 'int32',
|
||||
],
|
||||
[
|
||||
'name' => 'created_at',
|
||||
'type' => 'int64',
|
||||
'optional' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'updated_at',
|
||||
'type' => 'int64',
|
||||
'optional' => true,
|
||||
],
|
||||
[
|
||||
'name' => '__soft_deleted',
|
||||
'type' => 'int32',
|
||||
'optional' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'synonyms',
|
||||
'type' => 'string[]',
|
||||
],
|
||||
],
|
||||
],
|
||||
'search-parameters' => [
|
||||
'query_by' => 'name,synonyms',
|
||||
],
|
||||
],
|
||||
AnimeTheme::class => [
|
||||
'collection-schema' => [
|
||||
'enable_nested_fields' => true,
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'id',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'created_at',
|
||||
'type' => 'int64',
|
||||
'optional' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'type',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'sequence',
|
||||
'type' => 'string',
|
||||
'optional' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'type_sequence',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'anime',
|
||||
'type' => 'object',
|
||||
],
|
||||
[
|
||||
'name' => 'song',
|
||||
'type' => 'object',
|
||||
'optional' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'search-parameters' => [
|
||||
'query_by' => implode(',', [
|
||||
'song.title',
|
||||
'song.title_native',
|
||||
'anime.name',
|
||||
'anime.synonyms',
|
||||
'type_sequence',
|
||||
]),
|
||||
'query_by_weights' => '10,8,6,5,5',
|
||||
],
|
||||
],
|
||||
AnimeThemeEntry::class => [
|
||||
'collection-schema' => [
|
||||
'enable_nested_fields' => true,
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'id',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'created_at',
|
||||
'type' => 'int64',
|
||||
'optional' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'version',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'type_sequence_version',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'animetheme',
|
||||
'type' => 'object',
|
||||
],
|
||||
],
|
||||
],
|
||||
'search-parameters' => [
|
||||
'query_by' => implode(',', [
|
||||
'animetheme.song.title',
|
||||
'animetheme.song.title_native',
|
||||
'animetheme.anime.name',
|
||||
'animetheme.anime.synonyms',
|
||||
'type_sequence_version',
|
||||
]),
|
||||
'query_by_weights' => '10,8,6,5,5',
|
||||
],
|
||||
],
|
||||
Artist::class => [
|
||||
'collection-schema' => [
|
||||
'enable_nested_fields' => true,
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'id',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'name',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'synonyms',
|
||||
'type' => 'string[]',
|
||||
],
|
||||
[
|
||||
'name' => 'as',
|
||||
'type' => 'string[]',
|
||||
],
|
||||
[
|
||||
'name' => 'created_at',
|
||||
'type' => 'int64',
|
||||
'optional' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'search_text',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
],
|
||||
'search-parameters' => [
|
||||
'query_by' => implode(',', [
|
||||
'name',
|
||||
'synonyms',
|
||||
'as',
|
||||
'search_text',
|
||||
]),
|
||||
'query_by_weights' => '10,8,3,1',
|
||||
'text_match_type' => 'sum_score',
|
||||
],
|
||||
],
|
||||
Series::class => [
|
||||
'collection-schema' => [
|
||||
'enable_nested_fields' => true,
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'id',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'name',
|
||||
'type' => 'string',
|
||||
'sort' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'created_at',
|
||||
'type' => 'int64',
|
||||
'optional' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'anime',
|
||||
'type' => 'object[]',
|
||||
],
|
||||
],
|
||||
],
|
||||
'search-parameters' => [
|
||||
'query_by' => implode(',', [
|
||||
'name',
|
||||
'anime.synonyms',
|
||||
]),
|
||||
'query_by_weights' => '10,8',
|
||||
],
|
||||
],
|
||||
Song::class => [
|
||||
'collection-schema' => [
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'id',
|
||||
'type' => 'string',
|
||||
'sort' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'title',
|
||||
'type' => 'string',
|
||||
'optional' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'title_native',
|
||||
'type' => 'string',
|
||||
'optional' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'created_at',
|
||||
'type' => 'int64',
|
||||
'optional' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'search-parameters' => [
|
||||
'query_by' => 'title,title_native',
|
||||
'query_by_weights' => '10,8',
|
||||
],
|
||||
],
|
||||
Studio::class => [
|
||||
'collection-schema' => [
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'id',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'name',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'created_at',
|
||||
'type' => 'int64',
|
||||
'optional' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'search-parameters' => [
|
||||
'query_by' => 'name',
|
||||
],
|
||||
],
|
||||
AnimeSynonym::class => [
|
||||
'collection-schema' => [
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'id',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'text',
|
||||
'type' => 'string',
|
||||
'sort' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'search-parameters' => [
|
||||
'query_by' => 'text',
|
||||
],
|
||||
],
|
||||
Synonym::class => [
|
||||
'collection-schema' => [
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'id',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'text',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'created_at',
|
||||
'type' => 'int64',
|
||||
'optional' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'search-parameters' => [
|
||||
'query_by' => 'text',
|
||||
],
|
||||
],
|
||||
Video::class => [
|
||||
'collection-schema' => [
|
||||
'enable_nested_fields' => true,
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'id',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'created_at',
|
||||
'type' => 'int64',
|
||||
'optional' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'filename',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'tags',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'name' => 'entries',
|
||||
'type' => 'object[]',
|
||||
],
|
||||
],
|
||||
],
|
||||
'search-parameters' => [
|
||||
'query_by' => implode(',', [
|
||||
'filename',
|
||||
'tags',
|
||||
'entries.animetheme.song.title',
|
||||
'entries.animetheme.song.title_native',
|
||||
'entries.animetheme.anime.name',
|
||||
'entries.animetheme.anime.synonyms',
|
||||
'entries.type_sequence_version',
|
||||
]),
|
||||
'query_by_weights' => '10,8,6,7,5,5,5',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -33,6 +33,9 @@ parameters:
|
||||
- '#Access to an undefined property App\\Models\\Wiki\\Artist::\$pivot.#'
|
||||
- "#BelongsToMany<.*,\\s*'pivot'> but returns .*BelongsToMany<.*,\\s*string>.#"
|
||||
- '#Call to method assertActionMounted\(\) on an unknown class static.#'
|
||||
-
|
||||
identifier: nullsafe.neverNull
|
||||
path: app/Scout/Typesense/Models/*
|
||||
-
|
||||
message: '#Call to an undefined method PHPUnit\\Framework\\TestCase::.*\(\)#'
|
||||
path: tests/*
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="SCOUT_QUEUE" value="false"/>
|
||||
<env name="SCOUT_DRIVER" value="null"/>
|
||||
<env name="SCOUT_DRIVER" value="collection"/>
|
||||
<env name="HORIZON_TEAM_ID" value="null"/>
|
||||
<env name="TEAM_CREATOR_ID" value="null"/>
|
||||
</php>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Http\Api\Parser\SearchParser;
|
||||
use App\Search\Criteria;
|
||||
use App\Scout\Criteria;
|
||||
|
||||
uses(Illuminate\Foundation\Testing\WithFaker::class);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Http\Api\Parser\FilterParser;
|
||||
use App\Http\Api\Parser\IncludeParser;
|
||||
use App\Http\Api\Parser\SearchParser;
|
||||
use App\Http\Api\Parser\SortParser;
|
||||
use App\Search\Criteria as SearchCriteria;
|
||||
use App\Scout\Criteria as SearchCriteria;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\Unit\Http\Api\Query\FakeQuery;
|
||||
|
||||
Reference in New Issue
Block a user