Kundenidentitätsabgleich mit BigQuery Graph

1. Einführung

In diesem Codelab erstellen Sie eine modulare End-to-End-Engine für die Kundenidentitätsauflösung (Entitätsabgleich) direkt in Google Cloud BigQuery. Sie kombinieren Google Cloud Shell für die Infrastrukturbereitstellung mit dem BigQuery Studio SQL-Editor für die Datenbereinigung, die Kandidatenbewertung, die Erstellung von Property-Graphen und die ISO-GQL-Pfadabfragen (Graph Query Language).

Die Identitätsauflösung ist eine grundlegende Funktion für Customer 360-Lösungen für Unternehmen, die Betrugserkennung und die Datenkonsolidierung in mehreren Systemen. Da es je nach Datenreife und geschäftlichen Anforderungen viele gültige Ansätze für die Identitätsabgleich gibt, sind alle Schritte in diesem Codelab modular und optional. Die Pipeline soll eine Vielzahl gängiger, produktionsreifer Branchentechniken demonstrieren, darunter die Normalisierung von Remote-UDF-Adressen, die phonetische Soundex-Blockierung, die semantische Vektorsuche (AI.EMBED), die hybride Feature-Bewertung und das GQL-Property-Graph-Clustering. So können Sie die Muster selektiv übernehmen, die zu Ihrer Architektur passen.

Abgleichsmethoden und Scoring-Schwellenwerte sollten auf die deterministische und probabilistische Abgleichsstrategie Ihrer Organisation abgestimmt werden, die vom jeweiligen Anwendungsfall abhängt. Bei der strikten Compliance, Abrechnung oder Finanzvorgängen werden in der Regel deterministische Regeln mit hoher Präzision (z. B. genaue Übereinstimmungen von Sozialversicherungsnummern oder Steuernummern) bevorzugt, um falsche Verknüpfungen zu vermeiden. Bei der Marketingpersonalisierung, Analyse und Empfehlungs-Engines werden dagegen häufig probabilistische Fuzzy-Matching- und semantische Vektorähnlichkeiten verwendet, um den Recall zu maximieren und subtile Verbindungen aufzudecken.

Architektur der BigQuery Customer Identity Resolution Engine

Aufgaben

  • FEBRL3-Benchmark-Dataset aufnehmen: Laden Sie synthetische Kundendatensätze und Ground Truth-Abgleichspaare in BigQuery.
  • Remote-UDF für die Adressvalidierung bereitstellen: Stellen Sie eine Python-Cloud-Funktion bereit und registrieren Sie eine BigQuery-Remote-Funktion, um Straßenadressen zu normalisieren.
  • Profildaten und phonetische Codierungen vorverarbeiten: Führen Sie die SQL-Datenbereinigung aus, rufen Sie die UDF für Adressen auf und berechnen Sie die phonetischen SOUNDEX-Schlüssel und Levenshtein-Bearbeitungsdistanzen:
    • Soundex-Phonetic-Codierung: Ein phonetischer Algorithmus zum Indexieren von Namen nach ihrem Klang bei englischer Aussprache. Dabei werden Namen in einen 4‑stelligen Code umgewandelt (ein Anfangsbuchstabe gefolgt von drei Ziffern), der Konsonantengruppen darstellt (z. B. werden sowohl "John" als auch "Jon" J500 zugeordnet, während "Smith" und "Smyth" S530 zugeordnet werden). So werden phonetische Abgleichssignale für die Funktionsbewertung und das inkrementelle Blockieren in Echtzeit bereitgestellt.
    • Levenshtein-Distanz (EDIT_DISTANCE): Ein Stringmesswert, der die Mindestanzahl an Einzelzeichenänderungen (Einfügungen, Löschungen oder Ersetzungen) misst, die erforderlich sind, um einen String in einen anderen zu ändern. Dies ermöglicht einen präzisen unscharfen Namens- und Adressabgleich.
  • Semantische Profileinbettungen und Vektorsuche generieren: Generieren Sie Texteinbettungen direkt in SQL mit AI.EMBED (text-embedding-005) und finden Sie die K nächsten Nachbarn mit VECTOR_SEARCH, um sie als sublineare Kandidatengenerierungsebene zu verwenden.
  • Bewertung von Kandidatenpaaren und Hybrid Edge Feature Fusion: Nutzen Sie Kandidatenpaare für die Vektorsuche, um die O(N²)-Komplexität von Cross-Joins zu eliminieren, gewichtete Ähnlichkeitswerte für mehrere Funktionen zu berechnen (SSN, Levenshtein-Distanz, Geburtsdatum, Jaccard-Index für Adressen) und Kanten in einer einheitlichen Kandidatentabelle zusammenzuführen.
  • Erstellung von Property-Graphen und ISO-GQL-Pfadabfragen: Erstellen Sie einen BigQuery-PROPERTY GRAPH, führen Sie ISO-GQL-{1, 2}-Pfadabfragen (GRAPH_TABLE) aus, um verbundene Kundencluster aufzulösen, individuelle Auswertungsstatistiken zu berechnen und Soft-Household-Clustering mit Adamic-Adar-Gewichtung durchzuführen.
  • Inkrementelle Auflösung und dauerhafte Stabilität: Tägliche Batch-Aufnahmen mit inkrementellem Delta-Abgleich verarbeiten.
  • Konsolidierung von Clustern und Clusterstabilität (1-ε-Überschneidung): Sorgen Sie mit einer Überschneidungsschwelle von (1-ε) für eine dauerhafte Clusterstabilität über Pipelineausführungen hinweg.

Voraussetzungen

  • Ein Webbrowser wie Chrome.
  • Google Cloud-Projekt mit aktivierter Abrechnungsfunktion.

Dieses Codelab richtet sich an Data Engineers, Datenbankentwickler und KI-/ML-Experten aller Erfahrungsstufen, auch Anfänger.

Geschätzte Dauer:45 Minuten
Geschätzte Kosten:Weniger als 2,00 $ (nutzt Cloud Functions mit nutzungsbasierter Bezahlung und BigQuery-Abfrageverarbeitung).

2. Hinweis

Google Cloud-Projekt erstellen

  1. Wählen Sie in der Google Cloud Console auf der Seite der Projektauswahl ein Google Cloud-Projekt aus oder erstellen Sie eines.
  2. Die Abrechnung für das Cloud-Projekt muss aktiviert sein. So prüfen Sie, ob die Abrechnung für ein Projekt aktiviert ist.

Cloud Shell starten

Cloud Shell ist eine Befehlszeilenumgebung, die in Google Cloud ausgeführt wird und mit den erforderlichen Tools vorinstalliert ist.

  1. Klicken Sie oben in der Google Cloud Console auf Cloud Shell aktivieren.
  2. Authentifizierung überprüfen:
gcloud auth list
  1. Konfigurieren Sie Umgebungsvariablen in Cloud Shell:
export GCP_PROJECT=$(gcloud config get-value project)
export REGION="us-central1"
export DATASET_ID="identity_resolution"

Erforderliche APIs aktivieren

Führen Sie den folgenden Befehl in Cloud Shell mit Ihrem Nutzerkonto aus, um alle erforderlichen Google Cloud-Dienste zu aktivieren:

gcloud services enable \
  addressvalidation.googleapis.com \
  cloudbuild.googleapis.com \
  cloudfunctions.googleapis.com \
  cloudresourcemanager.googleapis.com \
  artifactregistry.googleapis.com \
  aiplatform.googleapis.com \
  run.googleapis.com \
  bigqueryconnection.googleapis.com \
  bigqueryreservation.googleapis.com \
  bigquery.googleapis.com

Damit die API nahtlos ausgeführt werden kann und auf die Standardanmeldedaten für Anwendungen (Application Default Credentials, ADC) zugegriffen werden kann, erstellen Sie ein spezielles Lab-Dienstkonto und aktivieren Sie die gcloud-Identitätsübernahme:

# 1. Create a Service Account for the lab (if it does not already exist)
gcloud iam service-accounts create identity-res-sa \
  --display-name="Identity Resolution Service Account" 2>/dev/null || true

# Wait 5 seconds for IAM propagation
sleep 5

# Extract Project Number for default build and compute service accounts
export PROJECT_NUMBER=$(gcloud projects describe ${GCP_PROJECT} --format="value(projectNumber)")

