# 🗂️ Product Taxonomy Classifier - Deep Category Trees (`that_red_bird/product-taxonomy-classifier`) Actor

⚡ Classify thousands of products into a deep, multi-level category tree with no LLM. ✅ Walks the taxonomy level by level so every path is internally consistent, scores with TF-IDF-ish cosine + token overlap + trigram similarity, and routes low-confidence items to a review queue instead of.

- **URL**: https://apify.com/that\_red\_bird/product-taxonomy-classifier.md
- **Developed by:** [mohamed alaya](https://apify.com/that_red_bird) (community)
- **Categories:** E-commerce, AI
- **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 Taxonomy Classifier

Classify thousands of products into a **deep, multi-level category tree** — deterministically,
with **no LLM and no embeddings model**. This is the actor behind "sort our catalog into the
category tree the storefront/marketplace actually uses" without paying per-token for an LLM call
on every SKU.

### What it actually does

**1. Hierarchy, done properly.** The moat is not "guess a category" — it's guessing *correctly,
one level at a time*. Level 1 is scored only against the taxonomy's root categories. Once a root
is accepted, level 2 is scored **only against that root's own children** — a "Headphones" leaf
under "Automotive" can never be reached even if its keywords happen to overlap, because it is
never a candidate once "Electronics" wasn't the level-1 pick. This repeats down the tree, so
every emitted path is walked edge by edge and is **always internally consistent**.

**2. Two taxonomy input shapes, freely mixed.**

```json
["Electronics > Audio > Headphones", "Electronics > Audio > Speakers"]
```

or a nested tree:

```json
[{ "name": "Electronics", "children": [
  { "name": "Audio", "children": [
    { "name": "Headphones", "keywords": ["earbuds", "anc", "over-ear"] }
  ] }
] }]
```

**3. Scoring blends three dependency-free signals**, computed per level against the current
candidate set only (see `src/classify.js`):

| Signal | What it catches |
|---|---|
| TF-IDF-ish cosine | Term weighting where IDF is built from the *sibling* categories competing at that level, so the terms that matter are the ones that discriminate between the current candidates |
| Token overlap | How much of a category's own vocabulary (name + keywords + hints) shows up in the product text |
| Character-trigram Dice | Typo/plural/word-order tolerance that pure token matching misses |

**4. Confidence per level, not just per product.** Confidence blends the winning score's raw
strength with its separation from the runner-up — a high score that's barely ahead of the next
category is exactly the ambiguous case the review queue exists for.

**5. A review queue, not a forced guess.** The first level where nothing clears `reviewBelow`
stops the walk there. The product is queued for review with the partial path reached, the reason,
and the top-3 candidates that were considered — never silently mis-filed.

**6. Optional keyword hints.** Pass extra keywords per category (by full path or bare name) to
boost recall for categories whose name alone is too generic ("Boots" vs. hiking/rain/work boots).

**7. Reporting.** Per-category counts (`type: "categoryCount"`) and a `SUMMARY` with the review
rate, taxonomy shape, and top categories used.

### Input

```json
{
  "products": [{ "title": "Sony WH-1000XM5 Headphones", "description": "...", "tags": ["audio"] }],
  "taxonomy": ["Electronics > Audio > Headphones", "Electronics > Audio > Speakers"],
  "keywordHints": { "Electronics > Audio > Headphones": ["earbuds", "anc"] },
  "reviewBelow": 55,
  "tfidfWeight": 50, "overlapWeight": 30, "trigramWeight": 20
}
```

`reviewBelow` and the three weights are 0–100 integers (Apify input schemas have no float type);
weights are normalized against each other, so only their ratio matters.

### Output

`type: "classified"` rows carry the full `path` array, `fullPath` string, per-level
`levelConfidences`, and an overall `confidence` (the weakest level in the chain — a deep path is
only as trustworthy as its worst step). `type: "review"` rows carry the reason and best-guess
candidates. `type: "categoryCount"` rows summarize where products landed.

### Honest limitations

- **Lexical, not semantic.** There is no embeddings model here — matching is TF-IDF/token/trigram
  based. "Footwear" and "Shoes" are related to a human but share almost no characters or tokens;
  give the taxonomy explicit `keywords` or `keywordHints` to bridge synonyms the algorithm can't
  infer on its own. This is a deliberate trade-off for a fast, deterministic, LLM-free actor.
- Very short/sparse product text (a bare SKU with no description or tags) has little for any
  lexical method to work with and will often land in review rather than being force-fitted.
- Confidence is a relative signal (how well the winner beat the field), not a calibrated
  probability — tune `reviewBelow` against your own precision/recall needs.
- Capped at 200,000 products per run.

# Actor input Schema

## `products` (type: `array`):

The products to classify, as an array of flat objects with a title, description and/or tags. Combine freely with productDatasetIds.

## `productDatasetIds` (type: `array`):

Apify dataset IDs to pull products from, in addition to (or instead of) the inline products list.

## `taxonomy` (type: `array`):

Your category tree. Either an array of path strings ("Electronics > Audio > Headphones") or a nested tree of {"name":"Electronics","keywords":\[...],"children":\[...]} objects. Shapes can be mixed. Classification is always constrained level by level to the accepted parent's own children.

## `keywordHints` (type: `object`):

Optional extra keywords to boost specific categories, keyed by full category path or by bare category name, e.g. {"Electronics > Audio > Headphones": \["earbuds", "anc", "over-ear"]}. Merged with each category's own name for scoring.

## `idField` (type: `string`):

Field on each product to use as its output ID. Defaults to an auto-generated P000001-style ID when omitted or blank.

## `titleField` (type: `string`):

Field holding the product's title/name. Carries the strongest classification signal.

## `descriptionField` (type: `string`):

Field holding the product's longer description text.

## `tagsField` (type: `string`):

Field holding curated tags/attributes (string or array). Weighted between title and description.

## `reviewBelow` (type: `integer`):

If the best-matching category at any level scores below this confidence, the product stops there and is sent to the review queue instead of being forced into a low-confidence guess. Expressed 0-100; 55 means 0.55.

## `tfidfWeight` (type: `integer`):

Relative weight of TF-IDF-ish cosine similarity in the score. Weights are normalized against overlapWeight and trigramWeight, so only the ratio between them matters.

## `overlapWeight` (type: `integer`):

Relative weight of how much of a category's own vocabulary (name + keywords + hints) is found in the product text.

## `trigramWeight` (type: `integer`):

Relative weight of character-trigram Dice similarity, which tolerates typos and word-order drift that token matching misses.

## `includeCategoryCounts` (type: `boolean`):

Emit one row per used category with how many products landed there.

## `includeReviewItems` (type: `boolean`):

Emit rows for products that could not be confidently classified, with the reason and the top candidate categories that were considered.

## Actor input object example

```json
{
  "products": [
    {
      "title": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
      "description": "Over-ear Bluetooth headphones with active noise cancellation",
      "tags": [
        "audio",
        "wireless",
        "bluetooth"
      ]
    },
    {
      "title": "KitchenAid 5-Quart Stand Mixer",
      "description": "Countertop mixer for baking with dough hook and whisk attachments",
      "tags": [
        "kitchen",
        "baking"
      ]
    },
    {
      "title": "Nike Air Zoom Pegasus Running Shoes",
      "description": "Men's lightweight running shoes with breathable mesh upper",
      "tags": [
        "footwear",
        "running"
      ]
    }
  ],
  "taxonomy": [
    "Electronics > Audio > Headphones",
    "Electronics > Audio > Speakers",
    "Home & Kitchen > Small Appliances > Mixers",
    "Home & Kitchen > Small Appliances > Blenders",
    "Clothing & Shoes > Footwear > Running Shoes",
    "Clothing & Shoes > Footwear > Boots"
  ],
  "titleField": "title",
  "descriptionField": "description",
  "tagsField": "tags",
  "reviewBelow": 55,
  "tfidfWeight": 50,
  "overlapWeight": 30,
  "trigramWeight": 20,
  "includeCategoryCounts": true,
  "includeReviewItems": 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 = {
    "products": [
        {
            "title": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
            "description": "Over-ear Bluetooth headphones with active noise cancellation",
            "tags": [
                "audio",
                "wireless",
                "bluetooth"
            ]
        },
        {
            "title": "KitchenAid 5-Quart Stand Mixer",
            "description": "Countertop mixer for baking with dough hook and whisk attachments",
            "tags": [
                "kitchen",
                "baking"
            ]
        },
        {
            "title": "Nike Air Zoom Pegasus Running Shoes",
            "description": "Men's lightweight running shoes with breathable mesh upper",
            "tags": [
                "footwear",
                "running"
            ]
        }
    ],
    "taxonomy": [
        "Electronics > Audio > Headphones",
        "Electronics > Audio > Speakers",
        "Home & Kitchen > Small Appliances > Mixers",
        "Home & Kitchen > Small Appliances > Blenders",
        "Clothing & Shoes > Footwear > Running Shoes",
        "Clothing & Shoes > Footwear > Boots"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("that_red_bird/product-taxonomy-classifier").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 = {
    "products": [
        {
            "title": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
            "description": "Over-ear Bluetooth headphones with active noise cancellation",
            "tags": [
                "audio",
                "wireless",
                "bluetooth",
            ],
        },
        {
            "title": "KitchenAid 5-Quart Stand Mixer",
            "description": "Countertop mixer for baking with dough hook and whisk attachments",
            "tags": [
                "kitchen",
                "baking",
            ],
        },
        {
            "title": "Nike Air Zoom Pegasus Running Shoes",
            "description": "Men's lightweight running shoes with breathable mesh upper",
            "tags": [
                "footwear",
                "running",
            ],
        },
    ],
    "taxonomy": [
        "Electronics > Audio > Headphones",
        "Electronics > Audio > Speakers",
        "Home & Kitchen > Small Appliances > Mixers",
        "Home & Kitchen > Small Appliances > Blenders",
        "Clothing & Shoes > Footwear > Running Shoes",
        "Clothing & Shoes > Footwear > Boots",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("that_red_bird/product-taxonomy-classifier").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 '{
  "products": [
    {
      "title": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
      "description": "Over-ear Bluetooth headphones with active noise cancellation",
      "tags": [
        "audio",
        "wireless",
        "bluetooth"
      ]
    },
    {
      "title": "KitchenAid 5-Quart Stand Mixer",
      "description": "Countertop mixer for baking with dough hook and whisk attachments",
      "tags": [
        "kitchen",
        "baking"
      ]
    },
    {
      "title": "Nike Air Zoom Pegasus Running Shoes",
      "description": "Men'\''s lightweight running shoes with breathable mesh upper",
      "tags": [
        "footwear",
        "running"
      ]
    }
  ],
  "taxonomy": [
    "Electronics > Audio > Headphones",
    "Electronics > Audio > Speakers",
    "Home & Kitchen > Small Appliances > Mixers",
    "Home & Kitchen > Small Appliances > Blenders",
    "Clothing & Shoes > Footwear > Running Shoes",
    "Clothing & Shoes > Footwear > Boots"
  ]
}' |
apify call that_red_bird/product-taxonomy-classifier --silent --output-dataset

```

## MCP server setup

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

```

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/hTvbICftgi8tPam18/builds/dYtkpVQigPQJfTYW4/openapi.json
