You are an aggregator with a hundred suppliers. Each sends a catalog in its own format. One has "Smartphone Samsung S24 256Gb", another has "SAMSUNG Galaxy S24 (256 GB) Black SM-S921B". It's the same product. This is the task of automatic supplier product matching, or product matching. Product deduplication is only part of the solution. Without matching, you create duplicates, lose stock and prices. We build a system that automatically finds matches—from hard rules to ML.
In 7–8 working days, you get a pipeline covering 80–90% of products without human intervention. With proper configuration, automatic matching covers that volume. The remaining 10–20% goes to a manual review queue. The result is a unified catalog with minimal moderation costs.
In one project with a catalog of 200,000 items, the pipeline automatically matched 85% of products in the first month. A moderator spent 1.5 hours per day on the remaining 15%. After three months, with re-training on the moderator's decisions, automation grew to 93%.
Matching Solves the Duplication Problem
Matching is fundamentally different from deduplication. Deduplication looks for obvious duplicates in one catalog. Matching works with different naming systems. We use a chain of methods—from the most reliable to probabilistic.
Exact Identifiers
- GTIN/EAN—most reliable, covers 40–60% of electronics products.
- MPN + brand—adds another 20–30%.
- ISBN, ASIN for specific categories.
Structured Attributes
If no barcode, we match by brand + model + characteristics (capacity, color, size). Effective for standardized categories.
Fuzzy Text
Algorithms Jaro-Winkler and Levenshtein on normalized names. TF-IDF + cosine similarity on descriptions. Covers non-standardized items—building materials, spare parts.
Vector Matching (ML)
Embeddings via sentence-transformers (local) or OpenAI API. Works even with no common words—captures meaning. Adds another 5–10% accuracy on top of fuzzy.
Comparison of Matching Methods
| Method | Precision | Coverage | Speed |
|---|---|---|---|
| GTIN/EAN | 100% (if present) | 40–60% | ~1 ms |
| MPN+brand | 95%+ | 20–30% | ~2 ms |
| Fuzzy text | 85–90% | 10–20% | ~10 ms |
| Vector (ML) | 90–95% | 5–10% | ~50 ms (with GPU) |
Fuzzy is cheaper and faster than ML, but ML is 15–20% more accurate for complex descriptions. We combine them in a pipeline.
How the Matching Pipeline Works
The pipeline is a chain of methods where each subsequent method is activated if the previous one didn't produce a result with sufficient confidence. First, exact methods (GTIN, MPN) are applied, then probabilistic ones (fuzzy, ML). This minimizes false positives and maximizes coverage.
Advantages of ML Matching
ML matching is justified when suppliers describe products with different words. For example, one writes "Smartphone Samsung Galaxy S24 256 GB", another "Telefon Samsung S24 256 Gb". Traditional fuzzy won't understand they are the same, but an embedding model captures semantic similarity. In practice, vector matching adds 5–10% accuracy, critical for catalogs with 30% unstructured items.
Data Schema
-- Matching table + embeddings table
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);
Matching Pipeline
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;
}
}
Vector Matcher via 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)),
]));
}
}
Manual Review Interface and Feedback Loop
Products with status pending_review enter the moderator queue. The interface shows the supplier product on the left (name, SKU, photo) and the catalog candidate on the right with a match percentage. Buttons: Confirm, Reject, Find Another. Hotkeys (→ accept, ← reject). An experienced moderator processes 100–150 pairs per hour.
Each moderator decision becomes a training example for the 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);
}
}
}
Performance and Optimization
With a catalog of 100,000+ items, matching cannot be done by brute-forcing all pairs. We use:
- Blocking—first select candidates by brand/category, then match within the block.
- Batch embeddings—request vectors in batches of 100.
- pgvector IVFFlat index—approximate nearest neighbor in milliseconds.
With a catalog of 100,000 items, this architecture saves up to €2,000 per month on moderation.
Want to test matching on your data? Contact us—we'll select the optimal configuration.
What's Included in the Work
- Development of a matching pipeline for your stack (Laravel, Django, Node.js)
- Configuration of GTIN, MPN, fuzzy, and ML matchers
- Database schema design (matches, embeddings, indexes)
- Manual review interface with hotkeys
- Integration with suppliers (API or import)
- API documentation and data schema
- Moderator training (2 hours)
- 1 month support after launch
Implementation Timeline
| Stage | Duration |
|---|---|
| GtinMatcher + MpnBrandMatcher + FuzzyMatcher | 2 days |
| VectorMatcher + pgvector | 2 days |
| Pipeline + manual review queue + interface | 2–3 days |
| Feedback loop + metrics | 1 day |
| Total | 7–8 working days |
Why Order Matching from Us?
We have implemented matching for five aggregators with catalogs from 50,000 to 500,000 items. Our pipeline automatically matches 80–90% of products. The remainder is a manual review queue that takes no more than 2 hours per day. You get a unified catalog without duplicates or confusion.
We'll evaluate your task in 1 day, propose an architecture and accurate timeline. Contact us to discuss the project. Order matching development for your stack.







