Files
SkinbaseNova/app/Console/Commands/ForceIndexArtworkCommand.php

182 lines
7.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\Artwork;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Meilisearch\Client as MeilisearchClient;
/**
* Directly write a single artwork into the Meilisearch index, bypassing the queue.
*
* Useful when:
* - A rebuild was run but the queue worker was not consuming the `search` queue.
* - A specific artwork is missing from the live index and you want it visible immediately.
* - You need to force-push a corrected document after schema changes.
*
* Usage:
* php artisan artworks:search-force-index 69810
* php artisan artworks:search-force-index # interactive prompt
* php artisan artworks:search-force-index 69810 --dry-run
* php artisan artworks:search-force-index 69810 --force # index even if not public/approved/published
*/
final class ForceIndexArtworkCommand extends Command
{
protected $signature = 'artworks:search-force-index
{artwork_id? : The artwork ID to force-index}
{--index= : Override the Meilisearch index name}
{--dry-run : Show what would be sent without actually writing}
{--force : Index the document even when the artwork is not public/approved/published}
{--no-cache-bump : Skip bumping the explore cache version after indexing}';
protected $description = 'Directly push a single artwork into Meilisearch, bypassing the queue.';
public function handle(MeilisearchClient $client): int
{
$artworkId = $this->resolveArtworkId();
if ($artworkId === null) {
$this->error('An artwork ID is required.');
return self::FAILURE;
}
$isDryRun = (bool) $this->option('dry-run');
$forceIndex = (bool) $this->option('force');
$this->line(sprintf(
'%sForce-indexing artwork #%d into Meilisearch%s…',
$isDryRun ? '[DRY RUN] ' : '',
$artworkId,
$forceIndex ? ' (--force: eligibility check bypassed)' : '',
));
// ── 1. Load artwork with all relations required for toSearchableArray() ──
$artwork = Artwork::query()
->with(['user', 'group', 'tags', 'categories.contentType', 'stats', 'awardStat'])
->find($artworkId);
if ($artwork === null) {
$this->error("Artwork #{$artworkId} was not found in the database.");
return self::FAILURE;
}
$this->comment('Artwork');
$this->line(sprintf(
' id=%d title="%s" public=%s approved=%s published_at=%s',
(int) $artwork->id,
(string) ($artwork->title ?? ''),
$artwork->is_public ? 'yes' : 'no',
$artwork->is_approved ? 'yes' : 'no',
$artwork->published_at?->toIso8601String() ?? 'null',
));
// ── 2. Eligibility check ─────────────────────────────────────────────────
$shouldBeIndexed = $artwork->is_public && $artwork->is_approved && $artwork->published_at !== null;
if (! $shouldBeIndexed && ! $forceIndex) {
$this->warn(sprintf(
'Artwork #%d is not eligible for the public index (is_public=%s, is_approved=%s, published_at=%s). ' .
'Use --force to index it anyway, or fix the artwork status first.',
$artworkId,
$artwork->is_public ? 'true' : 'false',
$artwork->is_approved ? 'true' : 'false',
$artwork->published_at?->toIso8601String() ?? 'null',
));
return self::FAILURE;
}
if (! $shouldBeIndexed && $forceIndex) {
$this->warn('Artwork is not normally eligible but --force was passed; indexing anyway.');
}
// ── 3. Build the Meilisearch document ────────────────────────────────────
$document = $artwork->toSearchableArray();
$this->comment('Generated document');
$this->line(json_encode($document, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
$this->newLine();
// ── 4. Resolve index name ────────────────────────────────────────────────
$indexName = $this->resolveIndexName($artwork);
$this->line("Target index: {$indexName}");
if ($isDryRun) {
$this->info('[DRY RUN] Document was NOT written to Meilisearch. Remove --dry-run to execute.');
return self::SUCCESS;
}
// ── 5. Write directly to Meilisearch (no queue) ──────────────────────────
try {
$taskResult = $client->index($indexName)->addDocuments([$document]);
$taskUid = $taskResult['taskUid'] ?? $taskResult['uid'] ?? 'n/a';
$this->info(sprintf(
'Document written to Meilisearch. Task uid: %s',
is_scalar($taskUid) ? (string) $taskUid : json_encode($taskUid),
));
} catch (\Throwable $e) {
$this->error('Meilisearch write failed: ' . $e->getMessage());
return self::FAILURE;
}
// ── 6. Bump explore cache version ────────────────────────────────────────
if (! $this->option('no-cache-bump')) {
try {
$newVersion = ((int) Cache::get('explore.cache.version', 1)) + 1;
Cache::forever('explore.cache.version', $newVersion);
$this->line("Explore cache version bumped to {$newVersion}.");
} catch (\Throwable $e) {
$this->warn('Could not bump explore cache version: ' . $e->getMessage());
}
}
// ── 7. Summary ────────────────────────────────────────────────────────────
$this->newLine();
$this->info(sprintf(
'Artwork #%d ("%s") has been pushed to index "%s" directly.',
(int) $artwork->id,
(string) ($artwork->title ?? ''),
$indexName,
));
$this->line('The artwork should now appear on browse and search pages.');
$this->line('If Meilisearch was still processing the task you can verify with:');
$this->line(sprintf(' php artisan artworks:search-inspect %d', $artworkId));
return self::SUCCESS;
}
private function resolveArtworkId(): ?int
{
$argument = $this->argument('artwork_id');
if ($argument !== null && $argument !== '') {
return max(1, (int) $argument);
}
if (! $this->input->isInteractive()) {
return null;
}
$answer = $this->ask('Artwork ID');
if ($answer === null || trim($answer) === '') {
return null;
}
return max(1, (int) $answer);
}
private function resolveIndexName(Artwork $artwork): string
{
$override = trim((string) $this->option('index'));
if ($override !== '') {
return $override;
}
return $artwork->searchableAs();
}
}