Реалізація автоматичного зіставлення товарів різних постачальників (Matching)

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація автоматичного зіставлення товарів різних постачальників (Matching)
Складна
~2-4 тижні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація автоматичного сопоставлення товарів різних постачальників (Matching)

Matching — це завдання встановлення відповідності між позиціями різних постачальників, що описують один фізичний товар. На відміну від дедублікації (усунення явних дублів в одному каталозі), matching працює з первинно різними системами номенклатури. У постачальника A товар називається «Смартфон Samsung S24 256Gb», у B — «SAMSUNG Galaxy S24 (256 GB) Black SM-S921B». Це один товар, але без matching система створить дві карточки.

Методи matching

Методи використовуються послідовно — від жорстких до м'яких:

1. Точні ідентифікатори

  • EAN/GTIN — найнадійніший, покриває 40–60% товарів в електроніці
  • Артикул виробника (MPN) + бренд — додатково 20–30%
  • ISBN для книг, ASIN для Amazon-сумісних каталогів

2. Структуровані атрибути

  • Бренд + модель + ключові характеристики (ємність, колір, розмір)
  • Працює для стандартизованих категорій (техніка, одяг)

3. Нечіткий текст

  • Jaro-Winkler / Levenshtein на нормалізованих назвах
  • TF-IDF + cosine similarity на описі
  • Покриває нестандартизовані категорії

4. Векторний matching (ML)

  • Embeddings через sentence-transformers або OpenAI API
  • Ефективний для товарів зі складними описами

Схема даних

-- Таблиця сопоставлень
CREATE TABLE product_matches (
    id              BIGSERIAL PRIMARY KEY,
    master_id       BIGINT NOT NULL REFERENCES products(id),
    supplier_id     INT NOT NULL REFERENCES suppliers(id),
    supplier_sku    VARCHAR(255) NOT NULL,
    match_method    VARCHAR(30) NOT NULL,   -- 'gtin', 'mpn_brand', 'fuzzy', 'ml', 'manual'
    confidence      FLOAT,                  -- 0.0–1.0
    status          VARCHAR(20) DEFAULT 'active', -- 'active', 'rejected', 'pending_review'
    created_at      TIMESTAMP DEFAULT NOW(),
    UNIQUE(supplier_id, supplier_sku)
);

CREATE INDEX idx_matches_master ON product_matches(master_id);
CREATE INDEX idx_matches_confidence ON product_matches(confidence) WHERE status = 'pending_review';

Pipeline matching

class ProductMatchingPipeline
{
    private array $matchers = [];

    public function __construct(
        private GtinMatcher      $gtinMatcher,
        private MpnBrandMatcher  $mpnBrandMatcher,
        private FuzzyMatcher     $fuzzyMatcher,
        private VectorMatcher    $vectorMatcher,
    ) {
        $this->matchers = [
            ['matcher' => $gtinMatcher,     'threshold' => 1.0,  'auto_accept' => true],
            ['matcher' => $mpnBrandMatcher,  'threshold' => 1.0,  'auto_accept' => true],
            ['matcher' => $fuzzyMatcher,     'threshold' => 0.90, 'auto_accept' => true],
            ['matcher' => $vectorMatcher,    'threshold' => 0.85, 'auto_accept' => false],
        ];
    }

    public function match(SupplierProductDTO $dto): MatchResult
    {
        foreach ($this->matchers as $config) {
            $result = $config['matcher']->find($dto);

            if (!$result) continue;

            if ($result->confidence >= $config['threshold'] && $config['auto_accept']) {
                return new MatchResult(
                    masterProductId: $result->productId,
                    confidence:      $result->confidence,
                    method:          $result->method,
                    status:          'active',
                );
            }

            if ($result->confidence >= 0.70) {
                // Відправити в чергу ручної перевірки
                return new MatchResult(
                    masterProductId: $result->productId,
                    confidence:      $result->confidence,
                    method:          $result->method,
                    status:          'pending_review',
                );
            }
        }

        // Не знайдено — створити новий мастер-товар
        return new MatchResult(masterProductId: null, confidence: 0, method: 'none', status: 'new');
    }
}

GTIN matcher