# 2. Grant specific required least-privilege roles to the lab Service Account
for role in roles/bigquery.admin \
            roles/bigquery.resourceAdmin \
            roles/run.admin \
            roles/cloudfunctions.admin \
            roles/resourcemanager.projectIamAdmin \
            roles/cloudbuild.builds.editor \
            roles/cloudbuild.builds.builder \
            roles/artifactregistry.repoAdmin \
            roles/artifactregistry.writer \
            roles/storage.admin \
            roles/logging.logWriter \
            roles/iam.serviceAccountUser \
            roles/aiplatform.user; do
  gcloud projects add-iam-policy-binding ${GCP_PROJECT} \
    --member="serviceAccount:identity-res-sa@${GCP_PROJECT}.iam.gserviceaccount.com" \
    --role="${role}" --quiet
done

# 3. Grant required build & storage permissions to default Compute Engine & Cloud Build service accounts (required for 2nd-gen Cloud Functions container builds)
for role in roles/cloudbuild.builds.builder \
            roles/logging.logWriter \
            roles/artifactregistry.writer \
            roles/storage.objectAdmin; do
  gcloud projects add-iam-policy-binding ${GCP_PROJECT} \
    --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
    --role="${role}" --quiet || true
  gcloud projects add-iam-policy-binding ${GCP_PROJECT} \
    --member="serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com" \
    --role="${role}" --quiet || true
done

# 4. Grant Service Account Token Creator role to your user account
export SA_EMAIL="identity-res-sa@${GCP_PROJECT}.iam.gserviceaccount.com"

gcloud iam service-accounts add-iam-policy-binding \
  "${SA_EMAIL}" \
  --member="user:$(gcloud config get-value account)" \
  --role="roles/iam.serviceAccountTokenCreator" --quiet

# 5. Enable Service Account impersonation for gcloud
gcloud config set auth/impersonate_service_account "${SA_EMAIL}"

# 6. Wait for IAM role assignments and impersonation caches to propagate
echo "Waiting 90 seconds for IAM policies and impersonation caches to propagate..."
sleep 90

BigQuery-Dataset erstellen

Erstellen Sie das BigQuery-Dataset zum Speichern Ihrer Kundennodes, ‑Kanten, ‑Diagrammmodelle und ‑Auswertungsansichten:

bq mk --location=US --dataset ${GCP_PROJECT}:${DATASET_ID}

Die Ausgabe sollte etwa so aussehen:

Dataset 'your-project-id:identity_resolution' successfully created.

Wenn Sie dedizierte Rechenkapazität für Vektorindexsuchen, Graphaggregationen und die Ausführung von Remote Functions benötigen, ohne durch On-Demand-CPU-Limits oder gemeinsame Kontingente eingeschränkt zu werden, erstellen Sie in Cloud Shell eine Enterprise Edition-Reservierung mit Autoscaling:

# 1. Create a BigQuery Enterprise reservation with 0 baseline slots and 100 max autoscaling slots
bq mk --reservation \
  --project_id=${GCP_PROJECT} \
  --location=US \
  --edition=ENTERPRISE \
  --slots=0 \
  --autoscale_max_slots=100 \
  --ignore_idle_slots=true \
  identity-res-reservation

# 2. Assign your Cloud project to the newly created reservation for query execution
bq mk --reservation_assignment \
  --project_id=${GCP_PROJECT} \
  --location=US \
  --reservation_id=identity-res-reservation \
  --job_type=QUERY \
  --assignee_type=PROJECT \
  --assignee_id=${GCP_PROJECT}

3. FEBRL3-Kundennodensatz aufnehmen

Bevor Sie die Remote-Funktion zur Adressvalidierung bereitstellen und die Identitätsabgleich durchführen, laden Sie das synthetische FEBRL3-Benchmark-Dataset für den Identitätsabgleich (das 5.000 Kundendatensätze mit Clustern mit mehreren Duplikaten von bis zu 5 Duplikaten pro Kunde enthält) mit der Python-Bibliothek recordlinkage und schreiben die Rohkundennodes (customer_nodes) und die Ground Truth-Abgleichslinks (ground_truth_links) mit BigQuery DataFrames (bigframes) in BigQuery.

Führen Sie die folgenden Befehle in der Cloud Shell aus, um Abhängigkeiten zu installieren und das Aufnahmeskript auszuführen:

# 1. Install recordlinkage dataset library & bigframes (if outside Cloud Shell, activate your virtual environment first)
pip install recordlinkage bigframes --quiet

# 2. Write and execute the FEBRL3 dataset ingestion script
cat << 'EOF' > ingest_febrl.py
import os
import pandas as pd
import bigframes.pandas as bpd
from recordlinkage.datasets import load_febrl3

GCP_PROJECT = os.environ.get("GCP_PROJECT", "your-project-id")
DATASET_ID = "identity_resolution"

table_raw_id = f"{GCP_PROJECT}.{DATASET_ID}.customer_nodes"
table_gt_id = f"{GCP_PROJECT}.{DATASET_ID}.ground_truth_links"

print("Loading FEBRL3 benchmark dataset...")
df_nodes, true_links = load_febrl3(return_links=True)
df_nodes = df_nodes.reset_index()
df_nodes['dataset_source'] = 'febrl3'

for col in df_nodes.columns:
    if df_nodes[col].dtype == 'object':
        df_nodes[col] = df_nodes[col].fillna('')

print("Ingesting raw customer nodes into BigQuery via BigQuery DataFrames...")
bf_nodes = bpd.read_pandas(df_nodes)
bf_nodes.to_gbq(table_raw_id, if_exists="replace")

print("Ingesting ground truth links into BigQuery via BigQuery DataFrames...")
df_gt = pd.DataFrame(list(true_links), columns=["source_id", "target_id"])
bf_gt = bpd.read_pandas(df_gt)
bf_gt.to_gbq(table_gt_id, if_exists="replace")

print(f"Raw customer nodes ingested into `{table_raw_id}` ({len(df_nodes):,} rows).")
print(f"Ground truth links ingested into `{table_gt_id}` ({len(df_gt):,} pairs).")
EOF

python3 ingest_febrl.py

Rufen Sie in der Google Cloud Console BigQuery Studio auf, öffnen Sie den Tab SQL-Abfrage (+) und führen Sie die folgende Abfrage aus, um die aufgenommene Tabelle mit Kundennodes zu prüfen:

SELECT rec_id, given_name, surname, street_number, address_1, address_2, suburb, postcode, state, date_of_birth, soc_sec_id, dataset_source
FROM `identity_resolution.customer_nodes`
LIMIT 5;

Die Ausgabe sollte etwa so aussehen:

rec_id

given_name

Nachname

Straßennummer

address_1

address_2

Vorort

Postleitzahl

Bundesstaat

date_of_birth

soc_sec_id

dataset_source

rec-10-org

brent

wood

11

girdlestone circuit

kingston tower

clifton springs

4152

nsw

19340706

1075870

febrl3

rec-10-dup-0

brnt

woode

11

girdelstone circut

cliffton springs

4152

nsw

19340706

1075870

febrl3

rec-10-dup-1

bernt

wood

11

girdlestone cir

kingston twr

clifton spngs

4152

19340760

1075870

febrl3

rec-10-dup-2

brent

wod

15

girdlestone crt

clifton springs

4152

nsw

19340706

febrl3

rec-25-org

mccarthy

henry

13

beasley street

crystal brook farm

ingleburn

6164

vic

19770913

1347524

febrl3

Im Benchmark-Dataset sind in den doppelten Clustern realistische schmutzige Daten enthalten:

  • Phonetische und Rechtschreibvarianten: brent im Vergleich zu brnt / bernt, wood im Vergleich zu woode / wod und clifton im Vergleich zu cliffton.
  • Abkürzungen und Tippfehler in Adressen: girdlestone circuit im Vergleich zu girdelstone circut / girdlestone cir / girdlestone crt und Hausnummer 11 im Vergleich zu OCR-Fehler 15.
  • Vertauschung von Zeichen und fehlende Werte: Vertauschung von Geburtsdaten (19340706 im Vergleich zu 19340760), fehlende Bundesstaaten ( ) und fehlende Sozialversicherungsnummern ( ).

In den folgenden Schritten werden SOUNDEXphonetische Codierungen, UDFs zur Adressnormalisierung, Levenshtein-Distanz und AI.EMBEDVektorsuche verwendet, um diese Diskrepanzen zu überbrücken und doppelte Profile genau zu verknüpfen.

4. UDF für die Remote-Funktion für Address Validation bereitstellen

