Реализация автоматического сопоставления товаров разных поставщиков (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 рабочих дней.