# 🛒 Product Matcher - Cross-Retailer Matching, No Shared SKU (`that_red_bird/product-matcher`) Actor

🔗 Match the SAME physical product across retailers/marketplaces, no shared SKU. ✅ Brand/model/pack/size extraction, unit canonicalisation, blocking. ⚠️ Different pack count or size never match, even on near-identical titles.

- **URL**: https://apify.com/that\_red\_bird/product-matcher.md
- **Developed by:** [mohamed alaya](https://apify.com/that_red_bird) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Product Matcher

Work out which listings from **different retailers or marketplaces are the same physical
product** when there is no shared SKU. There is exactly one product-matching actor in the
entire Apify Store — this is the second — and it is a problem serious enough that Databricks
and other data platforms publish whitepapers on it. This actor is pure computation: you supply
two or more product lists, nothing is scraped.

### What it actually does

**1. Title normalisation.** Retail titles are full of noise — "Best Seller!", "Free Shipping",
"2-Pack Deal", inconsistent punctuation and case. Marketing phrases are stripped, punctuation is
unified, and the result is used for similarity scoring. Attribute extraction (below) still runs
on the *raw* title first, so a pack count buried inside "2-Pack Deal" is recovered before the
phrase is thrown away.

**2. Structured attribute extraction.** Brand, model/part number, pack count, size/capacity and
colour are pulled from explicit fields when present, and parsed out of the title otherwise:

| Attribute | Example |
|---|---|
| Brand | explicit `brand` field, else the title's leading capitalised word |
| Model / part number | `Model: XYZ`, `SKU ABC-123`, or a bare alphanumeric token like `WH-1000XM4` |
| Pack size / count | "6 Pack", "Pack of 12", "3 x 500ml" |
| Size / capacity | "500ml", "1L", "16oz", "1kg", "12 inch" — **canonicalised** to ml, g or cm so units compare correctly (1L = 1000ml, 16oz ≈ 473ml, 1kg = 1000g) |
| Colour | matched against a fixed colour-word list |
| GTIN/EAN/UPC/ASIN | explicit identifier fields, digit/alnum normalised |

**3. Blocking.** Comparisons are grouped by brand + normalised model (or brand alone, or GTIN),
so N listings never costs O(n²). Oversized blocks are skipped rather than allowed to reintroduce
the blowup, and the run reports how many comparisons this avoided.

**4. Scoring.** A weighted blend of brand similarity, title trigram/token similarity, size
agreement and pack-count agreement. An exact model/part-number match is **near-decisive** and
floors the score high regardless of the rest.

**5. Hard disqualifiers — the actual accuracy lever.** A different pack count, or a different
canonical size, **rejects the pair outright**, before the weighted blend even runs. A 500ml
bottle and a 1L bottle of the identical product are NOT the same listing, no matter how similar
the titles read. This single rule is what separates real product matching from string-similarity
theatre.

**6. GTIN short-circuit.** If both listings carry a GTIN/EAN/UPC/ASIN: agreement is an instant
match, disagreement is an instant non-match — no further scoring runs either way.

### Input

Pass `sources`: an array of `{ name, records }` and/or `{ name, datasetId }`. Two sources is the
normal case (matching store A against store B); three or more also works and produces
transitive clusters (A~B, B~C ⇒ one product) when `outputMode` is `clusters`.

```json
{ "sources": [
  { "name": "storeA", "records": [{ "title": "Sony WH-1000XM4 Headphones, Black" }] },
  { "name": "storeB", "records": [{ "title": "Sony WH-1000XM4 Black Bluetooth Headphones" }] }
], "matchThreshold": 85, "reviewThreshold": 65 }
```

Thresholds are 0–100 integers (Apify input schemas have no float type).

### Output

`match` rows — matched pairs with confidence (0-100), the winning band, and per-field evidence ·
`review` rows — pairs between `reviewThreshold` and `matchThreshold`, reported rather than
matched · `unmatched` rows — listings from each source with no match, grouped by source ·
`cluster` rows (in `clusters` output mode) — one row per distinct product with every matching
listing from every source.

### Honest limitations

- **Text and structured-attribute matching only — there is no image comparison.** Two visually
  identical products with completely different, unparseable titles will not be matched.
- Model/part-number and colour extraction are regex heuristics, not a product taxonomy. Unusual
  title formats (non-English, no delimiters) will fall back to weaker title-similarity evidence.
- GTIN/EAN/UPC/ASIN values are normalised but never checksum-validated — garbage-in agreement
  still counts as agreement.
- Bare "oz" is always treated as a fluid ounce for size canonicalisation (matching the common
  "16oz = 473ml" retail convention), not a weight ounce. Pass an explicit `size` field to avoid
  the ambiguity for weight-in-ounces products.
- Blocking requires at least a brand, model, GTIN, or two shared leading title words. Two listings
  that share none of those are never compared, by design — this is what keeps the run off O(n²).
- Capped at 200,000 records per run.

# Actor input Schema

## `sources` (type: `array`):

Two or more retailer/marketplace product lists. Each entry is { "name": "storeA", "records": \[ {...} ] } and/or { "name": "storeB", "datasetId": "abc123" } — inline records and a dataset ID may be combined on the same source. Every record is a flat object; useful fields are title (or name), brand, model/mpn/sku, gtin/ean/upc/asin, packCount, size (or capacity/weight/volume), color. Any of these are optional — whatever is missing is parsed out of the title instead.

## `matchThreshold` (type: `integer`):

Pairs scoring at or above this are auto-matched. Higher = fewer false matches, more missed pairs. Expressed 0-100; 85 means 0.85. Ignored when GTIN/EAN/UPC/ASIN agree (instant match) or disagree (instant non-match).

## `reviewThreshold` (type: `integer`):

Pairs scoring between this and the match threshold are reported for human review instead of being auto-matched. A confidently wrong match is worse than one that needs a human glance.

## `maxBlockSize` (type: `integer`):

Blocks larger than this (e.g. thousands of listings sharing one generic brand) are skipped to avoid an O(n²) blowup. Other blocking keys still get their chance.

## `compareWithinSource` (type: `boolean`):

By default only listings from DIFFERENT sources are compared (that is the point of cross-retailer matching). Turn this on to also catch duplicate listings inside one source's own list.

## `weights` (type: `object`):

Override the default evidence weights, e.g. {"brand":3,"title":2,"size":2,"pack":1,"colour":1}. Higher weight = that field's agreement counts for more in the blended score. Model/part-number exact match and GTIN agreement are handled separately and are not affected by these weights.

## `outputMode` (type: `string`):

matches = one row per matched pair with confidence and evidence · clusters = one row per distinct product with every matching listing grouped together (useful with 3+ sources).

## `includeReviewPairs` (type: `boolean`):

Emit pairs that scored between reviewThreshold and matchThreshold, with their evidence, so a human can decide.

## `includeUnmatched` (type: `boolean`):

Emit listings from each source that were never matched to anything, so you know what still needs manual mapping.

## Actor input object example

```json
{
  "sources": [
    {
      "name": "storeA",
      "records": [
        {
          "title": "Sony WH-1000XM4 Wireless Noise Cancelling Headphones, Black - Best Seller!",
          "brand": "Sony",
          "gtin": "027242920404"
        },
        {
          "title": "CeraVe Moisturizing Cream, 16 oz - Free Shipping",
          "brand": "CeraVe"
        },
        {
          "title": "Bounty Paper Towels, 2-Ply, 6 Pack"
        }
      ]
    },
    {
      "name": "storeB",
      "records": [
        {
          "title": "Sony WH-1000XM4 Black Bluetooth Headphones (Free Shipping)",
          "brand": "Sony",
          "gtin": "027242920404"
        },
        {
          "title": "CeraVe Moisturizing Cream 19 oz Jar",
          "brand": "CeraVe"
        },
        {
          "title": "Bounty Paper Towels, 2-Ply, 12 Pack - Hot Deal"
        }
      ]
    }
  ],
  "matchThreshold": 85,
  "reviewThreshold": 65,
  "maxBlockSize": 1000,
  "compareWithinSource": false,
  "outputMode": "matches",
  "includeReviewPairs": true,
  "includeUnmatched": true
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `downloadCsv` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `count` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "sources": [
        {
            "name": "storeA",
            "records": [
                {
                    "title": "Sony WH-1000XM4 Wireless Noise Cancelling Headphones, Black - Best Seller!",
                    "brand": "Sony",
                    "gtin": "027242920404"
                },
                {
                    "title": "CeraVe Moisturizing Cream, 16 oz - Free Shipping",
                    "brand": "CeraVe"
                },
                {
                    "title": "Bounty Paper Towels, 2-Ply, 6 Pack"
                }
            ]
        },
        {
            "name": "storeB",
            "records": [
                {
                    "title": "Sony WH-1000XM4 Black Bluetooth Headphones (Free Shipping)",
                    "brand": "Sony",
                    "gtin": "027242920404"
                },
                {
                    "title": "CeraVe Moisturizing Cream 19 oz Jar",
                    "brand": "CeraVe"
                },
                {
                    "title": "Bounty Paper Towels, 2-Ply, 12 Pack - Hot Deal"
                }
            ]
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("that_red_bird/product-matcher").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "sources": [
        {
            "name": "storeA",
            "records": [
                {
                    "title": "Sony WH-1000XM4 Wireless Noise Cancelling Headphones, Black - Best Seller!",
                    "brand": "Sony",
                    "gtin": "027242920404",
                },
                {
                    "title": "CeraVe Moisturizing Cream, 16 oz - Free Shipping",
                    "brand": "CeraVe",
                },
                { "title": "Bounty Paper Towels, 2-Ply, 6 Pack" },
            ],
        },
        {
            "name": "storeB",
            "records": [
                {
                    "title": "Sony WH-1000XM4 Black Bluetooth Headphones (Free Shipping)",
                    "brand": "Sony",
                    "gtin": "027242920404",
                },
                {
                    "title": "CeraVe Moisturizing Cream 19 oz Jar",
                    "brand": "CeraVe",
                },
                { "title": "Bounty Paper Towels, 2-Ply, 12 Pack - Hot Deal" },
            ],
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("that_red_bird/product-matcher").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "sources": [
    {
      "name": "storeA",
      "records": [
        {
          "title": "Sony WH-1000XM4 Wireless Noise Cancelling Headphones, Black - Best Seller!",
          "brand": "Sony",
          "gtin": "027242920404"
        },
        {
          "title": "CeraVe Moisturizing Cream, 16 oz - Free Shipping",
          "brand": "CeraVe"
        },
        {
          "title": "Bounty Paper Towels, 2-Ply, 6 Pack"
        }
      ]
    },
    {
      "name": "storeB",
      "records": [
        {
          "title": "Sony WH-1000XM4 Black Bluetooth Headphones (Free Shipping)",
          "brand": "Sony",
          "gtin": "027242920404"
        },
        {
          "title": "CeraVe Moisturizing Cream 19 oz Jar",
          "brand": "CeraVe"
        },
        {
          "title": "Bounty Paper Towels, 2-Ply, 12 Pack - Hot Deal"
        }
      ]
    }
  ]
}' |
apify call that_red_bird/product-matcher --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,that_red_bird/product-matcher"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Q9CkC4rzUEqlcEgtC/builds/8510ttYM6bwgc9KTc/openapi.json
