Quella volta che siamo andati online con il nuovo ecommerce e ci siamo accorti che mancavano le foto dei prodotti

on

Nel refactoring di un ecommerce, ci sono due tipi di problemi:

  1. quelli previsti
  2. quelli che scopri mesi dopo… quando è troppo tardi

Tutti conosciamo la teoria delle migrazioni perfette: ambienti di staging, backup ridondanti, controlli QA rigorosi. E poi c’è la realtà.

Recentemente mi sono trovato in uno scenario da incubo durante il refactoring di un e-commerce (migrazione da WooCommerce a Next.js + LunarPHP).

Durante la migrazione, un freelance si occupa dell’importazione dei dati: prodotti, varianti, prezzi, categorie, media. Alla consegna facciamo controlli preliminari: tutto sembra ok, il freelance si congeda, passano settimane, poi mesi, si va online.

Dopo il go-live, ci accorgiamo che qualcosa non torna. Un gruppo di prodotti, nell’ordine delle migliaia, risulta senza immagini.

E nel commercio elettronico, un prodotto senza immagine è praticamente un prodotto invisibile:

  • conversione bassa
  • UX compromessa
  • fiducia ridotta
  • catalogo “rotto”

Il disastro operativo: niente backup e freelance irreperibile

A quel punto iniziano le domande: recuperiamo dal vecchio hosting? Chiediamo al freelance di rigenerare la migrazione?

Peccato che: il contratto con il vecchio hosting era stato disattivato e quindi sorgenti e gli upload non erano più accessibili, il freelance era impegnato su altro e quindi… sparito.

Che si fa?

La risposta classica sarebbe:

  • ripetere shooting fotografico o recuperare manualmente immagini, magari chiedendole al fornitore
  • ricostruire tutto a mano

Ma su migliaia di SKU non è una soluzione. Serviva qualcosa di diverso. Una soluzione creativa disponibile in poche ore, al massimo giorni, non settimane.

Automatizzare l’impossibile usando PHP, Google Custom Search e Computer Vision.

Ma c’è un problema: cercare su Google non è semplice come sembra. Non è sempre tutto oro quel che luccica nelle prime posizioni.

Fase 1: La strategia di “scraping” (Search Fallbacks)

Se cerchi PRODOTTO-123 Bracciale Argento potresti non trovare nulla. Magari Google ha indicizzato il prodotto, ma senza il codice SKU nel titolo. Oppure ha lo SKU, ma il nome è leggermente diverso.

Per questo, nel mio GoogleSearchService, non mi sono limitato a una singola query “secca”. Ho implementato una strategia a 4 livelli di profondità (Fallback Strategy) per “scavare” nella SERP finché non emerge qualcosa di utile.

Ecco come ragiona l’algoritmo quando cerca un’immagine:

  1. Il Tentativo Ottimista (Query Completa) Cerca la stringa esatta: SKU + Nome Prodotto. È la situazione ideale, ma spesso fallisce se il vecchio sito non aveva un buon SEO o se lo SKU è interno.
  2. La Pulizia (Sanitization Fallback) Se il primo tentativo fallisce, l’algoritmo riprova rimuovendo la “sporcizia”. Via punteggiatura, trattini inutili e spazi doppi. Spesso Google si confonde con simboli come _ o / in eccesso.
  3. L’Approccio Semantico (Solo Nome Prodotto) Qui si rischia di più, ma si ottengono più risultati. Se lo SKU “sporca” la ricerca, lo rimuoviamo brutalmente dalla stringa e cerchiamo solo il nome (es. “Bracciale Pandora Argento”). Rischio: Trovare prodotti simili ma non identici. Soluzione: Qui è fondamentale la validazione successiva con l’AI.
  4. L’Approccio Tecnico (Solo SKU) Ultima spiaggia. Cerchiamo solo il codice identificativo. Funziona benissimo per ricambi o elettronica, dove lo SKU è univoco nel mondo, ma malissimo per prodotti generici.

Solo combinando questi quattro approcci siamo riusciti a passare dal “Nessun risultato trovato” a una lista di candidati plausibili per il 90% dei prodotti mancanti.

Fase 2: Il Cervello (Vision + DeepL)

Trovare l’immagine è solo metà dell’opera. Scaricare l’immagine sbagliata è peggio che non averne nessuna. Come distinguere un “Bracciale Pandora” dal “Vaso di Pandora” o dal pianeta di Avatar?

