product-launch

PromoAzi: nouă lanțuri de supermarketuri într-un singur comparator, cu promoțiile tratate ca intervale, nu ca prețuri

Nouă lanțuri de supermarketuri, 115.884 de produse, 309.772 de promoții și 22,3 milioane de intervale de preț. Cum am modelat promoția ca interval cu reguli de cantitate, ce facem când sursa nu publică deloc codul de bare, și trei erori care au ținut mai mult decât ar fi trebuit: protecția anti-bot care a rupt propriul site, sincronizarea oprită șaptesprezece zile care raporta succes și multipack-ul confundat cu bucata.

Cuprins

Contextul

Promoția la alimente din România are o proprietate incomodă: durează o săptămână, se schimbă în fiecare joi, este diferită la fiecare lanț și nu există nicăieri într-un format pe care să îl poți citi. Ca să compari, deschideai nouă pliante, fiecare cu propriul calendar, propriile denumiri și propriile reguli de tip „al doilea la jumătate de preț". Practic, nimeni nu o făcea.

Am construit PromoAzi, comparatorul de prețuri pentru supermarketuri, ca să existe acel format. Acoperă nouă lanțuri, Auchan, Carrefour, Kaufland, Lidl, Mega Image, Metro, Penny, Profi și Selgros, cu un site public și o aplicație de mobil. Este produsul cu cea mai mare vechime din rețeaua „Azi" și cel din care a ieșit tot ce am refolosit apoi la FarmaAzi.

Cifrele de la data publicării acestui text: 115.884 de produse, 309.772 de promoții urmărite în 4.600 de cicluri promoționale, 5.026 de magazine în 1.010 localități și 22,3 milioane de intervale de preț în istoric. Site-ul public are 78.571 de pagini de produs indexabile.

Promoția nu e un preț, e un interval cu reguli

Prima decizie de model a fost să nu tratăm promoția ca pe un preț mai mic. O promoție are o dată de început, o dată de sfârșit, un lanț, adesea un set restrâns de magazine și, destul de des, o condiție de cantitate. Dacă o reduci la un număr, pierzi exact partea care contează pentru cine se uită la ea.

Prețurile în sine sunt stocate ca intervale cu început și sfârșit, nu ca un rând pe zi. Un preț care nu se mișcă trei săptămâni ocupă un rând, iar o modificare închide intervalul curent și deschide altul. La 2,2 milioane de prețuri active pe magazin, diferența dintre cele două modele nu e stilistică, e diferența dintre o bază de date care încape și una care nu.

Condițiile de cantitate au primit propriul lor analizor. „5+1 gratis", „al doilea produs la jumătate de preț", „cumperi 3, plătești 2" sunt scrise diferit de fiecare lanț, în română, cu numerale ordinale și cu topică liberă. Analizorul extrage cantitatea care declanșează reducerea și o salvează separat de preț, iar promoțiile cu cantitate sunt scoase din verificările de comparație între lanțuri. Motivul e simplu: un preț unitar care presupune că ai cumpărat șase bucăți nu se compară corect cu un preț unitar care presupune că ai cumpărat una.

Același produs în nouă cataloage

Ca la orice comparator, totul se reduce la a decide dacă două listări sunt același lucru. Prima linie este codul de bare EAN, cu verificări suplimentare pe brand, pe gramaj și pe denumire înainte de a accepta potrivirea, pentru că un EAN singur nu e o garanție suficientă atunci când sursele îl copiază greșit.

Problema e că o parte semnificativă dintre sursele de date nu publică deloc EAN-ul. Pentru rândurile fără cod de bare am construit un lanț de potrivire în trei trepte: întâi în interiorul aceluiași lanț după denumire, apoi între lanțuri prin grupurile de echivalență, apoi potrivire aproximativă pe text. Grupurile de echivalență sunt stratul care leagă produse care nu sunt identice, dar sunt comparabile, și sunt 1.663 de grupuri cu 4.805 membri.

Trei erori care au ținut mai mult decât ar fi trebuit

Le scriem pentru că partea utilă dintr-o lucrare nu e lista de funcționalități, ci ce s-a stricat, cât a durat până am observat și de ce.