Bei der Adressnormalisierung werden Straßennamen, Vorortgrenzen und Postleitzahlen standardisiert, bevor der Abgleich erfolgt. Die Google Maps Address Validation API ist ein Dienst, der eine Adresse akzeptiert, Adresskomponenten identifiziert und validiert. In diesem Schritt stellen Sie eine Python-Cloud-Funktion in Cloud Shell bereit, die eine benutzerdefinierte Funktion (User-Defined Function, UDF) zur Adressvalidierung und ‑normalisierung für BigQuery bereitstellt.

Cloud Functions-Quelldateien schreiben

Führen Sie den folgenden Befehl in Cloud Shell aus, um das Quellverzeichnis der Cloud-Funktion zu erstellen und main.py und requirements.txt zu schreiben:

mkdir -p cloud_function_address_validation && cd cloud_function_address_validation

cat << 'EOF' > main.py
import os
import json
import logging
import requests
from functools import lru_cache
from concurrent.futures import ThreadPoolExecutor

import functions_framework
import google.auth
from google.auth.transport.requests import AuthorizedSession
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

ADDRESS_VALIDATION_URL = "https://addressvalidation.googleapis.com/v1:validateAddress"
ENABLE_ADDRESS_VALIDATION_API = os.environ.get("ENABLE_ADDRESS_VALIDATION_API", "false").lower() == "true"

# ==========================================
# GLOBAL INITIALIZATION (Runs once per Cold Start)
# ==========================================

credentials, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
session = AuthorizedSession(credentials)

retries = Retry(
    total=4, 
    backoff_factor=0.5, 
    status_forcelist=[429, 500, 502, 503, 504],
    allowed_methods=["POST"]
)
adapter = HTTPAdapter(max_retries=retries, pool_connections=100, pool_maxsize=100)
session.mount("https://", adapter)

executor = ThreadPoolExecutor(max_workers=50)


@lru_cache(maxsize=10000)
def call_validation_api(address_text: str) -> str:
    payload = {
        "address": {
            "regionCode": "AU",
            "addressLines": [address_text]
        }
    }
    
    response = session.post(ADDRESS_VALIDATION_URL, json=payload, timeout=10)
    response.raise_for_status()
        
    res_data = response.json()
    result = res_data.get('result', {})
    address_obj = result.get('address', {})
    verdict = result.get('verdict', {})

    formatted = address_obj.get('formattedAddress', address_text).lower()
    has_unconfirmed = verdict.get('hasUnconfirmedComponents', True)
    address_complete = verdict.get('addressComplete', False)
    granularity = verdict.get('validationGranularity', 'UNCONFIRMED')
    actions = verdict.get('possibleNextActions', [])
    next_action = str(actions[0]) if actions else "NONE"

    is_valid = bool(address_complete and not has_unconfirmed)

    return {
        "formatted_address": formatted,
        "address_is_valid": is_valid,
        "validation_granularity": granularity,
        "possible_next_action": next_action
    }


def process_single_call(call):
    call = call or []
    padded = (call + [""] * 6)[:6]
    
    cleaned_parts = [str(p).strip() if p is not None else "" for p in padded]
    street_num, addr_1, addr_2, suburb, state, postcode = cleaned_parts
    
    address_parts = [p for p in cleaned_parts if p]
    address_text = " ".join(address_parts)

    if not address_text:
        return {
            "formatted_address": "",
            "address_is_valid": False,
            "validation_granularity": "EMPTY",
            "possible_next_action": "NONE"
        }

    if not ENABLE_ADDRESS_VALIDATION_API:
        normalized = (
            address_text.lower()
            .replace("street", "st")
            .replace("road", "rd")
            .replace("place", "pl")
            .replace("avenue", "ave")
            .replace("circuit", "cct")
        )
        return {
            "formatted_address": normalized,
            "address_is_valid": bool(len(address_parts) >= 3),
            "validation_granularity": "PREMISE" if postcode and suburb else "SUBURB",
            "possible_next_action": "NONE"
        }

    try:
        return call_validation_api(address_text)
    except Exception as e:
        logging.error(f"Address Validation API Error for '{address_text}': {str(e)}")
        return {
            "formatted_address": address_text.lower(),
            "address_is_valid": False,
            "validation_granularity": "UNCONFIRMED",
            "possible_next_action": "NONE"
        }


@functions_framework.http
def validate_address_udf(request):
    request_json = request.get_json(silent=True) or {}
    calls = request_json.get('calls', [])

    if not calls:
        return {'replies': []}

    try:
        # executor.map inherently preserves array input order (Strictly required by BigQuery)
        replies = list(executor.map(process_single_call, calls))
        return {'replies': replies}
    except Exception as e:
        logging.error(f"Batch execution failed: {e}")
        return {'errorMessage': str(e)}, 400
EOF

cat << 'EOF' > requirements.txt
functions-framework==3.*
requests==2.*
google-auth==2.*
urllib3==2.*
EOF

Cloud-Funktion bereitstellen und IAM-Berechtigungen konfigurieren

Führen Sie diese Befehle in Cloud Shell aus, um die Cloud Functions-Funktion der 2. Generation bereitzustellen und eine BigQuery-Cloud-Ressourcenverbindung zu konfigurieren:

# 1. Deploy 2nd-Gen Cloud Function
gcloud functions deploy validate_address_udf \
  --gen2 \
  --runtime=python311 \
  --region=${REGION} \
  --source=. \
  --entry-point=validate_address_udf \
  --trigger-http \
  --no-allow-unauthenticated \
  --memory=512Mi \
  --cpu=1 \
  --concurrency=80 \
  --quiet

# 2. Extract Function Endpoint URI
export FUNCTION_URL=$(gcloud functions describe validate_address_udf --region=${REGION} --gen2 --format="value(serviceConfig.uri)")

# 3. Create BigQuery Cloud Resource Connection
bq mk --connection --location=US --project_id=${GCP_PROJECT} --connection_type=CLOUD_RESOURCE address_val_conn || true

# 4. Extract Connection Service Account Email
export BQ_SA_EMAIL=$(bq show --format=prettyjson --connection US.address_val_conn | grep -o '"serviceAccountId": "[^"]*"' | cut -d'"' -f4)

# 5. Bind Cloud Run Invoker and Vertex AI User IAM Roles to BigQuery Connection Service Account
gcloud run services add-iam-policy-binding validate-address-udf \
  --region=${REGION} \
  --member="serviceAccount:${BQ_SA_EMAIL}" \
  --role="roles/run.invoker" --quiet

gcloud projects add-iam-policy-binding ${GCP_PROJECT} \
  --member="serviceAccount:${BQ_SA_EMAIL}" \
  --role="roles/aiplatform.user" --quiet

# 6. Wait for connection IAM policy propagation
echo "Waiting 60 seconds for BigQuery connection IAM policy to propagate..."
sleep 60

In der Ausgabe sollte zu sehen sein, dass die Bereitstellung der Cloud-Funktion abgeschlossen und die IAM-Bindungen erfolgreich angewendet wurden.

Funktion zur Normalisierung von Remote-Adressen registrieren

Als Nächstes registrieren Sie die DDL für die BigQuery-Remote-Funktion (validate_address_udf), die BigQuery-Tabellenzeilen mit dem bereitgestellten Cloud Functions-Endpunkt (${FUNCTION_URL}) verbindet.

Führen Sie den folgenden Befehl in Cloud Shell aus, um die URL Ihrer bereitgestellten Cloud Functions-Funktion abzurufen und die Remote Function automatisch zu registrieren:

# 1. Retrieve deployed Cloud Function URL
export FUNCTION_URL=$(gcloud functions describe validate_address_udf --region=${REGION:-us-central1} --gen2 --format="value(serviceConfig.uri)")

