💻 Tilores Studio ist jetzt verfügbar. Führen Sie Entity Resolution lokal auf Ihrem Rechner aus.Kostenlos laden

← Zurück zum Blog
Entity Resolution 11. August 2023 · 9 Min. Lesezeit

Die Komplexität der Entity-Resolution-Implementierung

Stefan Berkner
Stefan Berkner
CTO, Tilores
Die Komplexität der Entity-Resolution-Implementierung

Kurzfassung

  • Eine typische Entity-Resolution-Implementierung beginnt mit dem Profiling der Datensätze und Fuzzy Matching, reduziert dann über Blocking die Zahl der Kandidatenpaare, bewertet wahrscheinliche Übereinstimmungen, gruppiert sie zu Entitäten und speichert die Belege, die später nötig sind, um Entitäten zu erklären, zu aktualisieren, zu trennen oder zu löschen.
  • Das Schwierige ist nicht der eine Algorithmus zum Stringvergleich. Das Schwierige ist, das quadratische Wachstum der Paare zu beherrschen, Blocking-Schlüssel zu wählen, die keine echten Übereinstimmungen übersehen, transitive Verknüpfungen zu behandeln und Entitäten aktuell zu halten, während sich Datensätze ändern.
  • Ein Eigenbau kann für eng abgegrenzte, batch-orientierte Datenbestände funktionieren. Eine dedizierte Entity-Resolution-Schicht passt in der Regel besser, wenn Teams systemübergreifendes Kunden-Matching, Entitätsbildung bereits bei der Ingestion, Löschungen, Erklärbarkeit und API-Zugriff auf den aktuellen einheitlichen Kundenkontext benötigen.

Inhaltsverzeichnis

  1. Kurze Antwort
  2. Nächster Schritt mit Tilores
  3. Entscheidungshilfe
  4. Was passiert in einer Entity-Resolution-Pipeline?
  5. Warum Blocking die zentrale Skalierungsentscheidung ist
  6. Wo Graph-Logik in die Implementierung kommt
  7. Wie Sie selbst bauen oder kaufen bewerten
  8. Häufige Fragen

Kurze Antwort

Eine typische Entity-Resolution-Implementierung durchläuft sechs praktische Stufen: eingehende Datensätze normalisieren, Fuzzy-Matching-Regeln oder -Modelle wählen, die Kandidatenmenge durch Blocking reduzieren, Kandidatenpaare vergleichen, übereinstimmende Datensätze mit Graph-Logik zu Entitäten zusammensetzen und die entstandene Entität samt Match-Belegen für nachgelagerte Systeme bereitstellen.

Schwierig wird die Implementierung, sobald der Datenbestand wächst, sich verändert oder prüfbar sein muss. Teams müssen Blocking-Kompromisse, False Positives, False Negatives, transitive Übereinstimmungen, Graphgröße, Echtzeit-Ingestion, Löschanfragen und die operativen Kosten für das erneute Ausführen oder Reparieren von Batches im Griff behalten.

Nächster Schritt mit Tilores

Wählen Sie den Schritt, der zu Ihrer Evaluierungsphase passt.

Demo buchen Tilores Studio kostenlos testen

Entscheidungshilfe

FrageTilores passt, wenn …Worauf Sie achten sollten
Lässt sich jeder Datensatz mit jedem anderen vergleichen?Der Datenbestand ist groß genug, dass Kandidatenauswahl, Blocking und Entitätsbildung als produktive Architektur behandelt werden müssen und nicht als einmaliges Skript.Ein naiver paarweiser Vergleich wächst quadratisch. Eine Regel, die bei Tausenden von Datensätzen funktioniert, kann bei Millionen unbrauchbar werden.
Wie häufig ändern sich Datensätze?Neue, geänderte oder gelöschte Datensätze müssen aufgelöste Entitäten aktualisieren, ohne auf den nächsten vollständigen Monatslauf zu warten.Auch Echtzeit- und inkrementelle Ansätze brauchen sorgfältiges Blocking, nachvollziehbare Merges, definiertes Split-Verhalten und eine saubere Löschbehandlung.
Brauchen Teams erklärbare Übereinstimmungen?Prüfer, Analysten oder nachgelagerte Systeme müssen verstehen, warum Datensätze verknüpft wurden, und den aktuellen einheitlichen Kundenkontext über eine API abrufen.Werden Match-Belege verworfen, lassen sich Entitäten später schwerer prüfen, trennen, debuggen oder verbessern.
Ist der Anwendungsfall eng begrenzt oder geschäftskritisch?Entity Resolution ist zentral für Kunden-, Betrugs-, Risiko-, Compliance- oder KI-Agenten-Prozesse über mehrere Systeme hinweg.Ein kleiner statischer Batch kann eine Eigenentwicklung rechtfertigen. Eine veränderliche Umgebung mit vielen Quellen legt Wartungs- und Genauigkeitskosten meist schnell offen.