Prima: protecția anti-bot pe care am pornit-o noi a rupt propriul site. Am activat o regulă de protecție care punea o provocare de verificare pe rutele de API atunci când detecta trafic intens. Regula a început să răspundă cu o pagină HTML de verificare la cererile făcute din aplicație, care așteptau JSON. Un browser real poate rezolva provocarea; o cerere de date pornită din cod nu poate, iar mecanismul de preluare a paginii randate pe server nu avea ce face cu un răspuns HTML. Lecția pe care am dus-o mai departe: o regulă de protecție care nu distinge între o navigare de pagină și o cerere de date nu protejează site-ul, îl oprește.

A doua: o sincronizare a fost oprită șaptesprezece zile și totul raporta succes. Colectorul rula pe programul lui, termina corect și, la final, apela o adresă de pe propriul nostru domeniu ca să declanșeze pasul de import. Acel apel primea 403 de la stratul de protecție, pentru că venea din exterior și arăta ca trafic automat, ceea ce și era. Colectorul nu trata răspunsul final ca pe un rezultat, deci se termina fericit. Datele intrau în tabelul intermediar și rămâneau acolo. S-a văzut abia când cineva a observat că numărul de promoții active scade lent, fără motiv. Corectura tehnică a fost un declanșator local care ocolește stratul de protecție. Corectura reală a fost alta: pasul final al oricărui lanț de procesare trebuie să confirme ce a produs, nu doar că s-a terminat.

A treia: un multipack și o bucată au ajuns același produs. Analizorul de gramaj lua primul tipar de forma „număr × cantitate" pe care îl găsea în denumire. Într-o denumire de tip „5+1 × 0,33 l", primul tipar întâlnit era chiar cel al bucății, deci pachetul de șase și sticla singură primeau același gramaj normalizat și se potriveau. Rezultatul, vizibil în pagină, era un preț de multipack pus lângă un preț de bucată ca și cum ar fi fost aceeași ofertă. Fixul a fost ca tiparul de multipack să fie recunoscut înaintea celui de bucată, iar pachetele promoționale care primesc de la producător un cod de bare propriu să fie legate explicit de produsul de bază, cu verificare umană.

Calitatea datelor este o funcționalitate, nu o curățenie de final

Catalogul de coduri de bare are 151.254 de intrări, adunate din toate sursele. La scara asta, greșelile nu mai sunt excepții de tratat manual, sunt un flux constant care are nevoie de propriul mecanism.

Cea mai frecventă este codul de bare legat de mai multe produse simultan, aproape întotdeauna pentru că același produs a fost creat de două ori sub denumiri ușor diferite. Un audit dedicat a arătat că marea majoritate a acestor cazuri sunt duplicate care trebuie unite, nu conflicte reale. Le tratăm cu o coadă de verificare în care fiecare caz primește un verdict, iar unirea se face doar după verdict.

Regula pe care o aplicăm peste tot: un produs pe care nu îl putem identifica sigur rămâne vizibil ca neidentificat, nu este împins într-o potrivire plauzibilă. O comparație greșită între două produse diferite face mai mult rău decât o comparație care lipsește, pentru că cititorul nu are cum să o observe.

Ce am învățat despre randarea pe server

Site-ul public este Angular cu randare pe server peste Spring Boot și PostgreSQL. Cea mai scumpă lecție nu a fost despre viteză, ci despre ce anume se trimite când randarea nu apucă să se termine.

Pagina de produs pornea opt cereri de date în paralel, fără nimic care să spună procesului de randare să le aștepte pe toate. În unele cereri, serverul returna 200 cu scheletul de încărcare, adică o pagină validă, rapidă și complet goală de conținut și de etichete pentru motoarele de căutare. Un cod 200 pe o pagină goală e mai rău decât o eroare, pentru că nimic nu îl semnalează. De aici vin două reguli pe care le aplicăm acum peste tot: randarea așteaptă explicit datele de care depinde conținutul indexabil, iar o pagină inexistentă returnează efectiv 404, nu 200 cu un mesaj de eroare desenat în pagină.

Peste catalog am construit un strat de conținut generat din date: pagini de lanț, de oraș, de categorie și de brand, plus comparații. Sunt 1.601 pagini de categorie, 973 de pagini de oraș și 1.724 de pagini de tip lanț într-un anumit oraș, fiecare cu canonical propriu, iar paginile de filtrare sunt marcate să nu fie indexate ca să nu concureze cu paginile reale.

Aplicația și buletinul săptămânal