Ho creato un GoogleVisionService che funge da giudice imparziale:

  1. Analisi Visiva: Google Cloud Vision analizza l’immagine candidata e ci dice cosa vede (es. ['Jewelry', 'Silver', 'Bracelet']).
  2. Ponte Linguistico: Usiamo DeepL per tradurre le nostre keyword italiane (es. “Bracciale”) in inglese, la lingua nativa delle label di Vision.
  3. Relevance Score: Incrociamo i dati. Se le etichette dell’immagine matchano con le keyword tradotte del prodotto, il punteggio sale. Se il punteggio supera il 0.7 (o il 70% di confidenza), l’immagine è approvata.

L’idea è stata usare Google Vision AI come “semantic gatekeeper”, quindi non come riconoscitore generico, ma come filtro semantico automatico:

“Questa immagine appartiene davvero al contesto del prodotto?”

Il ponte linguistico è il vero “hack”

Poiché Google Vision restituisce etichette in inglese, abbiamo usato DeepL (anche questo strumento è AI based) come traduttore in tempo reale degli attributi del prodotto. Questo ci ha permesso di confrontare “Silver” (etichetta AI) con “Argento” (attributo colore presente a DB), garantendo che il match semantico non andasse perso nella traduzione. Un saluto a Silver, il creatore di Lupo Aberto, che grazie a questo hack non è finito in mezzo alle foto dei prodotti.

Non solo pertinenza: la sicurezza del Brand

Quando automatizzi il download di immagini da Google, il rischio “contenuti inappropriati” è dietro l’angolo. Nel codice ho integrato un filtro basato sulla SafeSearch detection di Vision AI. Se l’algoritmo rileva una probabilità elevata di contenuti adult, violence o racy, l’immagine viene scartata a prescindere dal punteggio di pertinenza. Automatizzare sì, ma con la cintura di sicurezza allacciata.

Paragrafo per gli smanettoni: apriamo il cofano

NB: Se ti basta quanto hai letto nella Fase 2, puoi passare alla Fase 3.

Cosa restituisce davvero Vision (labelAnnotations, score e topicality)

Per ogni immagine candidata, Google Cloud Vision non “indovina il prodotto”, ma restituisce un set di etichette (label) con un livello di confidenza. Questo output è perfetto per trasformare Vision in un filtro semantico: non cerco la verità assoluta, cerco indizi coerenti col contesto.

Ecco un esempio (ridotto) di risposta per una foto che Vision interpreta come gioielleria:

{
  "responses": [
    {
      "labelAnnotations": [
        { "description": "Jewelry", "score": 0.92, "topicality": 0.92 },
        { "description": "Bracelet", "score": 0.88, "topicality": 0.88 },
        { "description": "Silver", "score": 0.84, "topicality": 0.84 },
        { "description": "Fashion accessory", "score": 0.79, "topicality": 0.79 }
      ],
      "safeSearchAnnotation": {
        "adult": "VERY_UNLIKELY",
        "violence": "VERY_UNLIKELY",
        "racy": "UNLIKELY"
      }
    }
  ]
}

Due dettagli importanti:

  • description è la label (spesso in inglese);
  • score e topicality sono indicatori di confidenza/pertinenza, utili per pesare le evidenze (non tutte le label “valgono” uguale).

Il ponte linguistico: dall’italiano del catalogo alle label in inglese

Il catalogo e gli attributi (colore/materiale/tipologia) erano in italiano: “bracciale”, “argento”, “ciondolo”. Vision mi rispondeva con “bracelet”, “silver”, “jewelry”. Qui entra in gioco DeepL: traduco in tempo reale (o con cache) un set di keyword “core” e confronto mele con mele.

In pratica genero un vocabolario atteso in inglese (derivato da nome prodotto + attributi) e poi uso le label Vision come evidenze.

Lo scoring: pertinenza, ambiguità semantica e “forbidden labels”

Un match string-based puro è fragile. Quello che ha funzionato è un piccolo modello di scoring:

  1. prendo le label Vision più forti (es. top 8 per topicality);
  2. premio le label presenti nel set “expected” (tradotto da IT → EN);
  3. penalizzo label che indicano un contesto alternativo (es. “Vase”, “Mythology”, “Planet”, “Logo”);
  4. scarto a prescindere se SafeSearch segnala rischi (adult/violence/racy).

Ecco uno snippet (semplificato) che rende l’idea:

$expected = ['jewelry', 'bracelet', 'silver', 'necklace', 'ring'];      // da prodotto/attributi via DeepL
$forbidden = ['vase', 'mythology', 'logo', 'software', 'planet'];      // contesti “trappola”

