option('dry-run'); if (! $dryRun && ! $vectors->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); $embeddedOnly = (bool) $this->option('embedded-only'); $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 embedded_only=%s public_only=%s dry_run=%s', $startId, $afterId, $nextId, $batch, $limit > 0 ? (string) $limit : 'all', $embeddedOnly ? 'yes' : 'no', $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 ($embeddedOnly) { $query->whereHas('embeddings'); } 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; try { $payload = $vectors->payloadForArtwork($artwork); } catch (\Throwable $e) { $skipped++; $this->warn("Skipped artwork {$artwork->id}: {$e->getMessage()}"); continue; } $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 ?? ''), $payload['url'], $this->json($payload['metadata']) )); if ($dryRun) { $indexed++; $this->line(sprintf( '[dry] artwork=%d indexed=%d/%d', (int) $artwork->id, $indexed, $processed )); continue; } try { $vectors->upsertArtwork($artwork); $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 : '{}'; } }