Was passiert in einer Entity-Resolution-Pipeline?

Die Pipeline beginnt meist damit, Datensätze zu standardisieren, damit sich Felder wie Namen, Adressen, E-Mail-Adressen, Datumsangaben und Identifikatoren konsistent vergleichen lassen. Anschließend bildet sie Kandidatenpaare, wendet Fuzzy-Matching-Regeln oder -Modelle an, entscheidet, welche Paare wahrscheinlich übereinstimmen, und fügt diese paarweisen Entscheidungen zu Entitätsgruppen zusammen.

Das fertige System muss mehr speichern als eine gewinnende Entitäts-ID. Es sollte genügend Match-Belege, Kanten, Schwellenwerte und Historie der Quelldatensätze vorhalten, um Erklärbarkeit, das Trennen von Entitäten, Datenlöschung und späteres Feintuning der Regeln zu unterstützen.

Warum Blocking die zentrale Skalierungsentscheidung ist

Blocking reduziert die Zahl der Datensatzpaare, die aufwendig verglichen werden müssen. Statt jeden Datensatz mit jedem anderen zu vergleichen, gruppiert das System Datensätze anhand von Schlüsseln wie Stadt, phonetischem Namen, Q-Grammen, E-Mail-Fragmenten oder anderen domänenspezifischen Signalen in Kandidaten-Buckets.

Gutes Blocking erhöht die Geschwindigkeit, ohne zu viele echte Übereinstimmungen zu verlieren. Schlechtes Blocking erzeugt entweder riesige Buckets, die das quadratische Problem zurückbringen, oder zu enge Buckets, die Datensätze trennen, die eigentlich zusammengehören.

Wo Graph-Logik in die Implementierung kommt

Paarweise Übereinstimmungen sind nicht dasselbe wie aufgelöste Entitäten. Wenn Datensatz A mit Datensatz B übereinstimmt und Datensatz B mit Datensatz C, muss das System unter Umständen alle drei als eine zusammenhängende Komponente behandeln – auch wenn A und C nie direkt verglichen wurden oder nicht denselben Schwellenwert erreicht haben.

Diese Graphschicht ist nützlich für Erklärbarkeit und Korrekturen, wirft aber auch operative Fragen auf: Wie werden Entitäten zusammengeführt? Wie werden sie getrennt, wenn eine Verknüpfung entfällt? Wie viele Kantenbelege sollen aufbewahrt werden? Und wie bleibt der Graph beherrschbar, wenn sich viele Datensätze stark ähneln?

Wie Sie selbst bauen oder kaufen bewerten

Ein Eigenbau ist am ehesten realistisch, wenn das Datenvolumen klein ist, die Matching-Regeln stabil sind und verzögerte Batch-Verarbeitung akzeptabel ist. Das Team braucht trotzdem klare Verantwortung für Regel-Tuning, Blocking-Design, Performancetests, Monitoring und Löschbehandlung.

Eine dedizierte Entity-Resolution-Schicht passt besser, wenn das Unternehmen zuverlässiges Matching über unsaubere Kundendaten hinweg, häufige Aktualisierungen, erklärbare Ergebnisse und einen einheitlichen Kundenkontext braucht, der Anwendungen oder KI-Agenten zur Verfügung steht, ohne die Matching-Engine von Grund auf neu zu bauen.