Aplicația de mobil, scrisă în Flutter pentru iOS și Android, are partea pe care site-ul nu o poate face bine: scanare de cod de bare direct în magazin, liste de cumpărături și notificări. Folosește același backend și același contract de API ca site-ul, ceea ce înseamnă că o corecție de date se vede în ambele fără o a doua lansare.

Buletinul săptămânal are o particularitate de măsurare care merită spusă, pentru că este contraintuitivă. Nu ne uităm la rata de deschidere. Protecția de confidențialitate din clientul de email al Apple pre-încarcă imaginile de urmărire, ceea ce marchează ca „deschis" un email pe care nimeni nu l-a citit, iar peste jumătate din trafic vine de acolo. Indicatorul pe care îl raportăm este rata de clic, singurul care presupune că un om a făcut ceva.

Unde suntem acum

promoazi.ro rulează cu nouă lanțuri, 115.884 de produse și 309.772 de promoții urmărite, cu aplicația în ambele magazine. Colectarea, potrivirea și recalcularea clasamentelor rulează pe program automat zilnic.

Ce urmează: reducerea numărului de coduri de bare legate greșit la mai multe produse, care este cea mai mare sursă rămasă de erori vizibile în pagină; extinderea legării pachetelor promoționale de produsul de bază; și mutarea a ce am învățat aici mai departe în rețea, așa cum s-a întâmplat deja cu comparatorul pentru farmacii. Raportarea rămâne aceeași, inclusiv în lunile în care un număr scade. Livrarea e 40% din treabă; restul e ca lucrurile să meargă curat luni la rând (A.06).

Întrebări frecvente

Ce face PromoAzi?

Adună prețurile și promoțiile de la nouă lanțuri de supermarketuri din România și le pune într-un singur loc, pe site și în aplicație. Acoperă Auchan, Carrefour, Kaufland, Lidl, Mega Image, Metro, Penny, Profi și Selgros, cu 115.884 de produse, 309.772 de promoții urmărite și 5.026 de magazine în 1.010 localități. Aplicația de mobil adaugă scanarea codului de bare în magazin, listele de cumpărături și notificările.

De ce este o promoție mai greu de modelat decât un preț?

Pentru că o promoție are început, sfârșit, un set de magazine și, foarte des, o condiție de cantitate. „5+1 gratis" sau „al doilea la jumătate de preț" nu sunt prețuri mai mici, sunt reguli. Le extragem cu un analizor separat, care recunoaște formulările în limba română folosite de fiecare lanț, și salvăm cantitatea care declanșează reducerea distinct de preț. Promoțiile cu condiție de cantitate sunt apoi excluse din comparațiile directe între lanțuri, pentru că un preț unitar care presupune șase bucăți nu se compară corect cu unul care presupune una.

Cum potriviți produsele când sursa nu publică codul de bare?

Printr-un lanț de trei trepte, cu oprire la verificare umană atunci când nu se poate decide. Prima linie rămâne codul de bare EAN, cu verificări suplimentare pe brand, gramaj și denumire. Pentru rândurile fără EAN încercăm întâi potrivirea în interiorul aceluiași lanț, apoi între lanțuri prin cele 1.663 de grupuri de echivalență, apoi potrivirea aproximativă pe text. Cazul în care metoda cedează este marca proprie a lanțurilor, care nu apare în niciun dicționar de branduri; acolo perechea intră în coadă de verificare, nu se unește automat.

Cât de des se actualizează datele?

Zilnic, pe program automat. Colectarea, pasul de potrivire de după import și recalcularea clasamentelor rulează pe cron, iar sincronizarea din tabelele intermediare în promoțiile publicate rulează de două ori pe zi. Prețurile sunt stocate ca intervale cu început și sfârșit, nu ca un rând pe zi, deci istoricul se poate citi înapoi fără să ținem milioane de rânduri identice.

De ce raportați rata de clic la newsletter și nu rata de deschidere?

Pentru că rata de deschidere a devenit un indicator fals. Protecția de confidențialitate din clientul de email al Apple pre-încarcă imaginile de urmărire, deci marchează ca deschis un email pe care nimeni nu l-a citit, iar peste jumătate din trafic vine de acolo. Rata de clic este singurul indicator care presupune că un om a făcut efectiv ceva, așa că pe acela îl raportăm, chiar dacă numărul arată mai mic.