Podobne produkty i rekomendacje w sklepie internetowym: jak wykorzystać AI

05.11.2026 | Autor: Marcin Wiercioch

Sekcje „podobne produkty” i „klienci kupili również” to jeden z najprostszych sposobów na większy koszyk, a w wielu sklepach wciąż wypełniają je losowe produkty z tej samej kategorii. W drugim artykule z serii o AI w sklepach internetowych pokazujemy, jak zbudować trafniejsze rekomendacje z wektorów produktów, historii zamówień i modelu językowego. Z przykładami kodu dla WooCommerce.

Dwa różne pytania klienta

Rekomendacje w sklepie odpowiadają na dwa zupełnie różne pytania, które łatwo pomylić:

  • „Co zamiast tego?” Klient ogląda kurtkę, ale nie jest przekonany do koloru, ceny albo kroju. Szuka alternatyw, czyli produktów podobnych.
  • „Co do tego?” Klient już wybrał kurtkę i może potrzebować rękawiczek, czapki albo impregnatu. To produkty uzupełniające.

Pokazanie pięciu innych kurtek na karcie produktu ma sens. Pokazanie pięciu kurtek w koszyku, w którym kurtka już leży, to zmarnowane miejsce. Dlatego zanim wybierzemy technologię, warto ustalić, gdzie i na które pytanie odpowiada dana sekcja.

Jak to wygląda w większości sklepów

WooCommerce ma wbudowaną sekcję „Podobne produkty”, która losuje produkty z tych samych kategorii i tagów. Losowość sprawia, że przy każdym odświeżeniu strony klient widzi coś innego, a w szerokiej kategorii „Odzież” do kurtki trafiają skarpetki. Do tego dochodzą ręcznie uzupełniane pola „Sprzedaż w górę” i „Sprzedaż krzyżowa” w edycji produktu. Działają dobrze, ale tylko tam, gdzie ktoś je wypełnił, a przy kilku tysiącach produktów i ciągle dochodzących nowościach nikt tego nie utrzymuje.

Poniżej trzy podejścia, które da się połączyć: podobieństwo z wektorów, historia zamówień i model językowy podpowiadający, co do czego pasuje.

Podobne produkty z wektorów

W poprzednim artykule zamieniliśmy każdy produkt na wektor, czyli punkt na „mapie znaczeń”, i zapisaliśmy go w tabeli wp_product_embeddings. Te same dane od razu dają nam podobne produkty: zamiast szukać punktów najbliższych frazie klienta, szukamy punktów najbliższych danemu produktowi. Nie potrzeba żadnego dodatkowego zapytania do API, więc wyświetlanie rekomendacji nic nie kosztuje.

<?php
function get_similar_products(int $product_id, int $limit = 8): array
{
    global $wpdb;

    // Wynik zapamiętujemy na dobę, bo wektory produktów zmieniają się rzadko
    $cache_key = 'similar_products_' . $product_id;
    $cached = get_transient($cache_key);
    if (is_array($cached)) {
        return $cached;
    }

    $table = $wpdb->prefix . 'product_embeddings';
    $vector = $wpdb->get_var($wpdb->prepare(
        "SELECT VEC_ToText(embedding) FROM {$table} WHERE product_id = %d",
        $product_id
    ));
    if (!$vector) {
        return [];
    }

    // Bierzemy kilka razy więcej kandydatów, bo część odpadnie przy filtrowaniu
    $candidate_ids = $wpdb->get_col($wpdb->prepare(
        "SELECT product_id FROM {$table}
         WHERE product_id != %d
         ORDER BY VEC_DISTANCE_COSINE(embedding, VEC_FromText(%s))
         LIMIT %d",
        $product_id,
        $vector,
        $limit * 4
    ));

    // Zostawiamy tylko produkty widoczne w sklepie i dostępne od ręki
    $similar_ids = [];
    foreach ($candidate_ids as $candidate_id) {
        $candidate = wc_get_product($candidate_id);
        if ($candidate && $candidate->is_visible() && $candidate->is_in_stock()) {
            $similar_ids[] = (int) $candidate_id;
        }
        if (count($similar_ids) >= $limit) {
            break;
        }
    }

    set_transient($cache_key, $similar_ids, DAY_IN_SECONDS);

    return $similar_ids;
}

Wynik podpinamy pod wbudowaną sekcję „Podobne produkty”. WooCommerce domyślnie miesza kolejność tej listy, więc wyłączamy mieszanie, żeby najbardziej podobne produkty były na początku. Jeśli produkt nie ma jeszcze wektora, zostaje dotychczasowa lista:

<?php
add_filter('woocommerce_related_products', function ($related_ids, $product_id) {
    $similar_ids = get_similar_products((int) $product_id);

    return $similar_ids ?: $related_ids;
}, 10, 2);

add_filter('woocommerce_product_related_posts_shuffle', '__return_false');

Dwie poprawki, które w praktyce robią dużą różnicę:

  • Różnorodność. Najbliżej danego produktu często leżą jego własne warianty albo prawie identyczne modele. Osiem tych samych kurtek w różnych kolorach nikomu nie pomaga. Warto odrzucać produkty o bardzo małej odległości i ograniczyć liczbę produktów od jednego producenta.
  • Podobna cena. Alternatywa za trzykrotność ceny rzadko jest prawdziwą alternatywą. Prosty filtr, np. cena w przedziale od 70% do 130% ceny oglądanego produktu, wyraźnie poprawia trafność.

„Klienci kupili również” z historii zamówień

Na pytanie „co do tego?” najlepiej odpowiadają sami klienci. Jeśli wiele osób kupuje kurtkę razem z konkretnymi rękawiczkami, to jest najlepsza możliwa rekomendacja. Do tego nie potrzeba AI, wystarczy dobrze napisane zapytanie SQL. WooCommerce trzyma pozycje zamówień w tabeli wp_wc_order_product_lookup (tej samej, z której korzysta panel Analityki), więc liczenie, co kupowano razem, jest proste.

Jest jedna pułapka. Bestseller, np. popularna torba na zakupy, trafia do co drugiego koszyka, więc „kupowany razem” wychodzi z każdym produktem. Dlatego liczbę wspólnych zamówień dzielimy przez popularność produktu. Wtedy wysoko są produkty kupowane razem częściej, niż wynikałoby to z samej popularności:

SELECT other.product_id,
       COUNT(DISTINCT other.order_id) AS orders_together,
       COUNT(DISTINCT other.order_id) / popularity.orders_total AS score
FROM wp_wc_order_product_lookup base
JOIN wp_wc_order_product_lookup other
  ON other.order_id = base.order_id
 AND other.product_id != base.product_id
JOIN (
    SELECT product_id, COUNT(DISTINCT order_id) AS orders_total
    FROM wp_wc_order_product_lookup
    WHERE date_created >= NOW() - INTERVAL 12 MONTH
    GROUP BY product_id
) popularity ON popularity.product_id = other.product_id
WHERE base.product_id = 123
  AND base.date_created >= NOW() - INTERVAL 12 MONTH
GROUP BY other.product_id, popularity.orders_total
HAVING orders_together >= 3
ORDER BY score DESC
LIMIT 8;

Warunek HAVING odrzuca przypadkowe pary, które pojawiły się w jednym czy dwóch zamówieniach. Przy dużej liczbie zamówień takie zapytanie jest ciężkie, więc nie wykonujemy go przy każdym wyświetleniu strony. Wyniki dla wszystkich produktów przeliczamy raz na dobę w tle (np. przez Action Scheduler albo cron) i zapisujemy, np. w polach meta produktu.

Ograniczenie tego podejścia jest oczywiste: nowy produkt nie ma jeszcze historii zamówień, a mały sklep może mieć jej za mało. Tu z pomocą przychodzi model językowy.

Produkty uzupełniające z pomocą modelu językowego

Wektory odpowiadają na pytanie „co jest podobne”, a nie „co do czego pasuje”: najbliżej kurtki zawsze będą inne kurtki. Model językowy, taki jak te stojące za ChatGPT, ma natomiast ogólną wiedzę o świecie. Wie, że do kurtki przydadzą się rękawiczki, a do ekspresu do kawy odkamieniacz i filtry.

Nie musimy pytać modelu o każdy produkt osobno. Wystarczy zapytać o każdą kategorię: „klient kupuje coś z kategorii Kurtki, jakie kategorie z naszego sklepu mogą mu się przydać?”. Przy kilkudziesięciu kategoriach to kilkadziesiąt zapytań, wykonanych jeden raz. Model wybiera tylko spośród kategorii, które naprawdę mamy, a odpowiedź dostajemy w formacie JSON, łatwym do dalszej obróbki:

<?php
// Kategorie, które naturalnie uzupełniają zakup z danej kategorii
function suggest_complementary_categories(string $category_name, array $all_categories): array
{
    $prompt = "Prowadzisz sklep internetowy. Klient kupuje produkt z kategorii „{$category_name}”. "
        . "Wybierz z poniższej listy maksymalnie 4 kategorie produktów, które naturalnie uzupełniają taki zakup. "
        . "Nie wybieraj kategorii z produktami, które mogłyby go zastąpić. "
        . "Odpowiedz w formacie JSON: {\"categories\": [\"nazwa kategorii\"]}.\n\n"
        . "Kategorie sklepu:\n- " . implode("\n- ", $all_categories);

    $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
        'timeout' => 30,
        'headers' => [
            'Authorization' => 'Bearer ' . OPENAI_API_KEY,
            'Content-Type' => 'application/json',
        ],
        'body' => wp_json_encode([
            // np. najtańszy aktualny model z oferty dostawcy
            'model' => OPENAI_CHAT_MODEL,
            'messages' => [['role' => 'user', 'content' => $prompt]],
            'response_format' => ['type' => 'json_object'],
        ]),
    ]);

    if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
        return [];
    }

    $data = json_decode(wp_remote_retrieve_body($response), true);
    $answer = json_decode($data['choices'][0]['message']['content'] ?? '', true);

    // Model może się pomylić, więc zostawiamy tylko kategorie, które istnieją w sklepie
    return array_values(array_intersect($answer['categories'] ?? [], $all_categories));
}

Wynik warto potraktować jako propozycję, a nie gotową decyzję. Zapisujemy go w polu meta kategorii i pokazujemy w panelu, żeby ktoś z zespołu mógł go przejrzeć i poprawić w kilka minut. To nadal ogromna oszczędność w porównaniu z ręcznym uzupełnianiem sprzedaży krzyżowej dla każdego produktu.

Mając listę kategorii uzupełniających, w sekcji „Pasuje do tego” pokazujemy z nich bestsellery albo, jeszcze lepiej, produkty najbliższe znaczeniowo oglądanemu produktowi. Do zimowej kurtki trekkingowej trafią wtedy rękawiczki trekkingowe, a nie wieczorowe.

Jak to połączyć

Każde podejście ma swoje mocne strony, więc najlepiej działa ich połączenie:

Miejsce w sklepie Pytanie klienta Źródło rekomendacji
Karta produktu, sekcja „Podobne produkty” Co zamiast tego? Wektory, z filtrem ceny i dostępności
Karta produktu, sekcja „Pasuje do tego” Co do tego? Historia zamówień, a dla nowych produktów kategorie uzupełniające
Koszyk Czego jeszcze brakuje? Historia zamówień dla produktów w koszyku
Brak wyników wyszukiwania Czy jest coś podobnego? Wektory frazy z wyszukiwarki

Na co uważać

  • Nie polecaj tego, co klient już ma. Produkty z koszyka i niedawno kupione powinny znikać z rekomendacji.
  • Dostępność. Rekomendacja produktu, którego nie ma na stanie, to zmarnowane miejsce i frustracja klienta.
  • Wydajność. Wszystko, co się da, liczymy z wyprzedzeniem i trzymamy w cache. Karta produktu nie może czekać na ciężkie zapytania ani na odpowiedź z API.
  • Pomiar. Sprawdzaj, ile osób klika w rekomendacje i ile produktów trafia z nich do koszyka. Nową wersję sekcji najlepiej porównać ze starą testem A/B, np. w PostHog.

Ile to kosztuje

Podobne produkty z wektorów nie kosztują nic ponad to, co opisaliśmy w poprzednim artykule: wektory produktów już mamy, a samo wyszukiwanie sąsiadów odbywa się w bazie danych. Historia zamówień to wyłącznie SQL. Jedyny nowy koszt to zapytania o kategorie uzupełniające, wykonywane raz dla każdej kategorii. Przy kilkudziesięciu kategoriach i tanim modelu to koszt liczony w groszach. Ceny modeli zmieniają się często, więc przed wdrożeniem sprawdź aktualny cennik dostawcy.

Podobnie jak przy wyszukiwarce, prawdziwym kosztem jest czas wdrożenia: dobranie filtrów, przegląd propozycji kategorii i pomiar efektów.

Co dalej

Wektory produktów, historia zamówień i model językowy to także składniki asystenta zakupowego, który rozmawia z klientem na czacie, zna prawdziwy katalog i nie wymyśla produktów, których nie ma. Jak go zbudować i czego mu nie dawać, pokażemy w kolejnym artykule z serii o AI w sklepach internetowych.

Marcin Wiercioch Marcin Wiercioch

full stack developer

Współzałożyciel Okinet, programista PHP, Full stack developer, administrator linux i pasjonat technologii z 20 letnim stażem. Ostatnimi czasy szczególnie rozwijam umiejętności związane z optymalizacją i automatyzacją środowisk developerskich dzięki czemu budowane przez nas aplikacje internetowe są wydajne, bezpieczne i łatwo rozwijalne.

Wszystkie artykuły autora

Udostępnij

Oceń artykuł

Porozmawiaj z nami
o swoim projekcie

+48 506 160 480
biuro@okinet.pl

lub napisz