Entity Resolution ist der Prozess, bei dem festgestellt wird, ob zwei oder mehr Datensätze in einem Datenbestand dieselbe reale Entität bezeichnen – häufig eine Person oder ein Unternehmen. Auf den ersten Blick wirkt Entity Resolution wie eine vergleichsweise einfache Aufgabe: Zeigt man etwa einem kleinen Kind zwei Fotos einer Person, kann es mit ziemlich hoher Genauigkeit sagen, ob dieselbe Person darauf zu sehen ist. Für Computer gilt dasselbe: Zwei Datensätze mit Attributen wie Namen, Adressen, E-Mail-Adressen und so weiter zu vergleichen, ist ohne Weiteres möglich. Je tiefer man jedoch in das Thema eintaucht, desto anspruchsvoller wird es: Verschiedene Matching-Algorithmen müssen bewertet werden, der Umgang mit Millionen oder Milliarden von Datensätzen bedeutet quadratische Komplexität – von Echtzeit- und Datenlöschungs-Anwendungsfällen ganz zu schweigen.

Fuzzy Text Matching

Beginnen wir damit, zwei Datensätze des berühmten Künstlers Vincent Van Gogh zu vergleichen – oder war es Van Gough?

Im zweiten Datensatz stecken einige Fehler (abgesehen davon, dass er ein Jahrhundert später geboren wurde und eine E-Mail-Adresse besitzt): Der Name ist falsch geschrieben, der Geburtstag ist vertauscht, die Postleitzahl fehlt und die E-Mail-Adresse weicht leicht ab.

Wie lassen sich diese Werte also vergleichen? Wären die Namen identisch, würde ein einfacher Stringvergleich genügen. Da das nicht der Fall ist, brauchen wir fortgeschrittenes Fuzzy Matching. Für textbasiertes Fuzzy Matching gibt es viele verschiedene Algorithmen, die sich grob in drei Gruppen einteilen lassen. Phonetische Algorithmen betrachten, wie ähnlich ein Text ausgesprochen wird. Die bekanntesten sind Soundex und Metaphone, die überwiegend für englische Texte eingesetzt werden; für andere Sprachen existieren Varianten, etwa die Kölner Phonetik für das Deutsche. Textdistanz-Algorithmen definieren typischerweise, wie viele Zeichen eines Textes geändert werden müssen, um zum anderen Text zu gelangen. Levenshtein- und Hamming-Distanz sind zwei bekannte Vertreter dieser Gruppe. Ähnlichkeitsalgorithmen wie die Kosinus-Ähnlichkeit oder der Jaccard-Index berechnen die strukturelle Ähnlichkeit von Texten und geben sie oft als Prozentwert an.

Für diesen Artikel verwenden wir einen sehr einfachen Ansatz: nur eine Levenshtein-Distanz auf dem Namen und Gleichheit auf der Stadt. Dieses und alle folgenden Beispiele nutzen Golang als Programmiersprache und wo möglich bestehende Bibliotheken. Die Übertragung nach Python, Java oder in eine andere Sprache sollte trivial sein. Außerdem wird nur auf dem Attribut „name“ gematcht. Weitere Attribute hinzuzufügen oder das Ganze konfigurierbar zu machen, ist nicht Gegenstand dieses Artikels.

package main

import (
	"fmt"

	"github.com/hbollon/go-edlib"
)

type Record struct {
	ID int
	Name string
	City string
}

func matches(a, b Record) bool {
	distance := edlib.LevenshteinDistance(a.Name, b.Name)
	return distance <= 3 && a.City == b.City
}

func main() {
	a := Record{
		Name: "Vincent Van Gogh",
		City: "Paris",
	}
	b := Record{
		Name: "Vince Van Gough",
		City: "Paris",
	}
	if matches(a, b) {
		fmt.Printf("%s and %s are probably the same person\n", a.Name, b.Name)
	} else {
		fmt.Printf("%s and %s are probably not the same person\n", a.Name, b.Name)
	}
}

Im Go Playground ausprobieren: https://go.dev/play/p/IJtanpXEdyu

Die Levenshtein-Distanz zwischen den beiden Namen beträgt genau 3. Das liegt an den drei zusätzlichen Zeichen („en“ im Vornamen und „u“ im Nachnamen). Beachten Sie, dass das bei dieser konkreten Eingabe funktioniert. Perfekt ist es allerdings bei Weitem nicht. Die Namen „Joe Smith“ und „Amy Smith“ haben ebenfalls eine Levenshtein-Distanz von drei, sind aber offensichtlich nicht dieselbe Person. Ein Distanzalgorithmus in Kombination mit einem phonetischen Algorithmus könnte das Problem lösen, geht aber über den Rahmen dieses Artikels hinaus.