class GtinMatcher
{
    public function find(SupplierProductDTO $dto): ?MatchCandidate
    {
        if (!$dto->barcode) return null;

        $normalized = $this->normalizeGtin($dto->barcode);

        // Пошук у вже відомих відбитках
        $fingerprint = ProductFingerprint::where('type', 'gtin')
            ->where('value', $normalized)
            ->first();

        if (!$fingerprint) return null;

        return new MatchCandidate(
            productId:  $fingerprint->product_id,
            confidence: 1.0,
            method:     'gtin',
        );
    }

    private function normalizeGtin(string $raw): string
    {
        $digits = preg_replace('/\D/', '', $raw);
        // EAN-8 → EAN-13
        if (strlen($digits) === 8) {
            $digits = str_pad($digits, 13, '0', STR_PAD_LEFT);
        }
        return $digits;
    }
}

Векторний matcher через OpenAI Embeddings

class VectorMatcher
{
    public function find(SupplierProductDTO $dto): ?MatchCandidate
    {
        $text = $this->buildText($dto);

        // Отримати embedding для нового товару
        $vector = $this->openai->embeddings()->create([
            'model' => 'text-embedding-3-small',
            'input' => $text,
        ])->embeddings[0]->embedding;

        // Пошук найближчого сусіда в pgvector
        $result = DB::selectOne("
            SELECT product_id, 1 - (embedding <=> :vec) AS similarity
            FROM product_embeddings
            WHERE 1 - (embedding <=> :vec) > 0.80
            ORDER BY embedding <=> :vec
            LIMIT 1
        ", ['vec' => '[' . implode(',', $vector) . ']']);

        if (!$result) return null;

        return new MatchCandidate(
            productId:  $result->product_id,
            confidence: (float) $result->similarity,
            method:     'vector',
        );
    }

    private function buildText(SupplierProductDTO $dto): string
    {
        return implode(' ', array_filter([
            $dto->brand,
            $dto->name,
            $dto->sku,
            implode(' ', array_values($dto->attributes)),
        ]));
    }
}

Для pgvector потрібна розширення в PostgreSQL:

CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE product_embeddings (
    product_id  BIGINT PRIMARY KEY REFERENCES products(id),
    embedding   vector(1536),  -- OpenAI text-embedding-3-small
    updated_at  TIMESTAMP
);
CREATE INDEX idx_embeddings_cosine ON product_embeddings
    USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

Інтерфейс ручної перевірки

Товари зі статусом pending_review потрапляють в чергу модератора. Інтерфейс показує:

  • Зліва — товар постачальника (назва, артикул, фото)
  • Справа — кандидат з каталогу з відсотком збігу
  • Кнопки: Підтвердити, Відхилити, Знайти інший
  • Гарячі клавіші для швидкості (→ прийняти, ← відхилити)

Досвідчений модератор обробляє 100–150 пар на годину.

Зворотний зв'язок для покращення моделі

Кожне рішення модератора — навчальний приклад:

class MatchFeedbackService
{
    public function recordDecision(int $matchId, string $decision, int $userId): void
    {
        $match = ProductMatch::findOrFail($matchId);

        $match->update([
            'status'      => $decision === 'accept' ? 'active' : 'rejected',
            'reviewed_by' => $userId,
        ]);

        // Зберегти для дообучення
        MatchTrainingExample::create([
            'supplier_product_data' => $match->supplierProduct->toArray(),
            'master_product_id'     => $match->master_id,
            'label'                 => $decision === 'accept' ? 1 : 0,
            'confidence_was'        => $match->confidence,
        ]);

        // Якщо відхилено — створити новий мастер-товар
        if ($decision === 'reject') {
            $this->createNewMaster($match->supplierProduct);
        }
    }
}

Продуктивність

При каталозі 100 000+ позицій matching неможливо запускати перебором усіх пар. Оптимізації:

  • Blocking: спочатку відбирати кандидатів за брендом/категорією, потім матчити тільки усередину блоку
  • Batch embeddings: запитувати вектори пачками по 100 штук за запит до OpenAI
  • pgvector IVFFlat index: approximate nearest neighbor за мілісекунди

Строки реалізації

  • GtinMatcher + MpnBrandMatcher + FuzzyMatcher: 2 дні
  • VectorMatcher + pgvector: 2 дні
  • Pipeline + черга ручної перевірки + інтерфейс: 2–3 дні
  • Feedback loop + метрики: 1 день

Разом: 7–8 робочих днів.