Open-source entity resolution and golden-record engine.
Linkuity finds the records that refer to the same person or organization, links them into clusters, and merges each cluster into one explainable golden record β all inside your own environment.
- π Detect duplicates across CSVs, systems, and incremental loads
- π§© Build golden records with source-aware merge rules
- π§ Explain every match with per-field score breakdowns
- π Run locally or privately β no customer data leaves your environment
- β»οΈ Batch or durable incremental resolution as data arrives over time
Prerequisites: .NET 10 SDK.
Resolve the bundled 28-record sample into golden records:
git clone https://github.com/linkuity/linkuity.git
cd linkuity
dotnet run --project src/Linkuity.Cli -- run `
--input samples/people-multi-source/sample.csv `
--profile samples/people-multi-source/people-multi-source.profile.json `
--merge-policy samples/people-multi-source/people-multi-source.merge.json `
--output ./data/output/people-multi-sourceJob 5f3aβ¦c21e completed.
Golden records: β¦/data/output/people-multi-source/golden-records.csv
28 source records from CRM, Marketing, Support, and Billing β 10 golden records.
Add --neo4j-export to also write a graph bundle. Next: the guided
durable MDM quick start.
The demo above is generated from
docs/assets/demo.tapeby the Demo GIF workflow β see docs/assets.
Real systems rarely store a person the same way twice β a nickname here, a typo there, a phone number in a different format:
| id | source | first_name | last_name | phone | address_line | |
|---|---|---|---|---|---|---|
| crm-050 | CRM | Joseph | Martinez | joseph.martinez@example.com | (312) 555-0147 | 1420 Maple Avenue |
| mkt-051 | Marketing | Joe | Martinez | joe.martinez@example.com | 312-555-0147 | 1420 Maple Ave |
| sup-052 | Support | Joesph | Martinez | joseph.martinez@example.com | 312.555.0147 | 1420 Maple Avenue Apt 3B |
| bil-053 | Billing | Joseph | Martinez | joseph.martimez@example.com | 3125550147 | 1420 Maple Ave Apt 3B |
Every mismatch is one you have probably seen in production: a nickname (Joe), a
misspelling (Joesph), a one-letter email typo (martimez), one phone in
four formats, and an apartment number that only reached two systems.
Linkuity normalizes, blocks, scores, and clusters these into a single golden record:
| record_count | member_ids | first_name | phone | address_line | |
|---|---|---|---|---|---|
| 4 | crm-050|mkt-051|sup-052|bil-053 | Joseph | joseph.martinez@example.com | +13125550147 | 1420 Maple Ave Apt 3B |
And it is explainable β first_name wins by consensus (Joseph over the nickname
and the typo), email by CRM source priority (the typo-free address), phone
normalizes to one number, and address takes the Billing row with the unit. A golden
record is a composite, not a copy of any single row. See
how matching works and the
people-multi-source sample.
Most ways to resolve entities fall into one of three buckets, each with a catch: hosted matching services (you ship customer data to someone else's cloud), closed-source products (you can't inspect or tune how matches are decided), and low-level fuzzy-matching libraries (powerful, but you assemble blocking, scoring, clustering, merging, and persistence yourself).
Linkuity is a complete, open-source engine you run yourself:
| Hosted service | Closed product | Match library | Linkuity | |
|---|---|---|---|---|
| Open source | β | β | β | β |
| Data stays in your environment | β | varies | β | β |
| Explainable match decisions | varies | β | you build it | β |
| Whole workflow (merge, golden records, persistence) | β | β | β | β |
| Durable incremental resolution | varies | varies | β | β |
| Native .NET, cross-platform | β | β | varies | β |
- Customer 360 / MDM β one trusted record per customer from many systems
- CRM cleanup & contact deduplication β collapse duplicate people
- Supplier / vendor consolidation β dedupe and standardize organizations
- Organization resolution β match companies across name and domain variants
- Data migrations β dedupe before loading into a new system
- Incremental identity resolution β keep records linked as new data lands
- Graph analysis β export resolved entities to Neo4j and explore the links
flowchart TD
A[CSV / sources] --> B[Normalization]
B --> C[Blocking]
C --> D[Candidate retrieval]
D --> E[Scoring]
E --> F[Decision bands]
F --> G[Clustering]
G --> H[Golden records]
H --> I[Exports & review]
Comparing every record against every other is NΒ² and doesn't scale, so Linkuity only
scores candidate pairs that share a cheap blocking key, combines per-field
similarities into one score, and sorts each pair into a decision band: auto-merge,
review, or no-match. Accepted pairs cluster transitively, and each cluster merges into a
golden record via your source-priority rules β with a per-field breakdown you can read
back. Full detail (with a worked example): how-matching-works.md.
| Batch mode | Durable mode | |
|---|---|---|
| Entry point | linkuity run |
linkuity project / ingest-incremental |
| State | Stateless β resolves and forgets | Persistent store that remembers |
| New data | Reprocesses the whole file | Matched incrementally against stored records |
| History & review | β | Versioned golden records + review queue |
| Store backend | Output files | Local JSON file or PostgreSQL |
| Reach for it when | One-off dedupe, exports, CI | Ongoing MDM as data arrives over time |
The quick start uses batch mode. For durable mode, follow the durable MDM quick start; to back it with PostgreSQL, see durable-postgres.md.
- Matching engine β normalization, blocking (including phonetic), weighted similarity scoring, Lucene-backed candidate retrieval
- Taxonomy-driven β ships
personandorganizationbuilt in; a new content type (e.g. thelocationsample) is a config-only*.profile.json, no code change β see docs/configuration.md - Explainability β persisted per-field score breakdowns and read-back commands
- Golden records β configurable source-priority merge policies; composite output
- Two modes β stateless batch and durable incremental (versioning + review queue)
- Storage β local filesystem + JSON store, or a PostgreSQL durable backend
- Interfaces β CLI, HTTP API, and Docker Compose
- Graph export β Neo4j-ready node/relationship bundle
- Runtime β native .NET 10; cross-platform and single-file deployable
- License β Apache 2.0
The samples/ directory has small, self-contained datasets β each
with a walkthrough and test-pinned expected results β covering multi-source people and
organization merging, unreliable-field handling, name noise, and durable incremental
scenarios. Start with people-multi-source (the
28 β 10 example above).
For a full end-to-end demo on real public data, see showcases/company-resolution β resolve real companies across multiple public data sources.
- Configuration reference β matching profile and merge-policy schema, taxonomies
- How matching works β blocking, scoring, decision bands, merging, tuning
- Tutorials β hands-on durable MDM walkthroughs
- CLI reference β batch usage and building a standalone binary
- HTTP API β synchronous
/runand health endpoints - Durable mode on PostgreSQL β persistent incremental store
- Architecture β components, endpoints, and internals
- Private runtime & private server deployment β local-first and Docker Compose
- Optional Azure-compatible local stack β Aspire + Azurite (optional)
- Benchmark plan β how quality/cost will be measured (no results yet)
Active development. Near-term focus (full plan in docs/roadmap/PLAN.md):
- Packaged CLI releases so users don't build from source
- More realistic sample datasets
- Reproducible quality/cost benchmarks (plan)
- Better match explanations and review workflows
- SDK, connector, and plugin guidance
- Larger-scale private-server deployment examples
Contributions are welcome. Start with CONTRIBUTING.md, which covers development setup and the Contributor License Agreement (CLA.md). Please also review CODE_OF_CONDUCT.md, SECURITY.md, and CHANGELOG.md.
Linkuity is licensed under the Apache License 2.0.