Bei einem regelbasierten statt einem ML-basierten Ansatz ist die Wahl der Algorithmen, die für Ihren Anwendungsfall die besten Ergebnisse liefern, der entscheidende Faktor für den Geschäftserfolg. Genau hier sollten Sie die meiste Zeit verbringen. Wie wir gleich sehen werden, gibt es leider eine Menge anderer Dinge, die Sie von der Optimierung dieser Regeln ablenken, wenn Sie sich entscheiden, eine Entity-Resolution-Engine selbst zu entwickeln.

Naive Entity Resolution

Da wir nun wissen, wie sich zwei Datensätze vergleichen lassen, müssen wir alle Datensätze finden, die zueinander passen. Der einfachste Ansatz besteht darin, jeden Datensatz schlicht mit allen anderen zu vergleichen. Für dieses Beispiel arbeiten wir mit zufällig gewählten Namen und Städten. Bei den Namen erzwingen wir bis zu drei Fehler (ein beliebiges Zeichen wird durch x ersetzt).

var firstNames = [...]string{"Wade", "Dave", "Seth", "Ivan", "Riley", "Gilbert", "Jorge", "Dan", "Brian", "Roberto", "Daisy", "Deborah", "Isabel", "Stella", "Debra", "Berverly", "Vera", "Angela", "Lucy", "Lauren"}
var lastNames = [...]string{"Smith", "Jones", "Williams", "Brown", "Taylor"}

func randomName() string {
	fn := firstNames[rand.Intn(len(firstNames))]
	ln := lastNames[rand.Intn(len(lastNames))]
	name := []byte(fmt.Sprintf("%s %s", fn, ln))
	errors := rand.Intn(4)
	for i := 0; i < errors; i++ {
		name[rand.Intn(len(name))] = 'x'
	}
	return string(name)
}

var cities = [...]string{"Paris", "Berlin", "New York", "Amsterdam", "Shanghai", "San Francisco", "Sydney", "Cape Town", "Brasilia", "Cairo"}

func randomCity() string {
	return cities[rand.Intn(len(cities))]
}

func loadRecords(n int) []Record {
	records := make([]Record, n)
	for i := 0; i < n; i++ {
		records[i] = Record{
			ID:   i,
			Name: randomName(),
			City: randomCity(),
		}
	}
	return records
}

func compare(records []Record) (comparisons, matchCount int) {
	for _, a := range records {
		for _, b := range records {
			if a == b {
				continue // don't compare with itself
			}
			comparisons++
			if matches(a, b) {
				fmt.Printf("%s and %s are probably the same person\n", a.Name, b.Name)
				matchCount++
			}
		}
	}
	return comparisons, matchCount
}

func main() {
	records := loadRecords(100)
	comparisons, matchCount := compare(records)

	fmt.Printf("made %d comparisons and found %d matches\n", comparisons, matchCount)
}

Im Go Playground ausprobieren: https://go.dev/play/p/ky80W_hk4S3

Sie sollten eine Ausgabe ähnlich der folgenden sehen (womöglich müssen Sie das Programm mehrfach ausführen, falls die zufälligen Daten keine Übereinstimmungen ergeben):

Daisy Williams and Dave Williams are probably the same person
Deborax Browx and Debra Brown are probably the same person
Riley Brown and RxxeyxBrown are probably the same person
Dan Willxams and Dave Williams are probably the same person
made 9900 comparisons and found 16 matches

Mit etwas Glück erhalten Sie auch Fehltreffer wie „Daisy“ und „Dave“. Das liegt daran, dass wir eine Levenshtein-Distanz von drei verwenden, die als alleiniger Fuzzy-Algorithmus für kurze Namen viel zu hoch ist. Verbessern Sie das gern selbst.

Aus Performancesicht ist der wirklich problematische Teil die 9.900 Vergleiche, die bis zum Ergebnis nötig sind – denn eine Verdopplung der Eingabemenge vervierfacht die Zahl der benötigten Vergleiche in etwa. Für 200 Datensätze sind bereits 39.800 Vergleiche erforderlich. Bei den vergleichsweise geringen 100.000 Datensätzen wären es fast 10 Milliarden Vergleiche. Egal wie groß Ihr System ist: Mit wachsender Datenmenge kommt der Punkt, an dem es das nicht mehr in akzeptabler Zeit schafft.

