option('dry-run'); if (! $dryRun && ! $client->isConfigured()) { $this->error('Vision vector gateway is not configured. Set VISION_VECTOR_GATEWAY_URL and VISION_VECTOR_GATEWAY_API_KEY.'); return self::FAILURE; } $startId = max(0, (int) $this->option('start-id')); $afterId = max(0, (int) $this->option('after-id')); $batch = max(1, min((int) $this->option('batch'), 1000)); $limit = max(0, (int) $this->option('limit')); $publicOnly = (bool) $this->option('public-only'); $nextId = $startId > 0 ? $startId : max(1, $afterId + 1); $processed = 0; $indexed = 0; $skipped = 0; $failed = 0; $lastId = $afterId; if ($startId > 0 && $afterId > 0) { $this->warn(sprintf( 'Both --start-id=%d and --after-id=%d were provided. Using --start-id and ignoring --after-id.', $startId, $afterId )); } $this->info(sprintf( 'Starting vector index: start_id=%d after_id=%d next_id=%d batch=%d limit=%s public_only=%s dry_run=%s', $startId, $afterId, $nextId, $batch, $limit > 0 ? (string) $limit : 'all', $publicOnly ? 'yes' : 'no', $dryRun ? 'yes' : 'no' )); while (true) { $remaining = $limit > 0 ? max(0, $limit - $processed) : $batch; if ($limit > 0 && $remaining === 0) { break; } $take = $limit > 0 ? min($batch, $remaining) : $batch; $query = Artwork::query() ->with(['categories' => fn ($categories) => $categories->with('contentType')->orderBy('sort_order')->orderBy('name')]) ->where('id', '>=', $nextId) ->whereNotNull('hash') ->orderBy('id') ->limit($take); if ($publicOnly) { $query->public()->published(); } $artworks = $query->get(); if ($artworks->isEmpty()) { $this->line('No more artworks matched the current query window.'); break; } $this->line(sprintf( 'Fetched batch: count=%d first_id=%d last_id=%d', $artworks->count(), (int) $artworks->first()->id, (int) $artworks->last()->id )); foreach ($artworks as $artwork) { $processed++; $lastId = (int) $artwork->id; $nextId = $lastId + 1; $url = $imageUrl->fromArtwork($artwork); if ($url === null) { $skipped++; $this->warn("Skipped artwork {$artwork->id}: no vision image URL could be generated."); continue; } $metadata = $this->metadataForArtwork($artwork); $this->line(sprintf( 'Processing artwork=%d hash=%s thumb_ext=%s url=%s metadata=%s', (int) $artwork->id, (string) ($artwork->hash ?? ''), (string) ($artwork->thumb_ext ?? ''), $url, $this->json($metadata) )); if ($dryRun) { $indexed++; $this->line(sprintf( '[dry] artwork=%d indexed=%d/%d', (int) $artwork->id, $indexed, $processed )); continue; } try { $client->upsertByUrl($url, (int) $artwork->id, $metadata); $indexed++; $this->info(sprintf( 'Indexed artwork %d successfully. totals: processed=%d indexed=%d skipped=%d failed=%d', (int) $artwork->id, $processed, $indexed, $skipped, $failed )); } catch (\Throwable $e) { $failed++; $this->warn("Failed artwork {$artwork->id}: {$e->getMessage()}"); } } } $this->info("Vector index finished. processed={$processed} indexed={$indexed} skipped={$skipped} failed={$failed} last_id={$lastId} next_id={$nextId}"); return $failed > 0 ? self::FAILURE : self::SUCCESS; } /** * @param array $payload */ private function json(array $payload): string { $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); return is_string($json) ? $json : '{}'; } /** * @return array{content_type: string, category: string, user_id: string} */ private function metadataForArtwork(Artwork $artwork): array { $category = $this->primaryCategory($artwork); return [ 'content_type' => (string) ($category?->contentType?->name ?? ''), 'category' => (string) ($category?->name ?? ''), 'user_id' => (string) ($artwork->user_id ?? ''), ]; } private function primaryCategory(Artwork $artwork): ?Category { /** @var Category|null $category */ $category = $artwork->categories->sortBy('sort_order')->first(); return $category; } }