feat: added anime synonyms to series scout search (#772)

This commit is contained in:
Kyrch
2025-01-10 01:48:54 -03:00
committed by GitHub
parent d609fbd68a
commit d45545e7e1
15 changed files with 193 additions and 84 deletions
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace App\Actions\Discord;
use App\Enums\Actions\Models\Wiki\Video\NotificationType;
use App\Enums\Actions\Models\Wiki\Video\ShouldForceThread;
use App\Models\Wiki\Video;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Arr;
@@ -27,7 +26,6 @@ class DiscordVideoNotificationAction
public function handle(Collection $videos, array $fields): void
{
$type = Arr::get($fields, NotificationType::getFieldKey());
$shouldForce = ShouldForceThread::from(intval(Arr::get($fields, ShouldForceThread::getFieldKey())));
$newVideos = [];
@@ -36,7 +34,7 @@ class DiscordVideoNotificationAction
->load([
'animethemeentries.animetheme.anime.discordthread',
'animethemeentries.animetheme.anime.images',
'animethemeentries.animetheme.group',
Video::RELATION_GROUP,
'animethemeentries.animetheme.song.artists',
]);
@@ -44,7 +42,6 @@ class DiscordVideoNotificationAction
$anime = $theme->anime;
if ($anime->discordthread === null) {
if ($shouldForce === ShouldForceThread::NO) return;
$threadAction = new DiscordThreadAction();
@@ -37,7 +37,7 @@ trait CanCreateExternalResource
$url = $site->formatResourceLink($model::class, intval($matches[2]), $matches[2], $matches[1]);
}
if ($id !== null) {
if ($id !== null && !$site->usesIdInLink()) {
$url = $site->formatResourceLink($model::class, intval($id), $id);
}
}
@@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Enums\Actions\Models\Wiki\Video;
use App\Concerns\Enums\LocalizesName;
/**
* Enum ShouldForceThread.
*/
enum ShouldForceThread: int
{
use LocalizesName;
case NO = 0;
case YES = 1;
/**
* Get the field key to use in the admin panel.
*
* @return string
*/
public static function getFieldKey(): string
{
return 'should-force-thread';
}
}
+15 -2
View File
@@ -292,12 +292,12 @@ enum ResourceSite: int
* Get the URL of the site for resources by determined model.
*
* @param class-string $modelClass
* @param int $id
* @param int|null $id
* @param string|null $slug
* @param string|null $type
* @return string|null
*/
public function formatResourceLink(string $modelClass, int $id, ?string $slug = null, ?string $type = null): ?string
public function formatResourceLink(string $modelClass, ?int $id = null, ?string $slug = null, ?string $type = null): ?string
{
if ($modelClass === Anime::class) {
return match ($this) {
@@ -362,6 +362,19 @@ enum ResourceSite: int
return null;
}
/**
* Determine whether the resource uses external id in the url.
*
* @return bool
*/
public function usesIdInLink(): bool
{
return match ($this) {
ResourceSite::ANIME_PLANET => false,
default => true,
};
}
/**
* Get the URL capture groups of the resource site.
*
@@ -6,7 +6,6 @@ namespace App\Filament\BulkActions\Models\Wiki\Video;
use App\Actions\Discord\DiscordVideoNotificationAction as DiscordVideoNotificationActionAction;
use App\Enums\Actions\Models\Wiki\Video\NotificationType;
use App\Enums\Actions\Models\Wiki\Video\ShouldForceThread;
use App\Filament\BulkActions\BaseBulkAction;
use App\Filament\Components\Fields\Select;
use App\Models\Discord\DiscordThread;
@@ -70,14 +69,6 @@ class VideoDiscordNotificationBulkAction extends BaseBulkAction
->default(NotificationType::ADDED->value)
->required()
->rules(['required', new Enum(NotificationType::class)]),
Select::make(ShouldForceThread::getFieldKey())
->label(__('filament.bulk_actions.discord.notification.should_force.name'))
->helperText(__('filament.bulk_actions.discord.notification.should_force.help'))
->options(ShouldForceThread::asSelectArray())
->default(ShouldForceThread::NO->value)
->required()
->rules(['required', new Enum(ShouldForceThread::class)]),
]);
}
}
@@ -8,7 +8,7 @@ use App\Contracts\Http\Api\InteractsWithSchema;
use App\Http\Api\Schema\Schema;
use App\Http\Controllers\Controller;
use App\Http\Middleware\Auth\Authenticate;
use App\Http\Middleware\Models\AuthorizesPivot;
use App\Http\Middleware\Models\Pivot\AuthorizesPivot;
use Illuminate\Support\Str;
/**
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Http\Middleware\Models;
namespace App\Http\Middleware\Models\Pivot;
use App\Models\Auth\User;
use Closure;
+28
View File
@@ -14,6 +14,7 @@ use App\Models\BaseModel;
use App\Pivots\Wiki\AnimeSeries;
use Database\Factories\Wiki\SeriesFactory;
use Elastic\ScoutDriverPlus\Searchable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Collection;
@@ -39,6 +40,7 @@ class Series extends BaseModel
final public const ATTRIBUTE_SLUG = 'slug';
final public const RELATION_ANIME = 'anime';
final public const RELATION_ANIME_SYNONYMS = 'anime.animesynonyms';
/**
* The attributes that are mass assignable.
@@ -78,6 +80,32 @@ class Series extends BaseModel
*/
protected $primaryKey = Series::ATTRIBUTE_ID;
/**
* Modify the query used to retrieve models when making all of the models searchable.
*
* @param Builder $query
* @return Builder
*/
protected function makeAllSearchableUsing(Builder $query): Builder
{
return $query->with(Series::RELATION_ANIME_SYNONYMS);
}
/**
* Get the indexable data array for the model.
*
* @return array
*/
public function toSearchableArray(): array
{
$array = $this->toArray();
$array['anime'] = $this->anime->map(
fn (Anime $anime) => $anime->toSearchableArray()
)->toArray();
return $array;
}
/**
* Get the route key for the model.
*
-5
View File
@@ -126,11 +126,6 @@ class ArtistPolicy extends BasePolicy
*/
public function attachArtist(User $user, Artist $artist, Artist $artist2): bool
{
if ($artist->is($artist2)) {
// An artist cannot be a member/group of themselves
return false;
}
$attached = ArtistMember::query()
->where(ArtistMember::ATTRIBUTE_ARTIST, $artist->getKey())
->where(ArtistMember::ATTRIBUTE_MEMBER, $artist2->getKey())
@@ -27,8 +27,8 @@ abstract class ElasticQuery
* - Matching at least one term (word) (x0.6)
* - Matching fuzzy (x0.4)
*
* @param string $field
* @param string $searchTerm
* @param string $field
* @param string $searchTerm
* @return array
*/
protected function createTextQuery(string $field, string $searchTerm): array
@@ -71,8 +71,8 @@ abstract class ElasticQuery
/**
* Helper function for raw queries. This will wrap queries in nested queries.
*
* @param string $nestedResource
* @param array $nestedQueries
* @param string $nestedResource
* @param array $nestedQueries
* @return array
*/
protected function createNestedQuery(string $nestedResource, array $nestedQueries): array
@@ -92,9 +92,9 @@ abstract class ElasticQuery
* Shorthand function for calling `$this->createNestedQuery()` with the output from
* `$this->createTextQuery()`.
*
* @param string $nestedResource
* @param string $field
* @param string $searchTerm
* @param string $nestedResource
* @param string $field
* @param string $searchTerm
* @return array
*/
protected function createNestedTextQuery(string $nestedResource, string $field, string $searchTerm): array
@@ -7,8 +7,6 @@ namespace App\Scout\Elasticsearch\Api\Query\Wiki;
use App\Http\Api\Criteria\Search\Criteria;
use App\Models\Wiki\Series;
use App\Scout\Elasticsearch\Api\Query\ElasticQuery;
use Elastic\ScoutDriverPlus\Builders\MatchPhraseQueryBuilder;
use Elastic\ScoutDriverPlus\Builders\MatchQueryBuilder;
use Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder;
use Elastic\ScoutDriverPlus\Support\Query;
@@ -26,26 +24,26 @@ class SeriesQuery extends ElasticQuery
public function build(Criteria $criteria): SearchParametersBuilder
{
$query = Query::bool()
->should(
new MatchPhraseQueryBuilder()
->field('name')
->query($criteria->getTerm())
)
->should(
new MatchQueryBuilder()
->field('name')
->query($criteria->getTerm())
->operator('AND')
)
->should(
new MatchQueryBuilder()
->field('name')
->query($criteria->getTerm())
->operator('AND')
->lenient(true)
->fuzziness('AUTO')
)
->minimumShouldMatch(1);
->mustRaw([
'dis_max' => [
'queries' => [
[
'bool' => [
'should' => $this->createTextQuery('name', $criteria->getTerm())
]
],
[
'bool' => [
'boost' => 0.6,
'should' => $this->createNestedQuery(
'anime',
$this->createNestedTextQuery('anime.synonyms', 'text', $criteria->getTerm())
)
]
]
]
]
]);
return Series::searchQuery($query);
}
+2
View File
@@ -7,6 +7,7 @@ namespace Database\Seeders;
use Database\Seeders\Admin\Feature\FeatureSeeder;
use Database\Seeders\Auth\Permission\PermissionSeeder;
use Database\Seeders\Auth\Role\RoleSeeder;
use Database\Seeders\Scout\ImportModelsSeeder;
use Database\Seeders\Wiki\Audio\AudioSeeder;
use Database\Seeders\Wiki\Video\VideoSeeder;
use Illuminate\Database\Seeder;
@@ -29,5 +30,6 @@ class DatabaseSeeder extends Seeder
$this->call(AudioSeeder::class);
$this->call(HashidsSeeder::class);
$this->call(FeatureSeeder::class);
$this->call(ImportModelsSeeder::class);
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Database\Seeders\Scout;
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\Video;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Config;
/**
* Class ImportModelsSeeder.
*/
class ImportModelsSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run(): void
{
$driver = Config::get('scout.driver');
if (empty($driver)) {
$this->command->info('No driver configured for Scout. Skipping models importing.');
return;
}
$this->scoutImport(Playlist::class);
$this->scoutImport(Anime::class);
$this->scoutImport(AnimeSynonym::class);
$this->scoutImport(AnimeTheme::class);
$this->scoutImport(AnimeThemeEntry::class);
$this->scoutImport(Artist::class);
$this->scoutImport(Series::class);
$this->scoutImport(Song::class);
$this->scoutImport(Studio::class);
$this->scoutImport(Video::class);
}
/**
* Call the scout import command for the given model.
*
* @param string $modelClass
* @return void
*/
private function scoutImport(string $modelClass): void
{
$this->command->info("Importing Models for {$modelClass}");
Artisan::call('scout:import', ['model' => $modelClass]);
}
}
@@ -34,6 +34,61 @@ final class CreateSeriesIndex implements MigrationInterface
],
]);
$mapping->date('updated_at');
$mapping->nested('anime', [
'properties' => [
'anime_id' => [
'type' => 'long',
],
'created_at' => [
'type' => 'date',
],
'name' => [
'type' => 'text',
'copy_to' => ['anime_slug'],
],
'season' => [
'type' => 'long',
],
'slug' => [
'type' => 'text',
],
'synonyms' => [
'type' => 'nested',
'properties' => [
'anime_id' => [
'type' => 'long',
],
'created_at' => [
'type' => 'date',
],
'synonym_id' => [
'type' => 'long',
],
'text' => [
'type' => 'text',
'copy_to' => ['synonym_slug'],
],
'type' => [
'type' => 'long',
],
'updated_at' => [
'type' => 'date',
],
],
],
'synopsis' => [
'type' => 'text',
],
'updated_at' => [
'type' => 'date',
],
'year' => [
'type' => 'long',
],
]
]);
$mapping->text('anime_slug');
$mapping->text('synonym_slug');
});
}
-4
View File
@@ -441,10 +441,6 @@ return [
'notification' => [
'icon' => 'heroicon-o-bell',
'name' => 'Create Discord Notification',
'should_force' => [
'help' => 'If yes, the thread will be created if it does not exist',
'name' => 'Should force thread?',
],
'should_send' => [
'help' => 'If yes, the notification will be created.',
'name' => 'Should send notification?',