Eine schnelle, aber nahezu nutzlose Optimierung besteht darin, nicht jede Kombination zweimal zu vergleichen. Es sollte keine Rolle spielen, ob wir A mit B oder B mit A vergleichen. Das reduziert die Zahl der nötigen Vergleiche allerdings nur um den Faktor zwei, was angesichts des quadratischen Wachstums vernachlässigbar ist.

Komplexitätsreduktion durch Blocking

Betrachten wir die erstellten Regeln, fällt schnell auf: Bei unterschiedlichen Städten wird es nie eine Übereinstimmung geben. All diese Vergleiche sind vollständig verschwendet und sollten verhindert werden. Datensätze, die man für ähnlich hält, in einen gemeinsamen Bucket zu legen und andere in einen weiteren, nennt man in der Entity Resolution Blocking. Da wir die Stadt als Blocking-Schlüssel verwenden wollen, ist die Umsetzung recht einfach.

func block(records []Record) map[string][]Record {
	blocks := map[string][]Record{}
	for _, record := range records {
		blocks[record.City] = append(blocks[record.City], record)
	}
	return blocks
}

func main() {
	records := loadRecords(100)
	blocks := block(records)
	comparisons := 0
	matchCount := 0
	for _, blockRecords := range blocks {
		c, m := compare(blockRecords)
		comparisons += c
		matchCount += m
	}

	fmt.Printf("made %d comparisons and found %d matches\n", comparisons, matchCount)
}

Im Go Playground ausprobieren: https://go.dev/play/p/1z_j0nhX-tU

Das Ergebnis ist nun dasselbe, aber wir brauchen nur noch rund ein Zehntel der Vergleiche wie zuvor, weil wir zehn verschiedene Städte haben. In einer realen Anwendung wäre dieser Effekt wegen der viel größeren Varianz der Städte enorm viel größer. Außerdem lässt sich jeder Block unabhängig von den anderen verarbeiten, etwa parallel auf demselben oder auf verschiedenen Servern.

Den richtigen Blocking-Schlüssel zu finden, kann für sich genommen eine Herausforderung sein. Ein Attribut wie die Stadt kann zu ungleichmäßigen Verteilungen führen und damit dazu, dass ein einzelner riesiger Block (etwa eine Großstadt) sehr viel länger dauert als alle anderen. Oder die Stadt enthält einen winzigen Tippfehler und gilt nicht mehr als gültige Übereinstimmung. Mehrere Attribute und/oder phonetische Schlüssel oder Q-Gramme als Blocking-Schlüssel können diese Probleme lösen, erhöhen aber die Komplexität der Software.

Von Übereinstimmungen zur Entität

Bislang können wir über unsere Datensätze nur sagen, ob zwei von ihnen übereinstimmen oder nicht. Für sehr einfache Anwendungsfälle mag das bereits genügen. In den meisten Fällen wollen Sie jedoch alle Übereinstimmungen kennen, die zur selben Entität gehören. Das reicht von einfachen sternförmigen Mustern, bei denen A mit B, C und D übereinstimmt, über kettenförmige Muster, bei denen A mit B, B mit C und C mit D übereinstimmt, bis hin zu sehr komplexen graphartigen Mustern. Dieses sogenannte transitive Record Linkage lässt sich mit einem Connected-Components-Algorithmus leicht umsetzen, solange alle Daten in den Arbeitsspeicher eines einzelnen Servers passen. In einer realen Anwendung ist auch das wieder deutlich anspruchsvoller.

func compare(records []Record) (comparisons int, edges [][2]int) {
	for _, a := range records {
		for _, b := range records {
			if a == b {
				continue // don't compare with itself
			}
			comparisons++
			if matches(a, b) {
				edges = append(edges, [2]int{a.ID, b.ID})
			}
		}
	}
	return comparisons, edges
}

