Ви агрегатор із сотнею постачальників. Кожен надсилає каталог у своєму форматі. В одного — «Смартфон 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%. Ми маємо 7+ років досвіду в розробці matching-систем та успішно реалізували понад 10 проєктів, що підтверджує нашу експертизу та гарантує якість.
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. Автоматичний matching у 10 разів швидший за ручний пошук — модератор витрачає 2 хвилини на товар, а pipeline обробляє 1000 позицій за секунду.
Як працює 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€ на місяць на модерації. Це в 5 разів вигідніше, ніж найм додаткового модератора.
Хочете протестувати 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 під ваш стек.







