KYWIO
Deterministic, sub-millisecond entity resolution. Research preview.
bench/fetch_data.py hash-verifies the sequestered test sets against a committed
manifest, so you can prove we did not tamper with the data we report numbers on.
Both reproduce commands were tested from a clean checkout and rebuild our numbers to the digit.
Earlier today this page claimed a “reproducible benchmark” while the repo was still private — that was false, and we said so here until it wasn't.
Results — sequestered test set
One general model, zero-shot: no training data from you. Thresholds were selected on validation and frozen before the test set was read. The test set was read once. Data: Magellan / Zenodo 8164151 (CC-BY-4.0).
| dataset | KYWIO v0.2.0 | rapidfuzz | fine-tuned transformer (literature) |
|---|---|---|---|
| DBLP-ACM | 0.980 | 0.895 | ~0.99 |
| Abt-Buy | 0.863 | 0.199 | ~0.89 |
| company names | 0.794 | 0.735 | — |
| Amazon-Google | 0.609 | 0.406 | ~0.76 |
0.33–0.79 ms per pair (2 vCPU, CPU only) · $0.0008–$0.0013 per million pairs (measured, worst case) · byte-identical across runs, restarts and hash seeds.
Acme Corp. to ACME Corporation at all
(0.385) and lost to plain rapidfuzz on names. Training on real name aliases
fixed that (0.981), but it taught the model that similar strings mean the same
entity — which makes it more permissive. Amazon-Google fell
0.658 → 0.609, and a new false-positive mode appeared
(below). v0.1.0 missed obvious matches; v0.2.0 can merge distinct brands. There was no
free lunch, and we are not pretending there was.
Known limitations — read before trusting a score
0. CRITICAL — the score depends on JSON key order. Found by an external reviewer on 2026-07-13. We missed it.
same record pair, keys serialised in a different order -> different score 400 real pairs x every key permutation: max score spread 0.5902 verdict FLIPS (match <-> no-match) 6 / 400 = 1.5%
The score is not a pure function of your record's content. JSON has no guaranteed key order and many clients reorder freely. Send fields in a stable order. Our determinism claim holds across process restarts and hash seeds — but not across key ordering, which is probably the sense you actually care about.
We are disclosing rather than silently patching: canonicalising key order changes every score the model produces, which would invalidate every number published here. The honest fix is v0.3.0 with a fresh held-out evaluation. Our own claim-audit caught six false claims and missed this one — it took an outside reader. Self-audit is not audit.
1. FALSE POSITIVES on brand-disagreement pairs (new in v0.2.0).
CA Internet Security Suite vs McAfee Internet Security Suite -> 0.788 reported as a MATCH at the default threshold (0.48) ✗ These are DIFFERENT companies. v0.1.0 rejected this correctly.
v0.2.0 can merge distinct brands. If a false
merge is expensive for you, raise the threshold. Calibration: this
false pair scores 0.788, while true matches score
Acme/ACME Corporation 0.981, IBM 0.877,
Microsoft 0.945.
2. Acronyms are learned, not understood.
International Business Machines vs IBM →
0.877 (v0.1.0: 0.000). It works because Wikidata aliases contain many
acronym pairs — not because the model reasons about abbreviation. Expect it to
fail on acronyms unlike those in Wikidata.
- Field-count dependence: FIXED in v0.2.0. The same true pair now scores 0.977 with 1 field and 0.973 with 3 (v0.1.0: 0.412 vs 0.862 — it flipped across the threshold purely by adding columns).
- Amazon-Google regressed 0.658 → 0.609. Same cause as the false positives: the permissiveness that fixed names hurts the hardest product set.
- No blocking: dedupe is O(n²), capped at 1,000 records. Not a large-scale linkage engine.
Our default threshold (0.48) maximises F1 on OUR mixed distribution, not yours. Score your own labelled pairs and pick your own.
Method
- Every success criterion — and the consequence of failing it — was pre-registered before the code existed.
- The test sets are hash-pinned and read-only; the loader refuses to open
them without an explicit flag and a stated claim, and every read is logged.
Correction (2026-07-13, found by external review): this said "read
twice". The three product test sets were each read three times
(EXP-008, EXP-009, EXP-013). After the second we wrote in our own log "the test set
is now closed; any further material model change requires a fresh held-out split"
— and 63 minutes later touched it again to evaluate v0.2.0, justifying it as "a
different claim". The rule blocked shipping, so we reinterpreted the rule.
Every touch is in the log, which is how a reviewer caught us. Treat our absolute F1 as
mildly optimistic; weight the comparison against
rapidfuzz(which never saw the test set) more heavily. - Thresholds selected on validation and frozen. No threshold search on test. Test F1 came in below validation on every dataset — which is what sequestering is for, and we publish the drop.
- The experiment log includes every negative result, including three baselines we built badly, got flattering numbers from, and had to throw away. Read it here.
Use it
MCP (works today, no hosting needed):
git clone https://github.com/vanekyj/kywio && cd kywio python3 -m venv .venv .venv/bin/pip install -r requirements.txt # numpy, rapidfuzz, joblib (~84MB, measured) claude mcp add kywio -- "$PWD/.venv/bin/python" "$PWD/api/mcp_server.py"
Tools: match_records, dedupe_records, kywio_info.
The model (1.2 MB) is committed — nothing to download or train. scikit-learn is
not required: inference is pure numpy.
The install command published before
2026-07-13 was broken — it used bare python and a relative path, and would
have failed for everyone. We caught it by testing our own instructions. This one is
tested.
REST: not yet publicly hosted. openapi.json · agent card · llms.txt