func connectedComponents(edges [][2]int) [][]int {
	components := map[int][]int{}
	nextIdx := 0
	idx := map[int]int{}

	for _, edge := range edges {
		a := edge[0]
		b := edge[1]
		aIdx, aOk := idx[a]
		bIdx, bOk := idx[b]
		switch {
		case aOk && bOk && aIdx == bIdx: // in same component
			continue
		case aOk && bOk && aIdx != bIdx: // merge two components
			components[nextIdx] = append(components[aIdx], components[bIdx]...)
			delete(components, aIdx)
			delete(components, bIdx)
			for _, x := range components[nextIdx] {
				idx[x] = nextIdx
			}
			nextIdx++
		case aOk && !bOk: // add b to component of a
			idx[b] = aIdx
			components[aIdx] = append(components[aIdx], b)
		case bOk && !aOk: // add a to component of b
			idx[a] = bIdx
			components[bIdx] = append(components[bIdx], a)
		default: // create new component with a and b
			idx[a] = nextIdx
			idx[b] = nextIdx
			components[nextIdx] = []int{a, b}
			nextIdx++
		}
	}

	cc := make([][]int, len(components))
	i := 0
	for k := range components {
		cc[i] = components[k]
		i++
	}
	return cc
}

func main() {
	records := loadRecords(100)
	blocks := block(records)
	comparisons := 0
	edges := [][2]int{}
	for _, blockRecords := range blocks {
		c, e := compare(blockRecords)
		comparisons += c
		edges = append(edges, e...)
	}
	cc := connectedComponents(edges)

	fmt.Printf("made %d comparisons and found %d matches and %d entities\n", comparisons, len(edges), len(cc))
	for _, component := range cc {
		names := make([]string, len(component))
		for i, id := range component {
			names[i] = records[id].Name
		}
		fmt.Printf("found the following entity: %s from %s\n", strings.Join(names, ", "), records[component[0]].City)
	}
}

Im Go Playground ausprobieren: https://go.dev/play/p/vP3tzlzJ2LN

Die Connected-Components-Funktion iteriert über alle Kanten und legt entweder eine neue Komponente an, fügt die neue ID einer bestehenden Komponente hinzu oder führt zwei Komponenten zu einer zusammen. Das Ergebnis sieht dann ungefähr so aus:

made 1052 comparisons and found 6 matches and 2 entities
found the following entity: Ivan Smxth, Ixan Smith, Ivax Smitx from Cairo
found the following entity: Brxan Williams, Brian Williams from Cape Town

Diese Kanten aufzubewahren, bringt einige Vorteile. Wir können mit ihnen die entstehende Entität verständlich und erklärbar machen – idealerweise sogar mit einer ansprechenden Oberfläche, die zeigt, wie die Datensätze einer Entität miteinander verbunden sind. Oder wir nutzen die Kanten in einem Echtzeit-Entity-Resolution-System, um eine Entität wieder zu trennen, nachdem Daten entfernt wurden. Oder Sie verwenden sie beim Aufbau eines Graph Neural Network (GNN) und erzielen damit bessere ML-Ergebnisse als mit den Datensätzen allein.

image

Ähnliche Daten und andere Herausforderungen

Ein Problem mit den Kanten kann entstehen, wenn es sehr viele sehr ähnliche Datensätze gibt. Stimmt etwa A mit B überein und B mit C, dann stimmt je nach verwendeten Regeln möglicherweise auch C mit A überein. Passen D, E, F und so weiter ebenfalls zu den bestehenden Datensätzen, sind wir wieder beim quadratischen Wachstum – mit bald so vielen Kanten, dass sie nicht mehr beherrschbar sind.

Erinnern Sie sich, wie wir die Blocking-Buckets gebildet haben? Überraschung! Bei sehr ähnlichen Daten, die alle in wenigen riesigen Buckets landen, bricht die Rechenleistung erneut drastisch ein – selbst wenn Sie dem vorherigen Rat gefolgt sind und Buckets aus mehreren Attributen gebildet haben.

Ein typisches Beispiel für solche nicht identischen Duplikate ist jemand, der regelmäßig im selben Shop bestellt, aber als Gast (tut uns leid, keine schöne Kundennummer). Diese Person nutzt fast immer dieselbe Lieferadresse und ist meist in der Lage, den eigenen Namen korrekt zu schreiben. Solche Datensätze sollten deshalb besonders behandelt werden, um eine stabile Systemperformance sicherzustellen – aber das ist ein Thema für sich.

