İçindekiler
Bir satış temsilcisi CRM’i açar, en önemli müşterisini arar ve üç kayıt bulur: biri şirketin eski unvanıyla, biri büyük harflerle ve Türkçe karaktersiz yazılmış, biri de iki yıl önce ayrılmış bir yetkiliyle. CRM projeleri yazısında bu anın bir CRM’in kaderini nasıl belirlediğini anlatmıştım. Bu yazı o anın teknik tarafı: mükerrer müşteri kayıtlarını bulmak, aynı olanları birleştirmek, aynı görünüp farklı olanları birleştirmemek ve bunun için kalıcı bir süreç kurmak.
Yazının tezi tek cümle: Saha serbestçe girer, sistem geceleri standarda çeker. Veri kalitesi sahadaki ekibin formatla boğuşmasıyla değil; yazılı bir veri standardı, güvenilir kimlik numaraları, resmi kaynaklar ve bunları her gece uygulayan bir süreçle sağlanır. Örnekler Python, PostgreSQL ve Prefect ile; ama ilkeler hangi araçla kurarsanız kurun geçerli.
Mükerrer kayıt müşteriye nasıl yansır?
Mükerrer kayıt bir veri sorunu gibi görünür; ama bedelini müşteri öder:
- Aynı müşteri aynı hafta iki ayrı temsilci tarafından aranır.
- Kampanya e-postası aynı kişiye üç kez gelir; üçüncüsünde kişi e-posta listesinden çıkar.
- Müşteri hizmetleri müşterinin geçmişinin sadece bir kısmını görür ve bir şikayeti ilk kez duyuyormuş gibi davranır.
- Fatura eski adrese gider, tahsilat gecikir.
Ters yön daha tehlikelidir: Yanlış birleştirme, mükerrer kayıttan daha pahalıdır. İki farklı kişinin kayıtları birleşirse birinin faturası diğerine gider, birinin verdiği iletişim izni diğerine uygulanır, birinin borcu diğerinin hesabında görünür. Mükerrer kayıt müşteriyi rahatsız eder; yanlış birleştirme hukuki bir soruna dönüşebilir. Bu yazıdaki temkinli tasarımın, yani kesin kuralların, eşiklerin ve insan onayının nedeni bu.
İlke: Sahaya format yükü değil, arka planda standart
Veri kalitesi sorununa verilen ilk tepki genelde formu sıkılaştırmaktır: telefon için biçim maskesi, onlarca zorunlu alan, “unvanı resmi haliyle yazın” uyarıları. Bunların her biri sahadaki ekibi yavaşlatır ve onu CRM’den uzaklaştırır. Temsilci müşteriyi nasıl biliyorsa öyle yazar: “Örnek Yazılım”, “0532 123 45 67”, “Ali.Kaya@Ornek.com.TR”. Standardı bilmek ve uygulamak sistemin işidir. Bunun için iki şey gerekir.
Yazılı bir veri standardı. Her alanın hangi biçime dönüşeceğini anlatan, iş tarafıyla birlikte yazılmış bir tablo. Pazartesi raporu yazısındaki KPI sözlüğünün müşteri verisindeki karşılığıdır:
| Alan | Saha nasıl girebilir | Standart biçim |
|---|---|---|
| Telefon | 0532 123 45 67, (532) 1234567 | +905321234567 |
| Unvan | ÖRNEK YAZILIM SAN. VE TİC. A.Ş. | Eşleştirme anahtarı: ornek yazilim |
| E-posta | Ali.Kaya@Ornek.com.TR | ali.kaya@ornek.com.tr; alan adı: ornek.com.tr |
| Ad ve soyad | AYŞE yılmaz kaya | Eşleştirme anahtarı: ayse / yilmaz kaya |
| Vergi numarası | 10 hane, boşluklu ya da boşluksuz | 10 hane, kontrol hanesi doğrulanmış |
| Kimlik numarası | 11 hane, boşluklu ya da boşluksuz | Anahtarlı özet; numaranın kendisi sınırlı erişimde |
Ham veriyi ezmeyin. Temsilcinin yazdığı değer olduğu gibi kalır; standart değer ve resmi kaynaktan gelen değer yanında, ayrı sütunlarda tutulur:
| Alan | Ham (sahanın yazdığı) | Standart (eşleştirme için) | Resmi kaynak |
|---|---|---|---|
| Unvan | Örnek Yazılım | ornek yazilim | ÖRNEK YAZILIM SANAYİ VE TİCARET ANONİM ŞİRKETİ |
Bunun üç nedeni var. Kurallar zamanla iyileşir; ham değer durduğu sürece standart değerler her gece baştan hesaplanabilir. Bir değerin nereden geldiği her zaman izlenebilir. Ve ekranda, faturada her zaman bir insanın yazdığı ya da resmi kaynaktan gelen değer görünür; eşleştirme anahtarı hiçbir zaman müşterinin karşısına çıkmaz.
Kimlik: Kimi neyle tanırız?
Tekilleştirmenin ilk sorusu algoritma değil, kimliktir: Bir müşteriyi diğerinden ayıran nedir?
Tüzel kişi: vergi numarası. Bir firma için en güçlü tanımlayıcı vergi numarasıdır. Aynı geçerli vergi numarasına sahip iki kayıt aynı firmadır; bir istisnayla: şubeler. Aynı firmanın şubeleri aynı vergi numarasını taşır ama farklı adreslere, farklı yetkililere, bazen farklı fiyat anlaşmalarına sahiptir. Onları birleştirmeyin; bir “üst müşteri” ilişkisiyle gruplayın.
Gerçek kişi: T.C. kimlik numarası. Türkiye’de bir kişiyi tanımanın en güvenilir yolu kimlik numarasıdır. Aynı zamanda en hassas kişisel verilerden biridir; işlemek için KVKK kapsamında açık bir hukuki dayanağınız olmalı. Pratikte numarayı şifreli saklayın, erişimini sınırlayın, ekranlarda maskeleyin. Eşleştirme için de numaranın kendisini değil, anahtarlı özetini kullanın:
import hashlib
import hmac
def tckn_ozeti(tckn: str, anahtar: bytes) -> str:
"""Eşleştirme için kimlik numarasının anahtarlı özeti.
Düz SHA-256 yetmez: Geçerli kimlik numarası sayısı sınırlıdır ve hepsi
denenerek özet geri çözülebilir. Anahtar şifre yöneticisinde durur.
"""
return hmac.new(anahtar, tckn.encode(), hashlib.sha256).hexdigest()
Eşleştirme kodu böylece kimlik numaralarını hiç görmeden “bu iki kayıt aynı kişiye mi ait?” sorusunu cevaplayabilir. Anahtar, Prefect yazısında anlattığım gizli ayarlar gibi bir şifre yöneticisinde durur.
Kimlik numarası yoksa: Ad, soyad ve iletişim bilgisi neden yetmez?
Kayıtların bir kısmında kimlik numarası hiç olmaz. İlk akla gelen, ad, soyad, telefon ve e-postayı birlikte kullanmaktır. Türkiye’de bu dört alan tek başına yetmez:
- Baba ve oğul. Bir baba oğluna kendi adını vermiş, oğlunun kaydına da kendi telefonunu ve e-postasını girmiş olabilir. Ad, soyad, telefon ve e-posta birebir aynıdır; ama iki ayrı müşteri vardır.
- Ortak iletişim bilgisi. Bir ailenin ortak telefonu, bir şirketin santral numarası ya da info@ adresi kişiye değil haneye ya da kuruma aittir.
- Soyadı tuzağı. Evlilikle soyadı değişir; bazı kadınlar kızlık soyadını koruyarak iki soyadı kullanır (“Ayşe Yılmaz Kaya”). Soyadında birebir eşleşme aramak aynı kişiyi kaçırır. Soyadını kelime kelime karşılaştırın; soyadının farklı olması tek başına “farklı kişi” kanıtı sayılmaz.
Bu yüzden ad, soyad ve iletişim bilgisi hiçbir zaman otomatik birleştirmeye yetmez; en fazla bir aday üretir ve insana sorar. Ayırt etmek için doğum tarihi gibi bir alan ya da kimlik numarası gerekir. Sinyallerin tek başına ne söylediğini bir tabloda toplamak bütün tasarımın temelidir:
| Sinyal | Tek başına ne söyler? |
|---|---|
| Aynı geçerli kimlik numarası ya da vergi no. | Kesin aynı (vergi numarasında şubeler hariç) |
| Farklı geçerli kimlik numaraları | Kesin farklı; ad, soyad ve e-posta aynı olsa bile |
| Farklı doğum tarihleri | Kesin farklı |
| Resmi kaynakta aynı unvan | Çok güçlü |
| Aynı ad ve aynı doğum tarihi | Güçlü |
| Aynı telefon ya da e-posta | Zayıf; haneye ya da kuruma ait olabilir |
| Benzer ad ya da unvan | Zayıf; tek başına sadece aday üretir |
| Farklı soyadı | Hiçbir şey; evlilikle değişmiş olabilir |
İkinci satır bu yazının en önemli kuralıdır: Farklı kimlik numaraları, geri kalan her şeyin aynı olmasından daha güçlü bir kanıttır. Baba ile oğul sorununu bu kural çözer.
Türkçe karakter tuzağı
Tekilleştirme yazılarının çoğu İngilizce veriyle yazılır ve Türkçe veride sessizce çöker. Aşağıdaki tablodaki her satırda göz aynı şeyi görür, bilgisayar farklı:
| Durum | Göz ne görür | Bilgisayar ne yapar |
|---|---|---|
Python’da "İSTANBUL".lower() | istanbul | "i̇stanbul": görünmez bir nokta işaretiyle dokuz karakter; “istanbul”a eşit değil |
Python’da "şirket".upper() | ŞİRKET | "ŞIRKET": noktasız I |
Python’da "IŞIK".lower() | ışık | "işik" |
| Türkçe klavyesiz giriş | Şükrü Öztürk = Sukru Ozturk | İki farklı isim |
| Eski sistemden UTF-8 dosyayı Windows-1252 diye okumak | Şükrü Öztürk | "Şükrü Öztürk" |
| Windows-1254 dosyayı UTF-8 diye okumak | Şükrü | "��kr�": harfler geri dönülmez biçimde kaybolur |
Bunun nedeni, büyük-küçük harf dönüşümünün dile bağlı olmasıdır. Türkçede i’nin büyüğü İ, I’nın küçüğü ı’dır; Python’un ve çoğu veritabanının varsayılan kuralları ise İngilizceye göredir. Veritabanında aynı karşılaştırma, kullanılan collation’a göre farklı sonuç verebilir. Sorunu üç kuralla çözersiniz.
1. Eşleştirme anahtarında dile bağlı lower() ve upper() kullanmayın. İ, I, ı ve i’yi önce tek bir harfe indirin, ardından bütün Türkçe harfleri sabit bir tabloyla ASCII karşılığına katlayın. Sonuç bilgisayarın dil ayarından ve veritabanının collation’ından bağımsız olur; “Şükrü Öztürk”, “ŞÜKRÜ ÖZTÜRK” ve “Sukru Ozturk” aynı anahtarı verir:
import re
import unicodedata
# İ, I, ı ve i önce tek bir harfe iner, ardından Türkçe harfler ASCII'ye katlanır.
# Sonuç bilgisayarın dil ayarından ve veritabanının collation'ından bağımsızdır.
_TR = str.maketrans("İIıiŞşĞğÜüÖöÇç", "iiiissgguuoocc")
def katla(metin: str) -> str:
"""Eşleştirme anahtarı: harf büyüklüğü ve Türkçe karakter farkı kalmaz."""
metin = unicodedata.normalize("NFC", metin).translate(_TR)
# Kalan aksanları ve "i̇"deki görünmez noktayı at.
metin = "".join(c for c in unicodedata.normalize("NFKD", metin) if not unicodedata.combining(c))
return re.sub(r"\s+", " ", metin.lower()).strip()
Aynı katlamayı veritabanında da yapabilirsiniz. PostgreSQL’in translate fonksiyonu, karşılığı olmayan karakteri siler; görünmez nokta (chr(775)) böylece temizlenir:
SELECT lower(translate(unvan, 'İIıiŞşĞğÜüÖöÇç' || chr(775), 'iiiissgguuoocc')) AS anahtar
FROM musteri;
2. Kodlamayı içeri alırken çözün. Eski sistemlerden gelen dosyaların kodlamasını aktarım anında kontrol edin. UTF-8 bir metnin yanlış okunduğunu gösteren izler bellidir (“Ô, “Å” gibi) ve çoğu zaman geri çevrilebilir:
_BOZUK = ("Ã", "Å", "Ä", "�")
def bozuk_mu(metin: str) -> bool:
"""UTF-8 metnin yanlış kodlamayla okunduğunu gösteren izler."""
return any(iz in metin for iz in _BOZUK)
def onar(metin: str) -> str:
"""Windows-1252 diye okunmuş UTF-8 metni geri çevirir; çeviremezse dokunmaz."""
try:
return metin.encode("cp1252").decode("utf-8")
except (UnicodeEncodeError, UnicodeDecodeError):
return metin
Bozuk kayıtların oranını her aktarımda ölçün ve bir eşiği aşarsa aktarımı durdurun. ”�” işareti çıkmışsa veri zaten kaybolmuştur; onu düzeltmenin tek yolu kaynaktan doğru kodlamayla yeniden okumaktır.
3. Katlanmış anahtar sadece eşleştirme içindir. Ekranda, faturada ve e-postada her zaman orijinal yazım görünür. Şükrü Bey’e “Sayın Sukru Ozturk” diye hitap eden bir sistem, tekilleştirmede kazandığı güveni ilk e-postada kaybeder.
Standartlaştırma kuralları
Karakter katlama temelinin üzerine her alan için küçük, test edilebilir birer fonksiyon kurulur. Hepsinin ortak özelliği şu: Standarda çekemediği değeri bozmaz, None döner; ham değer yerinde kalır ve kayıt “standarda çekilemedi” olarak işaretlenir.
Unvan. Hukuki ve kalıp ifadeler firmayı birbirinden ayırmaz; “A.Ş.” ile “Anonim Şirketi” aynı şeydir. Bunları atınca geriye firmanın asıl adı kalır:
# Unvanda firmayı ayırt etmeyen hukuki ve kalıp ifadeler (katlanmış halleriyle).
_UNVAN_KALIPLARI = {
"anonim", "as", "sirketi", "limited", "ltd", "sti",
"sanayi", "san", "ticaret", "tic", "ve",
}
def unvan_anahtari(unvan: str) -> str:
"""'ÖRNEK YAZILIM SAN. VE TİC. A.Ş.' ve 'Ornek Yazilim AS' aynı anahtarı verir."""
metin = katla(unvan).replace(".", "")
kelimeler = re.sub(r"[^\w\s]", " ", metin).split()
return " ".join(k for k in kelimeler if k not in _UNVAN_KALIPLARI)
Kalıp listesini kendi verinize göre büyütün; ama sadece gerçekten ayırt edici olmayan ifadeleri ekleyin. “Yazılım” ya da “Lojistik” gibi faaliyet alanları kalıp değildir; iki farklı firmayı ayıran tek kelime olabilirler.
Telefon. Sahada her biçim görülür; standart tektir:
def telefon_standart(ham: str) -> str | None:
"""'0532 123 45 67', '(532) 1234567', '+90 532...' → '+905321234567'."""
rakamlar = re.sub(r"\D", "", ham)
if rakamlar.startswith("0090"):
rakamlar = rakamlar[4:]
elif rakamlar.startswith("90") and len(rakamlar) == 12:
rakamlar = rakamlar[2:]
elif rakamlar.startswith("0") and len(rakamlar) == 11:
rakamlar = rakamlar[1:]
if len(rakamlar) != 10 or rakamlar[0] not in "23458":
return None # standarda çekilemedi; ham değer yerinde kalır
return "+90" + rakamlar
Yurt dışı numaralarınız varsa bu fonksiyonu ülke koduna göre genişletmek yerine bu iş için yazılmış bir kütüphane kullanın; numara planları ülkeden ülkeye çok değişir.
E-posta. Küçük harfe çevrilir; alan adı ise başlı başına bir ipucudur. Aynı şirket alan adına sahip iki kayıt büyük olasılıkla aynı firmanın çalışanlarıdır; genel e-posta servisleri ise hiçbir şey söylemez:
_GENEL_ALAN_ADLARI = {
"gmail.com", "hotmail.com", "hotmail.com.tr", "outlook.com",
"yahoo.com", "yandex.com", "icloud.com", "live.com",
}
def eposta_standart(ham: str) -> str | None:
eposta = ham.strip().lower()
return eposta if re.fullmatch(r"[^@\s]+@[^@\s]+\.[a-z]{2,}", eposta) else None
def kurumsal_alan_adi(eposta: str) -> str | None:
"""Şirket alan adı bir ipucudur; genel e-posta servisleri değildir."""
alan = eposta.split("@", 1)[1]
return None if alan in _GENEL_ALAN_ADLARI else alan
Ad ve soyadı. CRM’inizde ad ve soyadı ayrı alanlardaysa ikisini ayrı katlayın. Tek alanda tutuluyorsa hangi kelimenin soyadı olduğunu tahmin etmeye çalışmayın; Türkiye’de iki adlı ve iki soyadlı kişiler yaygındır. Kelimeleri bir küme olarak karşılaştırmak tahmin etmekten daha güvenlidir.
Vergi ve kimlik numarası. İkisinin de son haneleri bir kontrol hanesidir; yanlış yazılmış bir numarayı daha içeri girerken yakalarsınız:
def tckn_gecerli_mi(tckn: str) -> bool:
if not re.fullmatch(r"[1-9]\d{10}", tckn):
return False
d = [int(c) for c in tckn]
onuncu = ((d[0] + d[2] + d[4] + d[6] + d[8]) * 7 - (d[1] + d[3] + d[5] + d[7])) % 10
return d[9] == onuncu and d[10] == sum(d[:10]) % 10
def vkn_gecerli_mi(vkn: str) -> bool:
if not re.fullmatch(r"\d{10}", vkn):
return False
d = [int(c) for c in vkn]
toplam = 0
for i in range(9):
ara = (d[i] + 9 - i) % 10
deger = (ara * 2 ** (9 - i)) % 9
if ara != 0 and deger == 0:
deger = 9
toplam += deger
return d[9] == (10 - toplam % 10) % 10
Kontrol hanesi tutmayan numara silinmez, “doğrulanamadı” diye işaretlenir. Kontrol hanesinin tutması ise numaranın gerçekten o kişiye ya da firmaya ait olduğunu kanıtlamaz; sadece yazım hatası olmadığını gösterir. Bir adım ötesi resmi kaynaklardır.
Resmi kaynaklarla zenginleştirmek
Sahadaki ekibin “Örnek Yazılım” diye yazdığı firma, resmi kayıtta “ÖRNEK YAZILIM SANAYİ VE TİCARET ANONİM ŞİRKETİ”dir. Vergi numarası girilmiş bir firmanın resmi bilgisini bir kez getirdiğinizde eşleştirme artık serbest metinle değil, resmi unvanla yapılır. Bu, tekilleştirmenin en ucuz ve en güvenilir kazancıdır.
- Vergi numarasından resmi unvana. E-fatura entegratörlerinin çoğu, vergi numarasıyla firmanın resmi unvanını ve e-fatura kullanıcısı olup olmadığını dönen bir mükellef sorgulama servisi sunar. Gelir İdaresi de e-Fatura kayıtlı kullanıcılarının listesini yayımlar. Hangisini kullanacağınızı entegratörünüzün sunduğu imkanlar belirler.
- Kimlik numarasından doğrulamaya. Kimlik numarasını ad, soyad ve doğum yılıyla doğrulayan bir kamu servisi de var. Ama kişisel veriyle çalıştığı için kullanmadan önce hukuki dayanağınızı ve servisin kullanım koşullarını hukuk birimiyle netleştirin.
Resmi kaynaktan gelen değer de ham değeri ezmez; kendi sütununda durur. İkisi arasındaki fark başlı başına bir bulgudur: Kayıtta “Örnek Yazılım” yazan bir vergi numarası resmi kaynakta “Deniz Lojistik” çıkıyorsa ya numara yanlış girilmiştir ya da yanlış firmanın numarası kopyalanmıştır. Bu kayıtlar birleştirmeye değil, düzeltme listesine gider.
Resmi kaynaklar genelde ücretli ya da kotalıdır; her gece binlerce kaydı baştan sorgulamak hem kotayı hem bütçeyi tüketir. Bir numaranın resmi unvanı her gün değişmez. Aşağıdaki Prefect akışında bu adım bir hız limitiyle sınırlanır, geçici hatalarda bekleyerek yeniden denenir ve aynı numara otuz gün içinde ikinci kez sorgulanmaz.
Eşleştirme ve üç eşik
Önce kesin eşleşmeler. Aynı kimlik özetine ya da aynı geçerli vergi numarasına sahip kayıtları bulmak için algoritmaya gerek yoktur, SQL yeter:
SELECT tckn_ozeti, array_agg(id ORDER BY id) AS kayitlar
FROM musteri
WHERE tckn_ozeti IS NOT NULL
AND birlestirildigi_id IS NULL
GROUP BY tckn_ozeti
HAVING count(*) > 1;
Sonra gruplama. Bir milyon kaydı birbiriyle karşılaştırmak yarım trilyon çift demektir. Kayıtları önce kaba bir anahtarla gruplayın ve sadece aynı gruptakileri karşılaştırın: firmalarda unvan anahtarının ilk kelimesi, kişilerde adın ilk harfleri ve varsa doğum yılı. Gruplama anahtarı katlanmış değerlerden üretilmelidir; yoksa “Şükrü” ile “Sukru” farklı gruplara düşer ve hiç karşılaştırılmaz.
Sonra karar. Her aday çift için bir karar fonksiyonu çalışır. Önce kesin kurallara bakar, sonra sinyalleri puanlar. Aşağıdaki fonksiyon kişiler içindir; firmalar için aynı yapı vergi numarası, resmi unvan ve kurumsal alan adıyla kurulur:
from dataclasses import dataclass
from difflib import SequenceMatcher
@dataclass
class Kisi:
ad: str
soyad: str
tckn_ozeti: str | None = None
dogum_tarihi: str | None = None # "1985-04-12"
telefon: str | None = None # standart biçimde
eposta: str | None = None # standart biçimde
def soyad_uyumu(a: str, b: str) -> str:
"""Soyadı kelime kelime karşılaştırılır: 'Yılmaz Kaya' ile 'Kaya' ortaktır."""
ka, kb = set(katla(a).split()), set(katla(b).split())
if not ka or not kb:
return "bilinmiyor"
if ka == kb:
return "ayni"
return "ortak" if ka & kb else "farkli"
OTOMATIK, INCELEME = 85, 50
def karar_ver(a: Kisi, b: Kisi) -> tuple[str, int, list[str]]:
"""('birlestir' | 'incele' | 'ayri', puan, gerekçeler)"""
# 1. Kesin kurallar: kimlik numarası ve doğum tarihi tartışmaya açık değildir.
if a.tckn_ozeti and b.tckn_ozeti:
if a.tckn_ozeti == b.tckn_ozeti:
return "birlestir", 100, ["Kimlik numarası aynı"]
return "ayri", 0, ["Kimlik numaraları farklı"]
if a.dogum_tarihi and b.dogum_tarihi and a.dogum_tarihi != b.dogum_tarihi:
return "ayri", 0, ["Doğum tarihleri farklı"]
# 2. Puanlı sinyaller: hiçbiri tek başına yetmez.
puan, gerekce = 0, []
if SequenceMatcher(None, katla(a.ad), katla(b.ad)).ratio() >= 0.9:
puan += 30
gerekce.append("Ad aynı ya da çok benzer")
soyad = soyad_uyumu(a.soyad, b.soyad)
if soyad == "ayni":
puan += 20
gerekce.append("Soyadı aynı")
elif soyad == "ortak":
puan += 15
gerekce.append("Soyadında ortak kelime var")
elif soyad == "farkli":
gerekce.append("Soyadı farklı (evlilikle değişmiş olabilir)")
if a.dogum_tarihi and a.dogum_tarihi == b.dogum_tarihi:
puan += 40
gerekce.append("Doğum tarihi aynı")
if a.telefon and a.telefon == b.telefon:
puan += 10
gerekce.append("Telefon aynı (aileye ait olabilir)")
if a.eposta and a.eposta == b.eposta:
puan += 10
gerekce.append("E-posta aynı (aileye ait olabilir)")
if puan >= OTOMATIK:
return "birlestir", puan, gerekce
return ("incele" if puan >= INCELEME else "ayri"), puan, gerekce
Puanlar keyfi değil, sinyal tablosunun koda dökülmüş halidir. Fonksiyonun bazı senaryolardaki davranışı şöyle:
| Senaryo | Puan | Karar |
|---|---|---|
| Baba ve oğul: ad, soyad, telefon, e-posta aynı; kimlik yok | 70 | İncele |
| Baba ve oğul: kimlik numaraları farklı | — | Ayrı |
| Baba ve oğul: doğum tarihleri farklı | — | Ayrı |
| Evlilikle soyadı değişmiş: ad, doğum tarihi, telefon aynı | 80 | İncele |
| ”Ayşe Kaya” ve “Ayşe Yılmaz Kaya”: ad, doğum tarihi aynı | 85 | Birleştir |
| Aynı kimlik numarası, farklı soyadı | 100 | Birleştir |
Dikkat edin: Ad, soyad, telefon ve e-posta birebir aynı olduğu halde baba ve oğul otomatik birleşmiyor; insana soruluyor. Soyadı değişmiş kadın da birleştirilmiyor ama kaçırılmıyor; o da insana soruluyor.
Eşikleri tahminle koymayın. Birkaç yüz aday çifti iş tarafıyla birlikte elle etiketleyin (“aynı” / “farklı”) ve eşikleri bu örneğe göre ayarlayın. Otomatik eşiğin hemen üstündeki çiftlerde tek bir yanlış birleştirme görüyorsanız eşik çok düşüktür. Daha sonra, insan kontrolünden gelen kararlar bu örneği her hafta büyütür.
Bu örnekte ad benzerliği için Python’un standart kütüphanesindeki difflib yeterli. Milyonlarca çiftte daha hızlı bir benzerlik kütüphanesine geçebilir ya da PostgreSQL’in pg_trgm eklentisiyle benzerliği doğrudan veritabanında hesaplayabilirsiniz. Karar mantığı aynı kalır.
Kararsız kayıtlar: insan gözüyle kontrol
“İncele” kararı veren çiftler bir inceleme kuyruğuna gider. Bu kuyruk tekilleştirmenin en az konuşulan ama en çok emek isteyen parçasıdır; iyi tasarlanmazsa insanların her gün aynı soruyu tekrar tekrar cevapladığı bir yere dönüşür.
Kim bakar? Sahadaki ekip değil, müşteri verisinin sahibi. İnceleme kuyruğu onun iş listesidir; kuyruğun yaşı da onun takip ettiği bir metriktir.
Ekranda ne görür? İki kayıt yan yana, farklı olan alanlar vurgulu ve en önemlisi, kararın gerekçesi. Karar veren kişinin bir puanı değil, nedeni görmesi gerekir:
| Kayıt A | Kayıt B | |
|---|---|---|
| Ad soyad | Mehmet Kaya | Mehmet Kaya |
| Kimlik numarası | yok | yok |
| Doğum tarihi | yok | yok |
| Telefon | +905321234567 | +905321234567 |
| E-posta | mehmet.kaya@gmail.com | mehmet.kaya@gmail.com |
| İlk kayıt | 2016 | 2025 |
Neden incelemeye geldi? Ad, soyad, telefon ve e-posta aynı; ama kimlik numarası da doğum tarihi de yok. Telefon ve e-posta aileye ait olabilir. Puan 70; otomatik birleştirme eşiğinin altında.
Hangi kararları verebilir?
- Aynı kişi: Çift bir sonraki gece birleştirilir.
- Farklı kişi: Çift bir daha önerilmez.
- İlişkili: Baba ve oğul ya da firma ve şubesi gibi; birleştirilmez, bir ilişkiyle bağlanır.
- Emin değilim: Kayıtların sahibi olan temsilciye tek bir soru olarak gider: “Bu iki kayıt aynı kişi mi?”
Kuyruk ve kararlar aynı tabloda durur. Çift her zaman küçük numara önde yazılır; böylece aynı çift iki farklı sırayla iki kez eklenemez:
CREATE TABLE inceleme_kuyrugu (
musteri_a bigint NOT NULL,
musteri_b bigint NOT NULL,
puan int NOT NULL,
gerekce text NOT NULL,
eklendi timestamptz NOT NULL DEFAULT now(),
karar text CHECK (karar IN ('ayni', 'farkli', 'iliskili', 'emin_degilim')),
karar_veren text,
karar_zamani timestamptz,
PRIMARY KEY (musteri_a, musteri_b),
CHECK (musteri_a < musteri_b)
);
-- Gece akışı yeni adayları ekler; daha önce sorulmuş bir çift tekrar eklenmez.
INSERT INTO inceleme_kuyrugu (musteri_a, musteri_b, puan, gerekce)
VALUES (%(a)s, %(b)s, %(puan)s, %(gerekce)s)
ON CONFLICT (musteri_a, musteri_b) DO NOTHING;
Bu tasarımın iki sonucu var:
- “Farklı” kararı kalıcıdır. Veri sahibi “bunlar farklı kişi” dediği bir çifti ertesi gece tekrar görmez. Bunu yapmayan bir kuyruk, birkaç hafta içinde kimsenin açmadığı bir listeye dönüşür.
- Kararlar eşikleri kalibre eder. Her karar, eşleştirmenin doğruluğunu ölçen etiketli bir örnektir. Otomatik eşiğin hemen altındaki çiftlerin neredeyse hepsi “aynı” çıkıyorsa eşik güvenle aşağı çekilebilir; “farklı” çıkanlar çoğalıyorsa bir sinyalin ağırlığı fazladır.
Birleştirme ve ana kayıt
İki kaydın aynı olduğuna karar verildikten sonra iki soru kalır: Hangi bilgi kalacak ve birleştirme nasıl yapılacak?
Hangi bilgi kalacak? Alan bazında bir öncelik sırası belirleyin: önce resmi kaynak, sonra en güncel kayıt, sonra en eksiksiz kayıt. Unvan resmi kaynaktan, adres en son güncellenen kayıttan, telefon dolu olan kayıttan gelir. Bu kuralları birleştirme anında kayıtların üzerine yazarak değil, birbirine bağlanmış kayıtlar üzerinden hesaplayarak uygulayın. Birleşen kaydın değerleri silinmediği sürece birleştirme her zaman geri alınabilir.
Birleştirme nasıl yapılacak? Mükerrer kayıt silinmez; “şu kayda birleştirildi” diye işaretlenir. Her birleştirme, geri alınabilmesi için gereken bilgiyle birlikte bir birleştirme kaydına yazılır:
ALTER TABLE musteri ADD COLUMN birlestirildigi_id bigint REFERENCES musteri (id);
CREATE TABLE birlestirme_kaydi (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kalan_id bigint NOT NULL REFERENCES musteri (id),
birlesen_id bigint NOT NULL REFERENCES musteri (id),
karar_veren text NOT NULL, -- 'otomatik' ya da kişi
gerekce text NOT NULL,
tasinan_teklifler bigint[] NOT NULL, -- geri almak için
zaman timestamptz NOT NULL DEFAULT now(),
geri_alindi timestamptz
);
-- Bir kayıt aynı anda yalnızca bir kez birleşmiş olabilir.
CREATE UNIQUE INDEX birlestirme_tek ON birlestirme_kaydi (birlesen_id)
WHERE geri_alindi IS NULL;
Birleştirmenin bütün adımları tek bir veritabanı işleminde yapılır: ya hepsi olur ya hiçbiri. Yarıda kesilen bir birleştirme, teklifleri taşınmış ama kendisi işaretlenmemiş bir müşteri bırakır; bu, mükerrer kayıttan daha kötü bir durumdur:
def birlestir(conn, kalan: int, birlesen: int, karar_veren: str, gerekce: str) -> bool:
p = {"kalan": kalan, "birlesen": birlesen, "karar_veren": karar_veren, "gerekce": gerekce}
with conn.transaction():
aktif = conn.execute(
"""SELECT id FROM musteri
WHERE id IN (%(kalan)s, %(birlesen)s) AND birlestirildigi_id IS NULL
FOR UPDATE""",
p,
).fetchall()
if len(aktif) != 2:
return False # biri zaten birleşmiş; dokunma
conn.execute(
"""INSERT INTO birlestirme_kaydi
(kalan_id, birlesen_id, karar_veren, gerekce, tasinan_teklifler)
SELECT %(kalan)s, %(birlesen)s, %(karar_veren)s, %(gerekce)s,
coalesce(array_agg(id), '{}')
FROM teklif WHERE musteri_id = %(birlesen)s""",
p,
)
conn.execute("UPDATE teklif SET musteri_id = %(kalan)s WHERE musteri_id = %(birlesen)s", p)
conn.execute("UPDATE musteri SET birlestirildigi_id = %(kalan)s WHERE id = %(birlesen)s", p)
return True
FOR UPDATE iki kaydı işlem boyunca kilitler. Aynı birleştirme ikinci kez denenirse fonksiyon hiçbir şeye dokunmadan False döner; bir şekilde bu kontrol atlansa bile benzersiz indeks ikinci kaydı reddeder ve bütün işlem geri alınır. Örnekte bağlı tablo olarak sadece teklifler var; siparişler, faturalar, görüşme notları için aynı adımı tekrarlayın.
Yanlış bir birleştirmeyi geri almak da tek bir işlemdir. Birleştirme kaydı neyin taşındığını bildiği için, birleştirmeden sonra açılmış yeni tekliflere dokunmadan sadece taşınanları geri götürür:
-- %(kayit)s: geri alınacak birleştirme kaydının numarası. Üç ifade tek bir işlemde çalışır.
UPDATE teklif t SET musteri_id = k.birlesen_id
FROM birlestirme_kaydi k
WHERE k.id = %(kayit)s AND k.geri_alindi IS NULL AND t.id = ANY (k.tasinan_teklifler);
UPDATE musteri m SET birlestirildigi_id = NULL
FROM birlestirme_kaydi k
WHERE k.id = %(kayit)s AND k.geri_alindi IS NULL AND m.id = k.birlesen_id;
UPDATE birlestirme_kaydi SET geri_alindi = now()
WHERE id = %(kayit)s AND geri_alindi IS NULL;
Benzersiz indeks sadece geri alınmamış birleştirmelere uygulandığı için, geri alınmış bir çift daha sonra doğru kararla yeniden birleştirilebilir.
Her şey birleştirilmez. Firma ve şubeleri bir üst müşteri ilişkisiyle, aynı hanenin bireyleri isterseniz bir hane ilişkisiyle bağlanır. Böylece “bu ailenin bütün kayıtları” sorusu cevaplanabilir, ama kimsenin faturası başkasına gitmez.
Prefect ile gece döngüsü
Buraya kadar anlatılan her parça tek başına çalışan bir fonksiyon. Onları her gece aynı sırayla, izlenebilir ve hata durumunda haber veren bir sürece bağlayan şey orkestrasyon:
yeni ve değişen kayıtlar → standartlaştır → resmi kaynaklarla zenginleştir
→ eşleştir → [onay kapısı] → birleştir → raporla
│
└─ belirsiz çiftler → inceleme kuyruğu
Akış bilerek ince tutuldu: İş mantığı yukarıdaki fonksiyonlarda ve projenin kendi modüllerinde durur, akış sadece sırayı, tekrar denemeyi, kotayı ve onayı yönetir:
from datetime import datetime, timedelta, timezone
from prefect import flow, task
from prefect.artifacts import create_table_artifact
from prefect.cache_policies import INPUTS
from prefect.concurrency.sync import concurrency, rate_limit
from prefect.flow_runs import pause_flow_run
from prefect.input import RunInput
from prefect.logging import get_run_logger
from prefect.tasks import exponential_backoff
from prefect.variables import Variable
from musteri import depo, entegrator, eslestirme # projenizin kendi modülleri
SON_CALISMA = "musteri_tekillestirme_son_calisma"
class BirlestirmeOnayi(RunInput):
onay: bool
aciklama: str = ""
@task(retries=3, retry_delay_seconds=exponential_backoff(backoff_factor=10))
def degisenleri_cek(baslangic: datetime) -> list[dict]:
"""Son çalışmadan beri eklenen ya da değişen müşteri kayıtları."""
return depo.degisen_musteriler(baslangic)
@task
def standartlastir(kayitlar: list[dict]) -> list[dict]:
"""Ham değerlere dokunmadan standart sütunları hesaplar ve yazar."""
return depo.standart_degerleri_yaz(kayitlar)
@task(
retries=3,
retry_delay_seconds=exponential_backoff(backoff_factor=30),
cache_policy=INPUTS,
cache_expiration=timedelta(days=30),
persist_result=True,
)
def resmi_bilgi(vkn: str) -> dict | None:
"""Vergi numarasının resmi unvanı. Aynı numara 30 gün içinde tekrar sorgulanmaz."""
rate_limit("entegrator-api")
return entegrator.mukellef_sorgula(vkn)
@task
def adaylari_bul(kayitlar: list[dict]) -> tuple[list[dict], list[dict]]:
"""(otomatik birleşecek çiftler, incelemeye gidecek çiftler)"""
return eslestirme.aday_ciftler(kayitlar)
@task
def kuyruga_yaz(ciftler: list[dict]) -> None:
depo.inceleme_kuyruguna_ekle(ciftler) # ON CONFLICT DO NOTHING
@task # Retry yok: yarım kalmış bir birleştirmeyi tekrar denemek yeni bir risk yaratır.
def birlestir(cift: dict) -> bool:
return depo.birlestir(cift["kalan"], cift["birlesen"], cift["karar_veren"], cift["gerekce"])
@flow(name="musteri-tekillestirme")
def musteri_tekillestirme(tam_tarama: bool = False, onay_iste: bool = False) -> None:
logger = get_run_logger()
simdi = datetime.now(timezone.utc)
baslangic = datetime(1970, 1, 1, tzinfo=timezone.utc)
if not tam_tarama:
baslangic = datetime.fromisoformat(Variable.get(SON_CALISMA, default=baslangic.isoformat()))
kayitlar = standartlastir(degisenleri_cek(baslangic))
for kayit in kayitlar:
if kayit["vkn_gecerli"]:
kayit["resmi"] = resmi_bilgi(kayit["vkn"])
otomatik, belirsiz = adaylari_bul(kayitlar)
kuyruga_yaz(belirsiz)
otomatik += depo.onaylanan_inceleme_kararlari() # veri sahibinin "aynı" dediği çiftler
create_table_artifact(
key="musteri-tekillestirme",
table=[{
"taranan": len(kayitlar),
"otomatik_birlesme": len(otomatik),
"incelemeye_giden": len(belirsiz),
"bekleyen_kuyruk": depo.bekleyen_inceleme_sayisi(),
}],
description="Gece tekilleştirme özeti",
)
if onay_iste and otomatik:
onay = pause_flow_run(wait_for_input=BirlestirmeOnayi, timeout=8 * 60 * 60)
if not onay.onay:
logger.info("Birleştirme onaylanmadı: %s", onay.aciklama)
return
with concurrency("musteri-birlestirme", occupy=1):
birlesen = sum(birlestir(cift) for cift in otomatik)
logger.info("%s kayıt birleştirildi.", birlesen)
Variable.set(SON_CALISMA, simdi.isoformat(), overwrite=True)
Akıştaki her ayar bir sorunu çözüyor:
- Retry sadece okumada. Kaynaktan okumak ve resmi kaynağı sorgulamak, geçici bir hatada tekrar denenebilecek kadar güvenlidir; artan bekleme süreleriyle (
exponential_backoff) karşı tarafa yüklenmeden yeniden denenir. Birleştirmede retry yoktur. Prefect yazısında anlattığım gibi, retry bir güvenlik ağı değil bir çarpandır; yazan bir adımı körlemesine tekrarlamak, sorunu da tekrarlar. - Kota ve maliyet.
rate_limit("entegrator-api"), resmi kaynağa atılan sorguları belirlediğiniz hızla sınırlar.cache_policy=INPUTSvecache_expiration=timedelta(days=30)ise aynı vergi numarasının otuz gün içinde ikinci kez sorgulanmasını önler; sonuç Prefect’in sakladığı önbellekten gelir. Önbelleğin çalışması için sonucun saklanması gerekir;persist_result=Truebu yüzden orada. - Kaldığı yer. Gece akışı sadece son başarılı çalışmadan beri değişen kayıtlarla çalışır. Son çalışma zamanı bir Prefect değişkeninde (
Variable) tutulur ve ancak akış başarıyla biterse güncellenir; yarıda kalan bir gece, ertesi gece aynı yerden yeniden başlar. - Çakışma. Birleştirme adımı
concurrency("musteri-birlestirme")ile tek kişilik bir kilidin içinde çalışır. Elle başlatılmış bir temizlik ile gece akışı aynı anda çalışsa bile aynı müşteriyi iki farklı kayda birleştiremez. - Rapor. Her çalışma Prefect arayüzüne küçük bir özet tablo bırakır. Hep aynı anahtarla (
musteri-tekillestirme) yayımlandığı için arayüzde gece gece nasıl değiştiği de görünür: Bekleyen kuyruk büyüyor mu, otomatik birleşme sayısı düşüyor mu?
Onay kapısı. İlk büyük temizlik, her gece yapılan küçük bakımdan farklıdır: Yıllardır biriken veride tek seferde binlerce birleştirme önerilebilir. Bu çalışmada akış birleştirmeye geçmeden önce özet tabloyu yayımlar ve pause_flow_run ile durur. Prefect arayüzünde bir onay formu açılır; veri sahibi özete bakar, “onay” kutusunu işaretler ya da gerekçesini yazarak reddeder. Veri sahibi onaylamadan tek bir kayıt bile birleşmez. CRM yazısındaki “Canlıya geçmeden önce bu veriye kim kefil olacak?” sorusunun teknik cevabı budur.
pause_flow_run akışı bellekte tutarak bekletir; varsayılan bekleme süresi bir saattir ve süre dolarsa çalışma başarısız sayılır. Örnekte bu yüzden sekiz saatlik bir süre verildi. Onay sadece elle başlatılan ilk temizlikte istenir; gece akışı onay beklemez. Uzun beklemeleri zamanlanmış bir çalışmada da istiyorsanız suspend_flow_run çalışmayı tamamen durdurur ve sunucu kaynağını boşa tutmaz; ama bunun için çalışmanın bir deployment’tan başlatılmış olması ve sonuçların saklanması gerekir.
Akış onay beklemeye geçtiğinde veri sahibinin bundan haberi olmalı. Prefect’te bir otomasyon tanımlayın: Tetikleyici “Flow run state change”, durum Paused, eylem veri sahibine bir bildirim. Aynı otomasyonu Failed durumu için de kurun; gece akışı sessizce durmasın.
Deployment’lar. Prefect yazısındaki ilke burada da geçerli: Deployment’lar kodda durur. Aynı akıştan iki deployment çıkar; biri her gece çalışır, diğeri ilk büyük temizlik için elle başlatılır:
from prefect import Flow
from prefect.schedules import Cron
akis = Flow.from_source(source="/app", entrypoint="flows/musteri_tekillestirme.py:musteri_tekillestirme")
# Her gece: sadece yeni ve değişen kayıtlar.
akis.deploy(
name="musteri-tekillestirme-gece",
work_pool_name="varsayilan-havuz",
schedule=Cron("0 2 * * *", timezone="Europe/Istanbul"),
tags=["veri-kalite", "musteri"],
)
# İlk büyük temizlik: elle başlatılır, bütün veriyi tarar ve birleştirmeden önce onay ister.
akis.deploy(
name="musteri-tekillestirme-ilk-temizlik",
work_pool_name="varsayilan-havuz",
parameters={"tam_tarama": True, "onay_iste": True},
tags=["veri-kalite", "musteri"],
)
Akışın kullandığı iki limit bir kez oluşturulur: biri birleştirmeyi tek kişilik yapan kilit, diğeri resmi kaynağa ortalama saniyede bir sorgu gönderen hız limiti. Örnekteki ayar en fazla beş sorguluk kısa ani artışlara izin verir, sonra hızı saniyede bire düşürür. Değerleri kendi entegratörünüzün kotasına göre ayarlayın:
prefect gcl create musteri-birlestirme --limit 1
prefect gcl create entegrator-api --limit 5 --slot-decay-per-second 1
Sahaya geri bildirim, ama yük değil
Sahadaki ekip bu sürecin büyük kısmını hiç görmez; görmemelidir de. Telefon numarasını standarda çekmek, unvanı katlamak, mükerrer kaydı birleştirmek sistemin işidir. Ekip sadece sistemin gerçekten çözemediği kayıtlardan haberdar olur, haftada bir kısa bir listeyle:
Bu hafta sizin müşterilerinizden üçünde düzeltme gerekiyor: İki firmanın vergi numarası resmi kaynakta bulunamadı, bir müşterinin iki kaydı için “aynı kişi mi?” sorusu sizi bekliyor.
Liste kısa olmalı; uzun bir liste, sorunun sahada değil kurallarda olduğunu gösterir. “Yeni müşteri eklemeden önce bir arama yapın” alışkanlığı da bir kural değil, öneri olarak kalır. Mükerrer kaydı önlemek güzeldir; ama asıl güvence, oluştuğunda onu bulan süreçtir.
Ölçmek
Prefect yazısındaki son ilke burada da geçerli: Akışın çalışmış olması değil, verinin iyileşmiş olması önemlidir. Beş metrik bunu gösterir:
| Metrik | Neyi gösterir |
|---|---|
| Standarda uyum oranı | Kayıtların yüzde kaçı otomatik olarak standarda çekilebiliyor |
| Resmi kaynakla doğrulanmış kayıt oranı | Vergi numarası resmi kaynakla eşleşen firmaların oranı |
| Mükerrer kayıt oranı | Bekleyen ve birleştirilen aday çiftlerin toplam kayda oranı |
| İnceleme kuyruğunun yaşı | En eski bekleyen çiftin kaç gündür beklediği |
| Geri alınan birleştirme sayısı | Yanlış birleştirmenin doğrudan ölçüsü; sıfıra yakın kalmalı |
Kuyruğun yaşı tek satırlık bir sorgudur:
SELECT count(*) AS bekleyen,
date_part('day', now() - min(eklendi)) AS en_eski_gun
FROM inceleme_kuyrugu
WHERE karar IS NULL;
Geri alınan birleştirmeler artıyorsa otomatik eşik çok düşüktür; inceleme kuyruğu büyüyorsa ya eşik çok yüksektir ya da veri sahibinin bu işe ayırdığı zaman yetmiyordur. İki durumda da çözüm akışta değil, kararlardadır.
Kontrol listesi
- Veri standardı yazılı; her alanın standart biçimi ve sahibi belli.
- Ham değerler korunuyor; standart ve resmi değerler ayrı sütunlarda.
- Eşleştirme anahtarları dile bağlı
lower()yerine sabit bir Türkçe katlama tablosuyla üretiliyor. - Aktarımlarda kodlama hataları ölçülüyor; eşik aşılınca aktarım duruyor.
- Vergi ve kimlik numaralarının kontrol haneleri doğrulanıyor; geçersizler işaretleniyor, silinmiyor.
- Kimlik numarası şifreli ve maskeli; eşleştirme anahtarlı özetle yapılıyor.
- Farklı geçerli kimlik numaraları ve farklı doğum tarihleri kesin ayrım sayılıyor.
- Ad, soyad ve iletişim bilgisi hiçbir zaman tek başına otomatik birleştirmeye yetmiyor.
- Soyadı kelime kelime karşılaştırılıyor; farklı soyadı ayrım kanıtı sayılmıyor.
- Eşikler elle etiketlenmiş bir örnekle ayarlandı ve inceleme kararlarıyla güncelleniyor.
- “Farklı” kararları kalıcı; aynı çift tekrar önerilmiyor.
- Birleştirme tek bir veritabanı işleminde, kayıt altında ve geri alınabilir.
- Şubeler ve aile bireyleri birleştirilmiyor, ilişkilendiriliyor.
- Resmi kaynak sorguları hız limitli, önbellekli ve tekrar denemeli.
- İlk büyük temizlik veri sahibinin onayı olmadan birleştirme yapmıyor.
- Akış onay beklediğinde ya da hata verdiğinde veri sahibine haber gidiyor.
Bonus: Bir yapay zeka asistanına kurdurmak
Aşağıdaki prompt’u Claude Code, Cursor ya da benzeri bir kodlama asistanına verebilirsiniz. Köşeli parantezli yerleri kendi verinize göre doldurun:
Sen kıdemli bir veri mühendisisin. Python, PostgreSQL ve Prefect 3 ile müşteri
verisi için bir tekilleştirme süreci kuracağız. Aşağıdaki kurallara harfiyen uy;
bir kuralı esnetmen gerekirse önce bana sor.
## Veri
- Müşteri tablosu ve alanları: [tablo ve sütun listesini buraya yapıştır]
- Bağlı tablolar (birleştirmede taşınacaklar): [ör. teklif, siparis, fatura]
- Müşteri tipleri: [ör. firma (vergi numarası), kişi (kimlik numarası)]
- Resmi kaynak: [ör. e-fatura entegratörünün mükellef sorgulama API'si]
## Kurallar
1. Ham değerlere asla dokunma. Standart değerler ve resmi kaynaktan gelen
değerler ayrı sütunlarda tutulur.
2. Eşleştirme anahtarlarında dile bağlı lower()/upper() kullanma. İ, I, ı ve i
önce tek harfe iner, Türkçe harfler sabit bir tabloyla ASCII'ye katlanır.
3. Vergi ve kimlik numaralarının kontrol hanelerini doğrula; geçersizleri sil-
me, işaretle. Kimlik numarası eşleştirmede HMAC özetiyle kullanılır; anahtar
Prefect Secret block'ta durur.
4. Kesin kurallar: aynı geçerli kimlik numarası = aynı kişi; farklı geçerli
kimlik numaraları ya da farklı doğum tarihleri = farklı kişi. Ad, soyad,
telefon ve e-posta asla tek başına otomatik birleştirme sebebi olmaz.
Soyadı kelime kelime karşılaştırılır; farklı soyadı ayrım kanıtı değildir.
5. Kararlar üç eşikle verilir: otomatik birleştir [85], incele [50], ayrı.
Her karar gerekçe listesiyle birlikte döner.
6. Belirsiz çiftler inceleme_kuyrugu tablosuna yazılır (küçük numara önde,
ON CONFLICT DO NOTHING). "Farklı" kararı verilmiş çift tekrar önerilmez.
7. Birleştirme tek bir veritabanı işleminde yapılır: iki kayıt FOR UPDATE ile
kilitlenir, bağlı kayıtlar taşınır, mükerrer kayıt silinmez işaretlenir,
taşınanlar birlestirme_kaydi tablosuna yazılır. Koşullu benzersiz indeks
aynı kaydın iki kez birleşmesini engeller. Geri alma prosedürü de yaz.
8. Prefect akışında retry sadece okuma ve resmi kaynak adımlarında olur.
Resmi kaynak sorgusu rate_limit ile sınırlanır ve 30 gün önbelleklenir.
Birleştirme tek kişilik bir concurrency limitinin içinde çalışır. Son
çalışma zamanı bir Prefect Variable'da tutulur. tam_tarama ve onay_iste
parametreleriyle ilk temizlik, birleştirmeden önce pause_flow_run ile
veri sahibinden onay ister.
## İlk görev
Tabloları, standartlaştırma ve doğrulama fonksiyonlarını birim testleriyle
birlikte yaz. Testlerde Türkçe karakter tuzaklarını, baba-oğul ve evlilikle
değişen soyadı senaryolarını mutlaka kapsa. Örnek veriler kurgusal olsun.
Her adımdan sonra ne yaptığını kısaca özetle ve varsayım yaptığın yerleri
açıkça belirt.
Özet
- Saha serbestçe girer, sistem geceleri standarda çeker. Veri standardı yazılıdır; ham değerler korunur, standart ve resmi değerler yanında durur.
- Kimlik her şeyden önce gelir. Firmada vergi numarası, kişide kimlik numarası en güvenilir tanımlayıcıdır. Farklı kimlik numaraları, geri kalan her şeyin aynı olmasından daha güçlü bir kanıttır.
- Ad, soyad ve iletişim bilgisi yetmez. Baba ile oğul aynı adı, aynı telefonu, aynı e-postayı taşıyabilir; soyadı evlilikle değişebilir.
- Türkçe karakterler dile bağlı fonksiyonlara emanet edilmez. Eşleştirme anahtarı sabit bir katlama tablosuyla üretilir; kodlama hataları içeri alırken yakalanır.
- Resmi kaynak, serbest metinden güvenilirdir. Vergi numarasından gelen resmi unvan, eşleştirmenin en ucuz ve en güçlü sinyalidir.
- Makine emin olamadığında insana sorar. Kararlar gerekçeyle birlikte gösterilir; “farklı” kararı kalıcıdır, kararlar eşikleri kalibre eder.
- Birleştirme geri alınabilir, tek bir veritabanı işleminde yapılır ve kayıt altındadır.
- Prefect süreci taşır: okumada retry, resmi kaynakta hız limiti ve önbellek, birleştirmede kilit, ilk temizlikte onay kapısı ve her sabah bir özet.
Tekilleştirme bir temizlik projesi değil, bir sahiplik meselesidir: Veri kalitesi sahadaki ekibin değil, sürecin sorumluluğudur.