$labels = collect($visionLabels) // es. da labelAnnotations
  ->sortByDesc(fn($l) => $l['topicality'] ?? $l['score'])
  ->take(8)
  ->map(fn($l) => strtolower($l['description']))
  ->values();

$score = 0.0;

// Reward/Penalty semplice ma efficace
foreach ($labels as $label) {
  if (in_array($label, $expected, true)) {
    $score += 0.18;      // reward
  }
  if (in_array($label, $forbidden, true)) {
    $score -= 0.45;      // penalty forte: evita disastri tipo “Pandora = vaso”
  }
}

// Bonus se Vision è molto “sicuro” della prima label
$top = $visionLabels[0] ?? null;
if ($top && (($top['score'] ?? 0) >= 0.90)) {
  $score += 0.10;
}

// SafeSearch: cintura di sicurezza
if ($safeSearch['adult'] === 'LIKELY' || $safeSearch['adult'] === 'VERY_LIKELY') {
  return ['ok' => false, 'score' => $score, 'reason' => 'SafeSearch: adult'];
}

return [
  'ok' => $score >= 0.70,
  'score' => round($score, 2),
  'labels' => $labels->all(),
];

La cosa interessante non è tanto il valore delle costanti (che ho tarato per tentativi), ma l’idea: l’AI produce feature (label), e io costruisco sopra una policy deterministica che gestisce ambiguità semantiche. “Pandora” diventa un caso gestibile: gioiello vs mito vs altro.

Fase 3: Persistenza e audit trail, perché Spatie Media Library è stata una scelta “da produzione”

Una volta validata l’immagine, non volevo solo “attaccarla al prodotto”. Volevo anche poter rispondere, mesi dopo, a domande del tipo:

  • Da dove arriva questa immagine?
  • Con quale query è stata trovata?
  • Quali label Vision hanno portato all’approvazione?
  • Con quale score e con che decisione SafeSearch?

Qui Spatie Media Library è perfetta: offre un’API fluida per associare file ai modelli Eloquent e, soprattutto, permette di salvare metadati strutturati tramite withCustomProperties() (JSON), recuperabili e ispezionabili in modo pulito.

Nel mio caso ho salvato sia l’evidenza (Vision) sia la provenienza (Search):

$variantModel->addMedia($tempFile)
    ->withCustomProperties([
        'image_source' => 'google',
        'google_search' => [
            'query' => $query,
            'url' => $imageUrl,
            'rank' => $rank,
        ],
        'validation' => [
            'score' => $decision['score'],
            'labels' => $decision['labels'],
            'safe_search' => $safeSearch,
            'approved_at' => now()->toIso8601String(),
        ],
    ])
    ->toMediaCollection('images');

Il vantaggio pratico è enorme: se domani devo “spiegare” un’immagine o rifare una parte del processo, ho tutto in chiaro nei metadati del media item (e posso persino costruire strumenti interni per filtrare/ricontrollare le immagini già importate).

In altre parole: non ho solo ripopolato un catalogo. Ho costruito un processo ripetibile, tracciabile, e difendibile.

Il Risultato

Lanciando il comando products:fetch-variant-images, il terminale ha iniziato a raccontare una storia:

  • 🔍 Processing SKU: 123-PND...
  • ⚠️ Query completa: 0 risultati.
  • 🔄 Fallback #2 (Solo Nome): Trovate 5 immagini.
  • 🔎 Validating image #1... Score: 0.15 (Rejected: Low relevance)
  • 🔎 Validating image #2... Score: 0.92 (Validated!)
  • ✅ Image attached.

L’algoritmo ha “ragionato” come un umano: ha provato a cercare in modo specifico, poi ha allargato il campo, e infine ha usato gli occhi (digitali) per scartare i falsi positivi.

AI come filtro operativo, non come “magia”

Questa è stata la parte più interessante del progetto: Google Vision non ha “risolto tutto”.

Ma ha permesso di costruire un sistema di cernita automatica:

  • Google Search = recall (trova candidate)
  • Vision AI = precision (filtra semanticamente)
  • Lunar Attach = persistenza finale

Una pipeline ingegneristica completa.

Conclusioni

L’AI nel web development non serve solo a generare codice. In questo caso, l’uso combinato di Logica di Fallback (per la ricerca) e Computer Vision (per la validazione) ha trasformato un disastro annunciato in un catalogo ripopolato automaticamente.

A volte, quando perdi i sorgenti, sei costretto a scrivere codice migliore di quello che avevi prima.

Related Posts:

  • No Related Posts

Rispondi

Questo sito utilizza Akismet per ridurre lo spam. Scopri come vengono elaborati i dati derivati dai commenti.