← Back to blog

·5 min read·Daniel Diaz

Duplicate detection in master data with a local LLM: rules find the candidates, the model decides

How I built a duplicate-detection pipeline for customer accounts with Python and a local LLM via Ollama: blocking, pairwise classification with a reason, a human review queue, and why it runs on-prem.

Every master-data team has the same backlog: accounts that are the same company typed twice. “Hotel Royal SL”, “HOTEL ROYAL S.L.” and “Royal Hotel (Madrid)” with three different ids, three different owners and three different payment terms. The MDM tool catches the easy ones. What is left is the pile nobody wants: thousands of pairs that might be duplicates, each one needing a human to look at two records and decide.

This is how I turned that pile into a queue a person can actually work through, with Python doing the search and a local language model doing the judgement.

The shape of the problem

Duplicate detection (record linkage, entity resolution — pick your name) has two halves that need very different tools:

  1. Finding candidate pairs. With n records there are n²/2 possible pairs. You cannot look at all of them, and you do not need to: most pairs are obviously different. This half is about recall — do not let a real duplicate escape.
  2. Deciding on each candidate. Given two records that look similar, are they the same entity? This half is about precision and about explaining the call, because a merge in MDM is expensive to undo.

Rules are excellent at the first half and mediocre at the second. Language models are the opposite. So the pipeline splits along that line.

Half one: rules generate the candidates

Plain Python, no model involved:

This step alone removes more than 95 % of the pairs. It is also fully deterministic, which matters when someone asks “why did this pair come up?”.

Half two: a local LLM decides, with a reason

For each grey-zone pair, the model gets both records side by side and a strict instruction: answer with a JSON object, nothing else.

{
  "same_entity": true,
  "confidence": "high",
  "reason": "Same tax id root and postcode; name differs only by legal form and word order."
}

Things that made it work:

Why a local model

The records are customer data. Sending them to a hosted API meant a data-processing review, a contract and a budget line — for a pipeline that runs a few times a month. A local model via Ollama (a small qwen2.5) sidestepped all of it: the data never left the laptop, and the cost was zero.

The constraint was hardware: a work laptop with 32 GB of RAM and no GPU. That rules out big models, but a 3B-class model on CPU answers a pair in a few seconds, and the grey zone after blocking is small enough that an overnight run covers it. The rules are what make a small model enough: the model never sees the easy pairs, only the ones where judgement is needed.

The review queue

Nothing merges automatically. The output is a queue sorted by confidence and impact (how many transactions hang off the accounts), with the two records, the score, the model’s decision and its reason. A steward accepts or rejects; the merge itself happens in the MDM tool, where it belongs. Rejections are kept — they are the best test set for the next version of the prompt and the thresholds.

What goes wrong

What I would tell someone starting this