# Fuzzy String Matcher and Near-Duplicate Finder (`firebrick_zamia/fuzzy-string-matcher`) Actor

Match strings against a candidate list or group near-duplicates with Levenshtein, Jaro-Winkler, Dice or token-set similarity and a score threshold. Pure compute, no network, no AI.

- **URL**: https://apify.com/firebrick_zamia/fuzzy-string-matcher.md
- **Developed by:** [Aadam Ahmed](https://apify.com/firebrick_zamia) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Fuzzy Match and Deduplicate Strings: Name Matching Tool

**Fuzzy-match names, companies or product titles against a reference list, or group near-duplicates in one list, using Levenshtein, Damerau-Levenshtein, Jaro-Winkler, Dice or token-set similarity with a score threshold. Deterministic, no web calls, no AI.**

### What you get

Match mode: per query, the best candidates above your threshold with 0-1 scores and positions. Dedupe mode: per string, a `cluster_id`, the canonical string and the similarity to it.

### Use cases

- Name matching and record linkage: link messy CRM, supplier or customer names to a master list.
- Deduplicate company names, product titles or addresses before import.
- Typo-tolerant lookup for an AI agent (`Jon Smyth` finds `John Smith`).

Match messy strings (company names, people, product titles, addresses) against a reference list, or group near-duplicates inside one list. **Pure compute: no web calls, no AI model, deterministic**, so the same input always gives the same scores. Callable by developers and AI agents through Apify's MCP server (https://mcp.apify.com).

Algorithms (all scored 0 to 1): Levenshtein, Damerau-Levenshtein (optimal string alignment, counts a swap of two adjacent letters as one edit), Jaro-Winkler, Dice over character bigrams, token sort and token set (ignore word order and extra words).

- **match mode**: for each query, the best candidates at or above your threshold, with scores and candidate positions, and the closest candidate when none qualify.
- **dedupe mode**: one record per string with a `cluster_id`, the canonical (first-seen) string and its similarity to it.
- Optional normalisation first: ignore case, strip accents, ignore punctuation, collapse spaces.

### Input

| Field | Meaning |
|---|---|
| `mode` | `match` (default) or `dedupe` |
| `queries` | strings to look up, or to group in dedupe mode (up to 20,000, each up to 500 characters) |
| `candidates` | match mode reference list (up to 20,000) |
| `algorithm`, `threshold` | similarity measure and minimum score (default Jaro-Winkler, 0.85) |
| `maxMatches` | matches per query (1-20) |
| `ignoreCase`, `stripAccents`, `ignorePunctuation`, `collapseWhitespace` | normalisation, all on by default |

### Example input and output

Input (bundled `test-input.json`): four queries, five candidates, `algorithm: token_set`, `threshold: 0.6`, `maxMatches: 2`. Real output with `token_set`, threshold 0.6, for the query `Mohamed Al Rashid`:

```json
{
  "kind": "match", "index": 1, "query": "Mohamed Al Rashid", "matched": true,
  "best_match": "Muhammad Al-Rashid", "best_score": 0.8333,
  "matches": [ { "candidate": "Muhammad Al-Rashid", "candidate_index": 1, "score": 0.8333 } ],
  "closest_below_threshold": null, "algorithm": "token_set", "threshold": 0.6, "error": null
}
```

### Limitations (read before relying on it)

- **Scores are string similarity, not identity.** `Mohamed Al Rashid` and `Muhammad Al-Rashid` can score well, but two different people can too. Always review matches before merging records, and pick the threshold on your own sample data; there is no universal good value.
- **No phonetic or language knowledge.** It does not transliterate (Arabic and Latin spellings of the same name will not match; the sibling Arabic Text Toolkit Actor offers a name key for that), expand abbreviations or understand synonyms.
- **Dedupe is greedy and order-dependent.** Each string is compared with the first member (canonical) of each existing cluster, so it is not transitive and the outcome can change if you reorder the list.
- **Size limits keep runs cheap**: match mode allows at most 5,000,000 query x candidate comparisons; dedupe allows lists of up to about 3,000 strings in the worst case (all unique). Split bigger jobs.
- The token-set measure is the author's own implementation in the spirit of the FuzzyWuzzy `token_set_ratio`; numbers are not guaranteed identical to that library. Jaro-Winkler uses the standard 0.1 prefix scale, 4-character prefix cap and 0.7 boost threshold (Winkler 1990). Levenshtein is computed on Unicode code points.

### Pricing (pay per result)

Pay-per-event with Apify's built-in dataset-item event: each query (match) or each string (dedupe) is one result. The live price is set in Apify Console and shown on the Store page (see Store). Planning figure in our notes: USD 1.00 per 1,000 results. Existing fuzzy dedupe Actors work on whole datasets rather than plain strings (see `apify/RESEARCH-3.md`; prices were not visible in the search results).

### FAQ

**Does it call any service?** No.

**Can it deduplicate an Apify dataset directly?** No; pass the strings (for example a name column) as input.

**Which algorithm should I pick?** `token_set` or `token_sort` for names and companies where word order or extra words vary; `jaro_winkler` for short names with typos. Test on your own sample.

**Will it merge "Ltd" and "Limited"?** No, it does not expand abbreviations. Example from the code: in dedupe mode with `token_set` and threshold 0.9, `Acme Corporation Ltd` and `ACME Corporation Ltd.` share a cluster, but `Acme Corporation Limited` starts its own.

**Example score:** `Jon Smyth` vs `John Smith` with `jaro_winkler` scores 0.917.

# Actor input Schema

## `mode` (type: `string`):

match: find the best candidates for every query. dedupe: group near-duplicate strings within queries.

## `queries` (type: `array`):

Strings to look up (match mode) or to group (dedupe mode), one per line (up to 20,000, each up to 500 characters). Each string is one billed result.

## `candidates` (type: `array`):

Match mode: the reference list to search in, one per line (up to 20,000). queries x candidates must not exceed 5,000,000 comparisons.

## `algorithm` (type: `string`):

Similarity measure, always scored 0 to 1. Jaro-Winkler suits names; Levenshtein suits typos; Dice suits longer text; token_sort and token_set ignore word order.

## `threshold` (type: `number`):

Candidates or duplicates must score at least this (0 to 1) to count as a match.

## `maxMatches` (type: `integer`):

Match mode: maximum matches returned per query.

## `ignoreCase` (type: `boolean`):

Compare strings case-insensitively.

## `stripAccents` (type: `boolean`):

Remove diacritics before comparing (é becomes e). Does not affect Arabic letters or their marks beyond combining marks.

## `ignorePunctuation` (type: `boolean`):

Treat punctuation as spaces before comparing.

## `collapseWhitespace` (type: `boolean`):

Trim and collapse repeated spaces before comparing.

## Actor input object example

```json
{
  "mode": "match",
  "queries": [
    "Acme Corporation Ltd",
    "Mohamed Al Rashid",
    "Jon Smyth"
  ],
  "candidates": [
    "ACME Corp. Ltd.",
    "Muhammad Al-Rashid",
    "John Smith",
    "Globex International"
  ],
  "algorithm": "jaro_winkler",
  "threshold": 0.85,
  "maxMatches": 3,
  "ignoreCase": true,
  "stripAccents": true,
  "ignorePunctuation": true,
  "collapseWhitespace": true
}
```

# Actor output Schema

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

All result records.

# 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 = {
    "queries": [
        "Acme Corporation Ltd",
        "Mohamed Al Rashid",
        "Jon Smyth"
    ],
    "candidates": [
        "ACME Corp. Ltd.",
        "Muhammad Al-Rashid",
        "John Smith",
        "Globex International"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("firebrick_zamia/fuzzy-string-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 = {
    "queries": [
        "Acme Corporation Ltd",
        "Mohamed Al Rashid",
        "Jon Smyth",
    ],
    "candidates": [
        "ACME Corp. Ltd.",
        "Muhammad Al-Rashid",
        "John Smith",
        "Globex International",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("firebrick_zamia/fuzzy-string-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 '{
  "queries": [
    "Acme Corporation Ltd",
    "Mohamed Al Rashid",
    "Jon Smyth"
  ],
  "candidates": [
    "ACME Corp. Ltd.",
    "Muhammad Al-Rashid",
    "John Smith",
    "Globex International"
  ]
}' |
apify call firebrick_zamia/fuzzy-string-matcher --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,firebrick_zamia/fuzzy-string-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/oRQSl5l0o60tVbsLM/builds/q6moHmBbk7zXTDBl0/openapi.json
