182 lines
7.4 KiB
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();
|
|
}
|
|
}
|