Вы агрегатор с сотней поставщиков. Каждый присылает каталог в своём формате. У одного — «Смартфон Samsung S24 256Gb», у другого — «SAMSUNG Galaxy S24 (256 GB) Black SM-S921B». Это один товар. Это задача автоматического сопоставления товаров поставщиков, или product matching. Дедупликация товаров — лишь часть решения. Без matching вы создадите дубли, потеряете остатки и цены. Мы строим систему, которая автоматически находит соответствия — от жёстких правил до ML.
За 7–8 рабочих дней вы получаете pipeline, покрывающий 80–90% товаров без участия человека. При правильной настройке автоматическое сопоставление покрывает такой объём. Оставшиеся 10–20% уходят в очередь ручной проверки сопоставлений. Итог — единый каталог с минимальными затратами на модерацию.
В одном из проектов с каталогом 200 000 позиций pipeline за первый месяц автоматически сопоставил 85% товаров. Модератор тратил по 1,5 часа в день на оставшиеся 15%. Через три месяца, после дообучения на решениях модератора, автоматизация выросла до 93%.
Matching решает проблему дублирования
Matching (сопоставление) принципиально отличается от дедупликации. Дедупликация ищет явные дубли в одном каталоге. Matching работает с разными системами номенклатуры. Мы используем цепочку методов — от самых надёжных до вероятностных.
Точные идентификаторы
- GTIN/EAN — самый надёжный, покрывает 40–60% товаров в электронике.
- MPN + бренд — даёт ещё 20–30%.
- ISBN, ASIN для специфических категорий.
Структурированные атрибуты
Если нет штрихкода, матчим по бренду + модели + характеристикам (ёмкость, цвет, размер). Эффективно для стандартизированных категорий.
Нечёткий текст (fuzzy)
Алгоритмы Jaro-Winkler и Levenshtein на нормализованных названиях. TF-IDF + косинусная близость на описаниях. Покрывает нестандартизированные позиции — стройматериалы, запчасти.
Векторный matching (ML)
Embeddings через sentence-transformers (локально) или OpenAI API. Работает даже при отсутствии общих слов — улавливает смысл. Обеспечивает ещё 5–10% точности поверх fuzzy.
Сравнение методов matching
| Метод | Точность | Покрытие | Скорость |
|---|---|---|---|
| GTIN/EAN | 100% (при наличии) | 40–60% | ~1 мс |
| MPN+бренд | 95%+ | 20–30% | ~2 мс |
| Нечёткий текст | 85–90% | 10–20% | ~10 мс |
| Векторный (ML) | 90–95% | 5–10% | ~50 мс (с GPU) |
Fuzzy дешевле и быстрее ML, но ML точнее на 15–20% для сложных описаний. Мы комбинируем их в pipeline.
Как работает pipeline сопоставления?
Pipeline — это цепочка методов, где каждый следующий включается, если предыдущий не дал результата с достаточной уверенностью. Первыми применяются точные методы (GTIN, MPN), затем — вероятностные (fuzzy, ML). Это позволяет минимизировать ложные срабатывания и максимизировать покрытие.
Преимущества ML-матчинга
ML-матчинг оправдан, когда поставщики описывают товары разными словами. Например, один пишет «Смартфон Samsung Galaxy S24 256 ГБ», другой — «Telefon Samsung S24 256 Gb». Традиционный fuzzy не поймёт, что это одно и то же, а embedding модель уловит семантическую близость. На практике векторный matching добавляет 5–10% точности, что критично для каталогов с 30% неструктурированных позиций.
Схема данных
-- Таблица сопоставлений + таблица эмбеддингов 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', created_at TIMESTAMP DEFAULT NOW(), UNIQUE(supplier_id, supplier_sku) ); CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE product_embeddings ( product_id BIGINT PRIMARY KEY REFERENCES products(id), embedding vector(1536), updated_at TIMESTAMP ); CREATE INDEX idx_matches_master ON product_matches(master_id); CREATE INDEX idx_matches_confidence ON product_matches(confidence) WHERE status = 'pending_review'; CREATE INDEX idx_embeddings_cosine ON product_embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); 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); 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); $vector = $this->openai->embeddings()->create([ 'model' => 'text-embedding-3-small', 'input' => $text, ])->embeddings[0]->embedding; $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)), ])); } } Интерфейс ручной проверки и обратная связь
Товары со статусом pending_review попадают в очередь модератора. Интерфейс показывает слева товар поставщика (название, артикул, фото), справа — кандидата из каталога с процентом совпадения. Кнопки: Подтвердить, Отклонить, Найти другой. Горячие клавиши (→ принять, ← отклонить). Опытный модератор обрабатывает 100–150 пар в час.
Каждое решение модератора становится обучающим примером для pipeline:
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 штук.
- pgvector IVFFlat index — approximate nearest neighbor за миллисекунды.
При каталоге 100 000 позиций такая архитектура экономит до 2000€ в месяц на модерации.
Хотите протестировать matching на своих данных? Свяжитесь с нами — мы подберём оптимальную конфигурацию.
Что входит в работу
- Разработка pipeline matching под ваш стек (Laravel, Django, Node.js)
- Настройка GTIN-, MPN-, fuzzy- и ML-матчеров
- Проектирование схемы БД (матчинги, эмбеддинги, индексы)
- Интерфейс ручной проверки с горячими клавишами
- Интеграция с поставщиками (API или импорт)
- Документация API и схемы данных
- Обучение модераторов (2 часа)
- Поддержка 1 месяц после запуска
Сроки реализации
| Этап | Длительность |
|---|---|
| GtinMatcher + MpnBrandMatcher + FuzzyMatcher | 2 дня |
| VectorMatcher + pgvector | 2 дня |
| Pipeline + очередь ручной проверки + интерфейс | 2–3 дня |
| Feedback loop + метрики | 1 день |
| Итого | 7–8 рабочих дней |
Почему стоит заказать matching у нас?
Мы реализовали matching для пяти агрегаторов с каталогами от 50 000 до 500 000 позиций. Наш pipeline автоматически сопоставляет 80–90% товаров. Остаток — очередь ручной проверки, которая не занимает больше 2 часов в день. Вы получаете единый каталог без дублей и путаницы.
Оценим вашу задачу за 1 день, предложим архитектуру и точные сроки. Свяжитесь с нами, чтобы обсудить проект. Закажите разработку matching под ваш стек.