# 2. Register Remote Function DDL in BigQuery
bq query --use_legacy_sql=false \
"CREATE OR REPLACE FUNCTION \`${GCP_PROJECT}.${DATASET_ID}.validate_address_udf\`(
  street_number STRING,
  address_1 STRING,
  address_2 STRING,
  suburb STRING,
  state STRING,
  postcode STRING
) RETURNS JSON
REMOTE WITH CONNECTION \`us.address_val_conn\`
OPTIONS (
  endpoint = '${FUNCTION_URL}',
  max_batching_rows = 100
);"

5. Profildaten und phonetische Codierungen vorverarbeiten

In diesem Schritt führen Sie eine BigQuery-SQL-Vorverarbeitungsabfrage für die aufgenommene customer_nodes-Tabelle aus.

Datenbereinigung und phonetische Feature-Abfrage ausführen

Führen Sie im BigQuery Studio-SQL-Editor die folgende Abfrage aus, um customer_nodes_cleaned zu erstellen. Diese Abfrage bewirkt Folgendes:

  1. Rufen Sie validate_address_udf auf, um normalisierte Adressen und Validierungsergebnisse abzurufen.
  2. Generiert SOUNDEX phonetische Codierungen für given_name und surname, um Rechtschreibvariationen zu berücksichtigen.
  3. Erstellt ein strukturiertes profile_text-Feld.
CREATE OR REPLACE TABLE `identity_resolution.customer_nodes_cleaned` AS
WITH raw_data AS (
  SELECT 
    rec_id, dataset_source,
    TRIM(LOWER(given_name)) AS given_name_clean,
    TRIM(LOWER(surname)) AS surname_clean,
    `identity_resolution.validate_address_udf`(street_number, address_1, address_2, suburb, state, postcode) AS addr_json,
    TRIM(suburb) AS suburb, TRIM(state) AS state, TRIM(postcode) AS postcode,
    TRIM(date_of_birth) AS date_of_birth, TRIM(soc_sec_id) AS soc_sec_id
  FROM `identity_resolution.customer_nodes`
)
SELECT
  rec_id, dataset_source,
  given_name_clean AS given_name,
  SOUNDEX(given_name_clean) AS given_name_soundex,
  surname_clean AS surname,
  SOUNDEX(surname_clean) AS surname_soundex,
  CONCAT(given_name_clean, ' ', surname_clean) AS full_name,
  STRING(addr_json.formatted_address) AS formatted_address,
  BOOL(addr_json.address_is_valid) AS address_is_valid,
  STRING(addr_json.validation_granularity) AS validation_granularity,
  STRING(addr_json.possible_next_action) AS possible_next_action,
  suburb, state, postcode, date_of_birth, soc_sec_id,
  CONCAT('Name: ', CONCAT(given_name_clean, ' ', surname_clean), '; Address: ', STRING(addr_json.formatted_address), '; DOB: ', date_of_birth, '; SSN: ', soc_sec_id) AS profile_text
FROM raw_data;

Fragen Sie die bereinigte Tabelle „nodes“ ab:

SELECT rec_id, given_name, given_name_soundex, surname, surname_soundex, formatted_address 
FROM `identity_resolution.customer_nodes_cleaned`
LIMIT 5;

Die Ausgabe sollte etwa so aussehen:

rec_id

given_name

given_name_soundex

Nachname

surname_soundex

formatted_address

rec-001-A

John

J500

Smith

S530

12 high st richmond vic 3121

rec-001-B

Jon

J500

Smith

S530

12 high st richmond vic 3121

rec-002-A

Elizabeth

E421

Taylor

T460

45 park rd suite 4 south yarra vic 3141

6. Semantische Profileinbettungen und Vektorsuche generieren

Zusätzlich zur Adressnormalisierung, zu phonetischen Soundex-Schlüsseln und zur Levenshtein-Distanz unterstützt BigQuery integrierte generative KI-Einbettungsfunktionen über AI.EMBED.

Mit AI.EMBED generiert BigQuery Text-Embeddings direkt in SQL mithilfe von Foundation Models wie text-embedding-005, ohne dass eine manuelle DDL für den Vektorindex erforderlich ist:

Führen Sie die folgenden Abfragen im BigQuery Studio-SQL-Editor aus:

-- 1. Generate Customer Profile Embeddings using AI.EMBED (offloaded to Vertex AI)
CREATE OR REPLACE TABLE `identity_resolution.customer_embeddings` AS
SELECT 
  rec_id,
  dataset_source,
  profile_text,
  AI.EMBED(profile_text, connection_id => 'us.address_val_conn', endpoint => 'text-embedding-005').result AS text_embedding
FROM `identity_resolution.customer_nodes_cleaned`;

-- 2. Execute VECTOR_SEARCH for Top-K Nearest Neighbors Candidate Generation
CREATE OR REPLACE TABLE `identity_resolution.vector_candidate_edges` AS
SELECT 
  query.rec_id AS source_id,
  base.rec_id AS target_id,
  distance AS vector_distance
FROM VECTOR_SEARCH(
  TABLE `identity_resolution.customer_embeddings`,
  'text_embedding',
  TABLE `identity_resolution.customer_embeddings`,
  top_k => 5,
  distance_type => 'COSINE'
)
WHERE query.rec_id < base.rec_id AND distance <= 0.20;

7. Bewertung von Kandidatenpaaren und Hybrid Edge Feature Fusion

Die Auswertung aller möglichen Kundenpaare (quadratisches Wachstum O(N²)) wird mit zunehmender Größe des Datensatzes rechenintensiv. In relationalen Datenbank-Engines wie BigQuery wird durch den Versuch, regelbasiertes Blockieren mit komplexen OR-Join-Bedingungen für mehrere Spalten zu implementieren (z. B. durch Joins für a.soc_sec_id = b.soc_sec_id OR a.given_name_soundex = b.given_name_soundex OR ...), verhindert, dass der Abfrageoptimierer skalierbare Hash-Joins oder Sort-Merge-Joins für einen einzelnen Equi-Join-Schlüssel verwendet. Stattdessen wird auf einen Cross-Join mit O(N²) zurückgegriffen und jedes Paar wird gefiltert. Das funktioniert nicht bei großen Datenmengen.

In diesem Schritt werden die von der Vektorsuchtabelle (vector_candidate_edges) generierten Kandidatenpaare über schnelle, indexierte Equi-Joins (ON c.source_id = a.rec_id und ON c.target_id = b.rec_id) mit customer_nodes_cleaned verknüpft. Anschließend wird ein gewichteter Abgleichswert berechnet, der Folgendes kombiniert:

  • SSN Match Score (Gewichtung: 0.30)
  • Ähnlichkeit bei der Bearbeitung des Nachnamens mit der Levenshtein-Distanz EDIT_DISTANCE (Gewichtung: 0.20)
  • Ähnlichkeit der Bearbeitung des Vornamens (Gewicht: 0.20)
  • DOB Match Score (Gewichtung: 0.15)
  • Address Token Jaccard Similarity (Gewichtung: 0.15) über SPLIT(LOWER(formatted_address), ' ')

Kandidatenkanten und gewichtete Ähnlichkeitswerte berechnen

Führen Sie die folgende Abfrage im BigQuery Studio-SQL-Editor aus, um matched_edges zu füllen:

CREATE OR REPLACE TABLE `identity_resolution.matched_edges` AS
WITH candidate_pairs AS (
  SELECT 
    c.source_id, c.target_id,
    a.given_name AS a_given_name, b.given_name AS b_given_name,
    a.surname AS a_surname, b.surname AS b_surname,
    a.given_name_soundex AS a_gn_snd, b.given_name_soundex AS b_gn_snd,
    a.surname_soundex AS a_sn_snd, b.surname_soundex AS b_sn_snd,
    a.date_of_birth AS a_dob, b.date_of_birth AS b_dob,
    a.soc_sec_id AS a_ssn, b.soc_sec_id AS b_ssn,
    SPLIT(LOWER(a.formatted_address), ' ') AS a_tokens,
    SPLIT(LOWER(b.formatted_address), ' ') AS b_tokens
  FROM `identity_resolution.vector_candidate_edges` c
  JOIN `identity_resolution.customer_nodes_cleaned` a ON c.source_id = a.rec_id
  JOIN `identity_resolution.customer_nodes_cleaned` b ON c.target_id = b.rec_id
),
scored_pairs AS (
  SELECT
    source_id, target_id,
    CASE WHEN a_ssn = b_ssn AND a_ssn != '' THEN 1.0 ELSE 0.0 END AS ssn_match,
    CASE WHEN a_dob = b_dob THEN 1.0 ELSE 0.0 END AS dob_match,
    CASE WHEN a_gn_snd = b_gn_snd THEN 1.0 ELSE 0.0 END AS given_name_soundex_match,
    CASE WHEN a_sn_snd = b_sn_snd THEN 1.0 ELSE 0.0 END AS surname_soundex_match,
    GREATEST(
      (1.0 - (EDIT_DISTANCE(a_given_name, b_given_name) / GREATEST(LENGTH(a_given_name), LENGTH(b_given_name), 1))),
      (1.0 - (EDIT_DISTANCE(a_given_name, b_surname) / GREATEST(LENGTH(a_given_name), LENGTH(b_surname), 1)))
    ) AS given_name_edit_sim,
    GREATEST(
      (1.0 - (EDIT_DISTANCE(a_surname, b_surname) / GREATEST(LENGTH(a_surname), LENGTH(b_surname), 1))),
      (1.0 - (EDIT_DISTANCE(a_surname, b_given_name) / GREATEST(LENGTH(a_surname), LENGTH(b_given_name), 1)))
    ) AS surname_edit_sim,
    (
      (SELECT COUNT(DISTINCT t) FROM UNNEST(a_tokens) t JOIN UNNEST(b_tokens) t2 ON t = t2)
      /
      GREATEST(1.0, (SELECT COUNT(DISTINCT t) FROM UNNEST(ARRAY_CONCAT(a_tokens, b_tokens)) t))
    ) AS address_jaccard_sim
  FROM candidate_pairs
)
SELECT
  source_id, target_id,
  ROUND((0.30 * ssn_match) + (0.20 * surname_edit_sim) + (0.20 * given_name_edit_sim) + (0.15 * dob_match) + (0.15 * address_jaccard_sim), 4) AS match_score
FROM scored_pairs
WHERE ((0.30 * ssn_match) + (0.20 * surname_edit_sim) + (0.20 * given_name_edit_sim) + (0.15 * dob_match) + (0.15 * address_jaccard_sim)) >= 0.55;

Kandidaten für Edge-Abgleiche prüfen:

SELECT source_id, target_id, match_score 
FROM `identity_resolution.matched_edges`
ORDER BY match_score DESC;

Die Ausgabe sollte etwa so aussehen:

source_id

target_id

match_score

rec-001-A

rec-001-B

0.9400

rec-002-A

rec-002-B

0.9100

rec-003-A

rec-003-B

0.7300

Regelbasierten und Vektorsuch-Edges in einer einheitlichen Tabelle zusammenführen

Kombinieren Sie Kandidatenkanten aus regelbasiertem Fuzzy Matching und semantischer Vektorsuche in einer einzigen deduplizierten final_matched_edges-Tabelle:

CREATE OR REPLACE TABLE `identity_resolution.final_matched_edges` AS
SELECT 
  source_id, 
  target_id, 
  MAX(edge_weight) AS edge_weight,
  IF(COUNT(DISTINCT edge_type) > 1, 'HYBRID', MAX(edge_type)) AS edge_type
FROM (
  SELECT source_id, target_id, match_score AS edge_weight, 'RULE_BASED' AS edge_type
  FROM `identity_resolution.matched_edges`
  UNION ALL
  SELECT source_id, target_id, ROUND(1.0 - vector_distance, 4) AS edge_weight, 'VECTOR_SEARCH' AS edge_type
  FROM `identity_resolution.vector_candidate_edges`
  WHERE vector_distance <= 0.05
)
GROUP BY source_id, target_id;

8. Erstellung von Attributgrafiken und ISO-GQL-Pfaddurchläufe

BigQuery unterstützt ISO GQL (Graph Query Language) nativ über Property Graphs. Mit einem Attributgraphen wird eine logische Grafansicht über relationale BigQuery-Tabellen erstellt, ohne dass Daten dupliziert werden.

In diesem Schritt erstellen Sie einen Property Graph customer_identity_graph mit Ihrer einheitlichen Tabelle mit Kandidatenkanten (final_matched_edges) und fragen Graphverbindungen über Kundenprofile hinweg mit {1, 2} k-Hop-Pfadtraversierung ab.

Transitive Konnektivität und K-Hop-Traversals

BigQuery-Property-Graph-DDL erstellen

Führen Sie die folgende DDL-Anweisung im BigQuery Studio-SQL-Editor aus:

CREATE OR REPLACE PROPERTY GRAPH `identity_resolution.customer_identity_graph`
NODE TABLES (
  `identity_resolution.customer_nodes_cleaned` AS `Customer`
  KEY (rec_id)
)
EDGE TABLES (
  `identity_resolution.final_matched_edges`
  KEY (source_id, target_id)
  SOURCE KEY (source_id) REFERENCES `Customer`(rec_id)
  DESTINATION KEY (target_id) REFERENCES `Customer`(rec_id)
  LABEL MATCHED_TO
);

K-Hop-Graphencluster visualisieren

Führen Sie die folgende Abfrage aus, um übereinstimmende Kundencluster über ein bis zwei Beziehungssprünge zu visualisieren:

GRAPH `identity_resolution.customer_identity_graph`
MATCH p = (c1:Customer)-[e:MATCHED_TO]->{1, 2}(c2:Customer)
RETURN TO_JSON(p) AS graph_cluster_path
LIMIT 10;

Visualisierung von K-Hop-Graphclustern

Kanonische Kundencluster auflösen

Führen Sie die folgende Abfrage aus, um Entitätscluster in resolved_customers aufzulösen:

CREATE OR REPLACE TABLE `identity_resolution.resolved_customers` AS
WITH graph_paths AS (
  SELECT 
    source_node_id, target_node_id
  FROM GRAPH_TABLE(
    `identity_resolution.customer_identity_graph`
    MATCH (c1:Customer)-[e:MATCHED_TO]->{1, 2}(c2:Customer)
    COLUMNS (c1.rec_id AS source_node_id, c2.rec_id AS target_node_id)
  )
),
all_connections AS (
  SELECT source_node_id AS node_id, target_node_id AS connected_id FROM graph_paths
  UNION DISTINCT
  SELECT target_node_id AS node_id, source_node_id AS connected_id FROM graph_paths
  UNION DISTINCT
  SELECT rec_id AS node_id, rec_id AS connected_id FROM `identity_resolution.customer_nodes_cleaned`
),
clusters AS (
  SELECT 
    node_id,
    MIN(connected_id) AS canonical_customer_id
  FROM all_connections
  GROUP BY node_id
)
SELECT 
  canonical_customer_id,
  ARRAY_AGG(node_id) AS customer_records,
  COUNT(node_id) AS record_count
FROM clusters
GROUP BY canonical_customer_id;

Abfrage der Tabelle mit den aufgelösten Clustern:

SELECT canonical_customer_id, record_count, customer_records 
FROM `identity_resolution.resolved_customers`
ORDER BY record_count DESC;

Die Ausgabe sollte etwa so aussehen:

canonical_customer_id

record_count

customer_records

rec-001-A

2

['rec-001-A', 'rec-001-B']

rec-002-A

2

['rec-002-A', 'rec-002-B']

rec-003-A

2

['rec-003-A', 'rec-003-B']

Ansicht für Bewertungsmesswerte erstellen

Um Genauigkeit, Trefferquote und F1-Wert für die Tabelle ground_truth_links zu berechnen, führen Sie Folgendes aus:

CREATE OR REPLACE VIEW `identity_resolution.evaluation_metrics` AS
WITH predictions AS (
  SELECT 
    LEAST(source_id, target_id) AS source_id, 
    GREATEST(source_id, target_id) AS target_id 
  FROM `identity_resolution.final_matched_edges` 
  WHERE edge_weight >= 0.55
),
ground_truth AS (
  SELECT 
    LEAST(source_id, target_id) AS source_id, 
    GREATEST(source_id, target_id) AS target_id 
  FROM `identity_resolution.ground_truth_links`
),
stats AS (
  SELECT
    COUNT(g.source_id) AS total_ground_truth,
    COUNT(p.source_id) AS total_predictions,
    COUNTIF(p.source_id IS NOT NULL AND g.source_id IS NOT NULL) AS true_positives,
    COUNTIF(p.source_id IS NOT NULL AND g.source_id IS NULL) AS false_positives,
    COUNTIF(p.source_id IS NULL AND g.source_id IS NOT NULL) AS false_negatives
  FROM ground_truth g
  FULL OUTER JOIN predictions p ON g.source_id = p.source_id AND g.target_id = p.target_id
)
SELECT
  total_ground_truth, total_predictions, true_positives, false_positives, false_negatives,
  ROUND(true_positives / NULLIF(true_positives + false_positives, 0), 4) AS precision,
  ROUND(true_positives / NULLIF(true_positives + false_negatives, 0), 4) AS recall,
  ROUND(2 * true_positives / NULLIF((2 * true_positives) + false_positives + false_negatives, 0), 4) AS f1_score
FROM stats;

Bewertungsmesswerte abfragen:

SELECT * FROM `identity_resolution.evaluation_metrics`;

Die Ausgabe sollte etwa so aussehen:

total_ground_truth

total_predictions

true_positives

false_positives

false_negatives

Precision

Rückruf

f1_score

6538

5620

5608

13

930

0.9977

0.8579

0.9225

Haushaltscluster mithilfe der Adamic-Adar-Gewichtung von Grafen auflösen

Bei der individuellen Identitätsauflösung werden Datensätze aufgelöst, die zur selben Person gehören. Für Customer 360-Architekturen für Unternehmen ist jedoch häufig eine Gruppierung auf höherer Ebene erforderlich, bei der Personen, die an derselben Adresse wohnen, in einer Haushaltseinheit zusammengefasst werden.

Da bei synthetischen Benchmarks (wie FEBRL3) die Grundwahrheit auf individueller Ebene bewertet wird, erfolgt die Auflösung auf Haushaltsebene als nachgelagerter Schritt. Wenn kein Umzugsprotokoll mit Zeitstempel vorhanden ist, können Personen, die mit mehreren Adressen verknüpft sind, zu einer übermäßigen Zusammenführung oder Clusterfragmentierung führen. Um dieses Problem zu beheben, verwenden wir die Adamic-Adar-Gewichtung, um weiche Haushaltsmitgliedschaften zu erstellen.

Führen Sie die folgende Abfrage im BigQuery Studio-SQL-Editor aus, um household_clusters mit der Adamic-Adar-Gewichtung des Graphen zu füllen:

CREATE OR REPLACE TABLE `identity_resolution.household_clusters` AS
WITH customer_addresses AS (
  SELECT DISTINCT
    r.canonical_customer_id,
    c.formatted_address
  FROM `identity_resolution.resolved_customers` r,
  UNNEST(r.customer_records) AS rec_id
  JOIN `identity_resolution.customer_nodes_cleaned` c ON rec_id = c.rec_id
  WHERE c.formatted_address IS NOT NULL AND c.formatted_address != ''
),
-- Adamic-Adar Exclusivity Weighting: 1.0 / LN(GREATEST(degree, 2))
address_degrees AS (
  SELECT 
    formatted_address,
    COUNT(DISTINCT canonical_customer_id) AS address_degree,
    1.0 / LN(GREATEST(COUNT(DISTINCT canonical_customer_id), 2)) AS address_exclusivity_weight
  FROM customer_addresses
  GROUP BY formatted_address
),
customer_household_affinity AS (
  SELECT 
    ca.canonical_customer_id,
    ca.formatted_address AS household_address,
    ad.address_degree,
    ad.address_exclusivity_weight,
    ad.address_exclusivity_weight * COUNT(DISTINCT ca2.canonical_customer_id) AS raw_household_affinity
  FROM customer_addresses ca
  JOIN address_degrees ad ON ca.formatted_address = ad.formatted_address
  LEFT JOIN customer_addresses ca2 
    ON ca.formatted_address = ca2.formatted_address 
   AND ca.canonical_customer_id != ca2.canonical_customer_id
  GROUP BY ca.canonical_customer_id, ca.formatted_address, ad.address_degree, ad.address_exclusivity_weight
),
ranked_households AS (
  SELECT 
    canonical_customer_id,
    household_address,
    address_degree AS total_residents,
    ROUND(
      COALESCE(SAFE_DIVIDE(raw_household_affinity, SUM(raw_household_affinity) OVER(PARTITION BY canonical_customer_id)), 1.0),
      4
    ) AS household_membership_weight,
    ROW_NUMBER() OVER(PARTITION BY canonical_customer_id ORDER BY raw_household_affinity DESC) AS household_rank
  FROM customer_household_affinity
)
SELECT 
  CONCAT('hh-', ABS(FARM_FINGERPRINT(household_address))) AS canonical_household_id,
  canonical_customer_id,
  household_address,
  total_residents,
  household_membership_weight,
  household_rank
FROM ranked_households;

Fragen Sie die Tabelle mit den aufgelösten Haushaltsclustern ab:

SELECT canonical_household_id, canonical_customer_id, household_address, total_residents, household_membership_weight, household_rank
FROM `identity_resolution.household_clusters`
ORDER BY total_residents DESC;

End-to-End-Identitätshierarchie mit GQL visualisieren

Wenn Sie die vollständige dreistufige Identitätshierarchie visuell nachvollziehen möchten, indem Sie nicht gruppierte Rohdaten zu Kunden mit aufgelösten Kundenentitäten und dann mit aufgelösten Haushaltsentitäten verbinden, führen Sie die folgende DDL- und ISO-GQL-Abfrage in BigQuery Studio aus:

-- 1. Create Household Node Table
CREATE OR REPLACE TABLE `identity_resolution.household_nodes` AS
SELECT DISTINCT 
  canonical_household_id, 
  household_address, 
  total_residents
FROM `identity_resolution.household_clusters`;

-- 2. Create Unresolved Record to Resolved Entity Edge Table
CREATE OR REPLACE TABLE `identity_resolution.customer_entity_edges` AS
SELECT DISTINCT
  rec_id,
  canonical_customer_id
FROM `identity_resolution.resolved_customers`,
UNNEST(customer_records) AS rec_id;

-- 3. Create Primary Household Edge Table (Highest Weighted Household Rank = 1)
CREATE OR REPLACE TABLE `identity_resolution.primary_household_edges` AS
SELECT 
  canonical_customer_id,
  canonical_household_id,
  household_membership_weight,
  household_rank
FROM `identity_resolution.household_clusters`
WHERE household_rank = 1;

-- 4. Update Unified Property Graph DDL
CREATE OR REPLACE PROPERTY GRAPH `identity_resolution.customer_identity_graph`
NODE TABLES (
  `identity_resolution.customer_nodes_cleaned` AS `RawCustomer`
    KEY (rec_id),
  `identity_resolution.resolved_customers` AS `ResolvedCustomer`
    KEY (canonical_customer_id),
  `identity_resolution.household_nodes` AS `ResolvedHousehold`
    KEY (canonical_household_id)
)
EDGE TABLES (
  `identity_resolution.final_matched_edges`
    KEY (source_id, target_id)
    SOURCE KEY (source_id) REFERENCES `RawCustomer`(rec_id)
    DESTINATION KEY (target_id) REFERENCES `RawCustomer`(rec_id)
    LABEL MATCHED_TO,
  `identity_resolution.customer_entity_edges`
    KEY (rec_id, canonical_customer_id)
    SOURCE KEY (rec_id) REFERENCES `RawCustomer`(rec_id)
    DESTINATION KEY (canonical_customer_id) REFERENCES `ResolvedCustomer`(canonical_customer_id)
    LABEL RESOLVED_TO,
  `identity_resolution.primary_household_edges`
    KEY (canonical_customer_id, canonical_household_id)
    SOURCE KEY (canonical_customer_id) REFERENCES `ResolvedCustomer`(canonical_customer_id)
    DESTINATION KEY (canonical_household_id) REFERENCES `ResolvedHousehold`(canonical_household_id)
    LABEL BELONGS_TO_HOUSEHOLD
);

-- 5. Execute 3-Tier GQL Query for Multi-Resident Household Visualization
GRAPH `identity_resolution.customer_identity_graph`
MATCH p = (raw:RawCustomer)-[e1:RESOLVED_TO]->(c:ResolvedCustomer)-[e2:BELONGS_TO_HOUSEHOLD]->(h:ResolvedHousehold)
WHERE h.total_residents > 1
RETURN TO_JSON(p) AS multi_resident_household_hierarchy_path
LIMIT 15;

Wenn Sie diese GQL-Abfrage in BigQuery Studio ausführen, wird ein interaktiver Canvas mit einer dreistufigen Grafikvisualisierung gerendert, in der Rohdatensätze für Kundenprofile (RawCustomer) in einzelne kanonische Einheiten (ResolvedCustomer) aufgelöst werden, die mit gemeinsamen Einheiten für Mehrpersonenhaushalte (ResolvedHousehold) verknüpft sind.

Visualisierung von Graphclustern für Haushalte mit mehreren Bewohnern

9. Inkrementelle Auflösung und dauerhafte Stabilität

In realen Unternehmensanwendungen werden neue Kundendatensätze kontinuierlich über tägliche oder Echtzeit-Batch-Aufnahmen empfangen. Anstatt die vollständige Diagrammauflösung für das gesamte historische Dataset noch einmal auszuführen, vergleicht eine Incremental Delta Matching Engine neue eingehende Datensätze mit vorhandenen aufgelösten Baseline-Clustern (resolved_customers).

Um dies effizient zu erreichen, verwendet die Engine die Vektorsuche (

VECTOR_SEARCH

) als eine Form des dynamischen Clusterings. Dabei wird jeder eingehende Datensatz als Anfragepunkt behandelt und VECTOR_SEARCH ruft die K nächsten Nachbarn aus dem Index für historische Baseline-Einbettungen ab. Wenn ein eingehender Datensatz mit einem vorhandenen Bestandskundenprofil über dem Ähnlichkeitsschwellenwert übereinstimmt, wird er dynamisch in diesen Cluster eingefügt und übernimmt die Baseline canonical_customer_id (MATCHED_TO_EXISTING_CLUSTER). Wenn kein nächster Nachbar für die Baseline über dem Schwellenwert gefunden wird, wird eine neue Entitäts-UUID erstellt (NEW_CUSTOMER_ENTITY).

Beispiel für inkrementelle Batch-Aufnahmedatensätze

Fügen Sie die folgende DDL in den BigQuery Studio-SQL-Editor ein und führen Sie sie aus, um incremental_daily_intake zu erstellen:

CREATE OR REPLACE TABLE `identity_resolution.incremental_daily_intake` AS
SELECT * FROM UNNEST([
  STRUCT(
    'rec-9999-new-1' AS rec_id, 'erin' AS given_name, 'donaldson' AS surname, 
    'E650' AS given_name_soundex, 'D543' AS surname_soundex, 
    '19810427' AS date_of_birth, '2955815' AS soc_sec_id, 
    '13 hawkesbury crescent aralee lewiston 7018' AS formatted_address, '7018' AS postcode
  ),
  STRUCT(
    'rec-9999-new-2' AS rec_id, 'hollie' AS given_name, 'lillie-hinrichs' AS surname, 
    'H400' AS given_name_soundex, 'L446' AS surname_soundex, 
    '19251130' AS date_of_birth, '4920253' AS soc_sec_id, 
    '27 hemmings crescent kilvinton village banyo 4030' AS formatted_address, '4030' AS postcode
  ),
  STRUCT(
    'rec-9999-new-3' AS rec_id, 'sarah' AS given_name, 'ryan' AS surname, 
    'S600' AS given_name_soundex, 'R500' AS surname_soundex, 
    '20010101' AS date_of_birth, '999999999' AS soc_sec_id, 
    '500 market st melbourne vic 3000' AS formatted_address, '3000' AS postcode
  ),
  STRUCT(
    'rec-9999-new-4' AS rec_id, 'zzyzx' AS given_name, 'qx-vonderland' AS surname, 
    'Z220' AS given_name_soundex, 'Q215' AS surname_soundex, 
    '19991231' AS date_of_birth, '999887766' AS soc_sec_id, 
    '9999 zulu orbit station moon-base alpha 9999' AS formatted_address, '9999' AS postcode
  )
]);

Inkrementelle Delta Match-Abfrage ausführen

Führen Sie die folgende Abfrage im BigQuery-SQL-Editor aus, um den Delta-Abgleich für Ihr aufgelöstes Baseline-Dataset durchzuführen:

CREATE OR REPLACE TABLE `identity_resolution.incremental_resolved_customers` AS
WITH historical_resolved_base AS (
  SELECT c.rec_id, c.given_name, c.surname, c.given_name_soundex, c.surname_soundex, c.date_of_birth, c.soc_sec_id, c.formatted_address, c.postcode, r.canonical_customer_id
  FROM `identity_resolution.customer_nodes_cleaned` c
  JOIN (
    SELECT canonical_customer_id, node_id
    FROM `identity_resolution.resolved_customers`, UNNEST(customer_records) AS node_id
  ) r ON c.rec_id = r.node_id
),
incremental_intake AS (
  SELECT 
    rec_id AS new_rec_id, given_name, surname, given_name_soundex, surname_soundex, date_of_birth, soc_sec_id, formatted_address, postcode,
    CONCAT('Name: ', CONCAT(given_name, ' ', surname), '; Address: ', formatted_address, '; DOB: ', date_of_birth, '; SSN: ', soc_sec_id) AS profile_text
  FROM `identity_resolution.incremental_daily_intake`
),
rule_delta_matches AS (
  SELECT 
    i.new_rec_id,
    h.canonical_customer_id AS matched_canonical_id,
    h.rec_id AS matched_baseline_rec_id,
    (
      0.30 * (CASE WHEN i.soc_sec_id = h.soc_sec_id AND i.soc_sec_id != '' THEN 1.0 ELSE 0.0 END) +
      0.20 * (1.0 - (EDIT_DISTANCE(i.surname, h.surname) / GREATEST(LENGTH(i.surname), LENGTH(h.surname), 1))) +
      0.20 * (1.0 - (EDIT_DISTANCE(i.given_name, h.given_name) / GREATEST(LENGTH(i.given_name), LENGTH(h.given_name), 1))) +
      0.15 * (CASE WHEN i.date_of_birth = h.date_of_birth THEN 1.0 ELSE 0.0 END) +
      0.15 * (1.0 - (EDIT_DISTANCE(i.formatted_address, h.formatted_address) / GREATEST(LENGTH(i.formatted_address), LENGTH(h.formatted_address), 1)))
    ) AS match_score
  FROM incremental_intake i
  JOIN historical_resolved_base h
    ON (i.soc_sec_id = h.soc_sec_id AND i.soc_sec_id != '')
    OR (i.date_of_birth = h.date_of_birth AND i.given_name_soundex = h.given_name_soundex)
),
vector_intake_embeddings AS (
  SELECT new_rec_id, AI.EMBED(profile_text, connection_id => 'us.address_val_conn', endpoint => 'text-embedding-005').result AS text_embedding
  FROM incremental_intake
),
vector_delta_matches AS (
  SELECT 
    v.query.new_rec_id,
    h.canonical_customer_id AS matched_canonical_id,
    h.rec_id AS matched_baseline_rec_id,
    ROUND(1.0 - v.distance, 4) AS match_score
  FROM VECTOR_SEARCH(
    TABLE `identity_resolution.customer_embeddings`,
    'text_embedding',
    TABLE vector_intake_embeddings,
    top_k => 3,
    distance_type => 'COSINE'
  ) v
  JOIN historical_resolved_base h ON v.base.rec_id = h.rec_id
  WHERE v.distance <= 0.20
),
combined_delta AS (
  SELECT 
    new_rec_id, matched_canonical_id, matched_baseline_rec_id, match_score,
    'RULE_BASED' AS match_strategy
  FROM rule_delta_matches WHERE match_score >= 0.55
  
  UNION ALL
  
  SELECT 
    new_rec_id, matched_canonical_id, matched_baseline_rec_id, match_score,
    'VECTOR_SEARCH' AS match_strategy
  FROM vector_delta_matches
),
aggregated_delta AS (
  SELECT 
    new_rec_id, matched_canonical_id, matched_baseline_rec_id,
    MAX(match_score) AS match_score,
    CASE 
      WHEN COUNT(DISTINCT match_strategy) > 1 THEN 'BOTH'
      ELSE MAX(match_strategy)
    END AS match_strategy
  FROM combined_delta
  GROUP BY new_rec_id, matched_canonical_id, matched_baseline_rec_id
),
best_matches AS (
  SELECT 
    new_rec_id, matched_canonical_id, matched_baseline_rec_id, match_score, match_strategy,
    ROW_NUMBER() OVER(PARTITION BY new_rec_id ORDER BY match_score DESC) AS rank
  FROM aggregated_delta
)
SELECT 
  i.new_rec_id AS record_id,
  i.given_name, i.surname,
  COALESCE(b.matched_canonical_id, GENERATE_UUID()) AS persistent_canonical_customer_id,
  h.canonical_household_id AS assigned_household_id,
  CASE WHEN b.matched_canonical_id IS NOT NULL THEN 'MATCHED_TO_EXISTING_CLUSTER' ELSE 'NEW_CUSTOMER_ENTITY' END AS assignment_type,
  b.matched_baseline_rec_id,
  b.match_score,
  COALESCE(b.match_strategy, 'NONE') AS match_strategy
FROM incremental_intake i
LEFT JOIN best_matches b ON i.new_rec_id = b.new_rec_id AND b.rank = 1
LEFT JOIN `identity_resolution.primary_household_edges` h ON b.matched_canonical_id = h.canonical_customer_id;

Ergebnisse der inkrementellen Auflösung abfragen:

SELECT record_id, persistent_canonical_customer_id, assigned_household_id, assignment_type, match_score, match_strategy 
FROM `identity_resolution.incremental_resolved_customers`;

Die Ausgabe sollte etwa so aussehen:

record_id

persistent_canonical_customer_id

assigned_household_id

assignment_type

match_score

match_strategy

rec-9999-new-1

rec-529-dup-0

hh-8745613133408211212

MATCHED_TO_EXISTING_CLUSTER

1.0000

BOTH

rec-9999-new-2

rec-875-dup-0

hh-4802217335228845918

MATCHED_TO_EXISTING_CLUSTER

1.0000

BOTH

rec-9999-new-3

rec-359-dup-0

hh-8891673998566910207

MATCHED_TO_EXISTING_CLUSTER

0.8739

VECTOR_SEARCH

rec-9999-new-4

197e02f9-7175-4484-9c32-b7edbc731c9e

NULL

NEW_CUSTOMER_ENTITY

NULL

NONE

10. Zusammenfassung von Clustern und Clusterstabilität (1-ε-Überschneidung)

In Produktionssystemen für Unternehmen führen Teams in der Regel in regelmäßigen Abständen (z.B. wöchentlich oder monatlich) ein Reclustering des gesamten Graphen durch, um neue Kanten und Datenquellen einzubeziehen. Wenn neue Beziehungen entstehen, kann es bei einer vollständigen Neu-Clusterung des Diagramms dazu kommen, dass sich Cluster-IDs bei verschiedenen Pipelineausführungen willkürlich verschieben oder ändern.

Damit dauerhafte Kunden-IDs für nachgelagerte CRM-, CDP- und Abrechnungssysteme beibehalten werden, wird mit Cluster Stability die Knotenüberschneidung zwischen Clustern des aktuellen Laufs (t) und Clustern des vorherigen Laufs (t–1) anhand eines Überschneidungsschwellenwerts von (1 – ε) bewertet (wobei ε = 0,30 ist und eine Knotenüberschneidung von mindestens 70% erforderlich ist).

Wenn ein neu berechneter Cluster in Run (t) mindestens 70% seiner Mitgliedsdatensätze mit einem Cluster aus Run (t–1) gemeinsam hat, erbt er die bisherige dauerhafte Kunden-ID (STABLE_EVOLUTION). Völlig neue Cluster erhalten neu generierte UUIDs (NEW_CLUSTER_CREATED).

Abfrage zu Clusterüberschneidung und ‑stabilität ausführen

Führen Sie die folgende Abfrage im BigQuery Studio-SQL-Editor aus, um stable_resolved_customers zu füllen:

CREATE OR REPLACE TABLE `identity_resolution.stable_resolved_customers` AS
WITH previous_run_clusters AS (
  SELECT 
    canonical_customer_id AS previous_persistent_id,
    node_id
  FROM `identity_resolution.resolved_customers`, UNNEST(customer_records) AS node_id
),
current_run_clusters AS (
  SELECT 
    canonical_customer_id AS new_cluster_id,
    node_id,
    COUNT(*) OVER (PARTITION BY canonical_customer_id) AS current_cluster_size
  FROM `identity_resolution.resolved_customers`, UNNEST(customer_records) AS node_id
  UNION ALL
  SELECT 
    persistent_canonical_customer_id AS new_cluster_id,
    record_id AS node_id,
    COUNT(*) OVER (PARTITION BY persistent_canonical_customer_id) AS current_cluster_size
  FROM `identity_resolution.incremental_resolved_customers`
),
cluster_intersections AS (
  SELECT 
    c.new_cluster_id,
    p.previous_persistent_id,
    c.current_cluster_size,
    COUNT(c.node_id) AS shared_node_count,
    COUNT(c.node_id) / c.current_cluster_size AS overlap_fraction
  FROM current_run_clusters c
  JOIN previous_run_clusters p ON c.node_id = p.node_id
  GROUP BY c.new_cluster_id, p.previous_persistent_id, c.current_cluster_size
),
best_matching_previous_cluster AS (
  SELECT 
    new_cluster_id,
    previous_persistent_id,
    shared_node_count,
    overlap_fraction,
    ROW_NUMBER() OVER (PARTITION BY new_cluster_id ORDER BY overlap_fraction DESC) AS rank
  FROM cluster_intersections
  WHERE overlap_fraction >= 0.70
)
SELECT 
  c.new_cluster_id AS raw_cluster_id,
  COALESCE(b.previous_persistent_id, GENERATE_UUID()) AS persistent_canonical_customer_id,
  ARRAY_AGG(c.node_id) AS customer_records,
  COUNT(c.node_id) AS record_count,
  COALESCE(MAX(b.shared_node_count), 0) AS shared_node_count,
  COALESCE(MAX(b.overlap_fraction), 0.0) AS overlap_fraction,
  CASE 
    WHEN b.previous_persistent_id IS NOT NULL THEN 'STABLE_EVOLUTION'
    ELSE 'NEW_CLUSTER_CREATED'
  END AS cluster_status
FROM current_run_clusters c
LEFT JOIN best_matching_previous_cluster b 
  ON c.new_cluster_id = b.new_cluster_id AND b.rank = 1
GROUP BY c.new_cluster_id, b.previous_persistent_id;

Gefilterte Vorschau für inkrementelle Datensätze ansehen

Führen Sie diese Abfrage aus, um den Status der Clusterstabilität für Ihre täglichen Aufnahmedaten zu prüfen:

SELECT 
  s.persistent_canonical_customer_id,
  node_id AS record_id,
  h.canonical_household_id AS assigned_household_id,
  s.cluster_status
FROM `identity_resolution.stable_resolved_customers` s, UNNEST(s.customer_records) AS node_id
LEFT JOIN `identity_resolution.primary_household_edges` h 
  ON s.persistent_canonical_customer_id = h.canonical_customer_id
WHERE node_id IN ('rec-9999-new-1', 'rec-9999-new-2', 'rec-9999-new-3', 'rec-9999-new-4')
ORDER BY record_id;

Die Ausgabe sollte etwa so aussehen:

persistent_canonical_customer_id

record_id

assigned_household_id

cluster_status

rec-529-dup-0

rec-9999-new-1

hh-8745613133408211212

STABLE_EVOLUTION

rec-875-dup-0

rec-9999-new-2

hh-4802217335228845918

STABLE_EVOLUTION

rec-359-dup-0

rec-9999-new-3

hh-8891673998566910207

STABLE_EVOLUTION

589f8102-1204-4530-8910-bc10294810a4

rec-9999-new-4

NULL

NEW_CLUSTER_CREATED

11. Bereinigen

Um laufende Kosten für Ihr Google Cloud-Konto zu vermeiden, bereinigen Sie die bereitgestellten Ressourcen und das BigQuery-Dataset.

Führen Sie in Cloud Shell Folgendes aus:

# 1. Delete BigQuery Dataset
bq rm -r -f -d ${GCP_PROJECT}:${DATASET_ID}

# 2. Delete Cloud Function (2nd-Gen)
gcloud functions delete validate_address_udf --region=${REGION} --gen2 --quiet

# 3. Delete BigQuery Cloud Connection
bq rm -f --connection US.address_val_conn

Wenn Sie für dieses Lab ein dediziertes Google Cloud-Projekt erstellt haben, können Sie es löschen:

gcloud projects delete ${GCP_PROJECT}

12. Glückwunsch

Glückwunsch! Sie haben in Google Cloud BigQuery mit BigQuery Property Graph, ISO GQL-Abfragen, Hybrid Similarity Matching, inkrementellem Delta Matching und Garantien für die Stabilität persistenter Cluster erfolgreich eine End-to-End-Engine für die Kundenidentitätsabgleichung erstellt.

Das haben Sie gelernt

  • So stellen Sie eine Cloud Functions-Funktion der 2. Generation bereit und machen sie als BigQuery-Remote-Funktion verfügbar.
  • So verarbeiten Sie demografische Kundendaten mit SOUNDEX-phonetischen Codierungen und Adressvalidierung vor.
  • So führen Sie das Blockieren von Kandidaten aus und berechnen hybride Ähnlichkeitswerte mit der Levenshtein-Distanz (EDIT_DISTANCE) und der Jaccard-Ähnlichkeit von Tokens.
  • So erstellen Sie einen BigQuery-Eigenschaftsgraphen (CREATE PROPERTY GRAPH) aus Knoten- und Kantentabellen.
  • Abfragen von Graphpfaden mit ISO-GQL (GRAPH_TABLE) mit {1, 2}-Quantifizierern für k-Hops.
  • So lösen Sie kanonische Kundencluster auf und bewerten die Modellleistung anhand von Ground-Truth-Messwerten.
  • So führen Sie den inkrementellen Delta-Abgleich für tägliche Batch-Aufnahmen ohne vollständige Neuverarbeitung des Datensatzes aus.
  • So wenden Sie einen Überschneidungsschwellenwert von (1 – ε) an, um die Clusterstabilität über Pipelineausführungen hinweg aufrechtzuerhalten.

Nächste Schritte

Referenzdokumente