Bevor Sie sich mit Ihrem neuen Wissen zu wohl fühlen und Ihre eigene Lösung bauen wollen, zerstöre ich kurz Ihre Träume. Über die Herausforderungen, all das in Echtzeit zu tun, haben wir noch gar nicht gesprochen. Selbst wenn Sie meinen, keine stets aktuelle Entität zu brauchen (der offensichtliche Vorteil), bringt ein Echtzeitansatz weiteren Nutzen: Sie müssen nicht immer wieder dieselben Berechnungen anstellen, sondern nur für die neuen Daten. Andererseits ist die Umsetzung deutlich komplexer. Sie wollen Blocking? Vergleichen Sie die neuen Datensätze mit allen Datensätzen des oder der Buckets, zu denen sie gehören – das kann aber dauern und wäre eher als inkrementeller Batch zu betrachten. Und bis das endlich abgeschlossen ist, warten schon jede Menge neuer Datensätze auf die Verarbeitung. Sie wollen die Entitäten mit Connected Components berechnen? Gern, halten Sie den gesamten Graphen im Arbeitsspeicher und fügen Sie einfach die neuen Kanten hinzu. Vergessen Sie dabei nur nicht, die beiden Entitäten nachzuhalten, die durch den neuen Datensatz gerade zusammengeführt wurden.

Sie sind also weiterhin bereit, das selbst zu implementieren. Und Sie haben die (in diesem Fall) kluge Entscheidung getroffen, keine Kanten zu speichern und Echtzeit nicht zu unterstützen. Ihren ersten Entity-Resolution-Batchjob über alle Daten haben Sie erfolgreich durchlaufen lassen. Es hat eine Weile gedauert, aber Sie machen das ja nur einmal im Monat, also ist das in Ordnung. Ungefähr in diesem Moment kommt vermutlich Ihr Datenschutzbeauftragter um die Ecke und teilt Ihnen mit, dass Sie wegen einer DSGVO-Beschwerde diese eine Person aus Ihrem Datenbestand entfernen müssen. Also lassen Sie den kompletten Batchjob für eine einzige gelöschte Entität erneut laufen – juhu.

Fazit

Entity Resolution mag auf den ersten Blick recht einfach aussehen, birgt aber eine Reihe erheblicher technischer Herausforderungen. Einige davon lassen sich vereinfachen und/oder ignorieren, andere müssen für eine gute Performance wirklich gelöst werden.

Häufige Fragen

Wie sieht eine typische Entity-Resolution-Implementierung von Anfang bis Ende aus?
Sie profiliert und normalisiert die Quelldatensätze, wählt über Blocking Kandidatenpaare aus, bewertet wahrscheinliche Übereinstimmungen mit Fuzzy-Matching-Regeln oder -Modellen, setzt Übereinstimmungen zu Entitäten zusammen, speichert die Match-Belege und stellt die aufgelöste Entität nachgelagerten Systemen bereit.
Warum wird Entity Resolution im großen Maßstab schwierig?
Der einfache Ansatz vergleicht jeden Datensatz mit jedem anderen, was quadratisch wächst. Große Datenbestände bringen zudem ungleichmäßige Blocking-Buckets, sehr ähnliche Datensätze, Probleme bei der Graphverwaltung, den Umgang mit Aktualisierungen und Löschanforderungen mit sich.
Was ist Blocking in der Entity Resolution?
Blocking bezeichnet das Gruppieren von Datensätzen in wahrscheinliche Kandidaten-Buckets vor dem Vergleich. Es reduziert überflüssige Vergleiche, doch die Blocking-Schlüssel müssen sorgfältig gewählt werden, damit das System keine echten Übereinstimmungen übersieht und keine überdimensionierten Buckets entstehen.
Sollten Unternehmen ihre eigene Entity-Resolution-Engine bauen?
Eine Eigenentwicklung kann für begrenzte, stabile, batch-orientierte Anwendungsfälle funktionieren. Unternehmen sollten eine dedizierte Entity-Resolution-Schicht in Betracht ziehen, wenn Matching geschäftskritisch ist, viele Quellen und große Volumina umfasst, erklärbar sein muss oder aktueller Kundenkontext in operativen Systemen benötigt wird.

Tilores mit Ihren eigenen Daten evaluieren

Wählen Sie den Schritt, der zu Ihrer Evaluierungsphase passt.

Demo buchen Tilores Studio kostenlos testen

Sehen Sie, was aufgelöste Entitätsdaten für Ihr Unternehmen — und Ihre KI — leisten.