Ders 03 / 23
Çözümleyici Zinciri
Metnin terime dönüşme kuralının kararı belirlemesi: belirteçleme, normalleştirme ve süzgeçlerden oluşan zincirin aynı derlem üzerinde üç yapılandırmayla kurulması, ham, küçük harfe indiren ve kök bulup durak sözcük atan zincirlerin dizin boyutu, dönen küme ve ilk beş sıra düzleminde karşılaştırılması, Türkçe harf katlaması, kesme işareti ve çekim ekinin yarattığı farkın sayılması.
İçindekiler
Önceki dersin dizini metni tek bir kuralla sözcüklere ayırdı ve bu kural hiç sorgulanmadı. Oysa
denizler ile denizin iki ayrı terim olması bir doğa yasası değil, o kuralın sonucudur. Metni
terime çeviren kural dizisine çözümleyici zinciri denir ve üç adımdan oluşur: belirteçleme
metni parçalara böler, normalleştirme parçaları tek bir yazıma indirger, süzgeçler kalan
belirteçleri değiştirir ya da atar.
Bu zincirin iki yerde birden çalışması esastır: belge dizinlenirken ve sorgu değerlendirilirken. İkisi ayrı kural kullanırsa sorgu, dizinde bulunmayan bir terimi arar. Ders bu zinciri üç ayrı yapılandırmada kuruyor ve aynı soruları üçünde de koşturuyor.
Zincirin Halkaları
Belirteçleme burada metnin arama birimlerine bölünmesidir; M06/K01’in sözcük birimi kavramı
bir dilbilgisi ayrıştırıcısına giren simgeyi adlandırır ve bölme ölçütü orada dilbilgisi
kuralıdır — buradaki ölçüt ise aramanın sözleşmesidir. Normalleştirme de aynı biçimde ayrı bir
şeydir: M17/K01’in ilişkisel normalleştirmesi verinin tekrarını tablolara bölerek giderirken,
buradaki normalleştirme aynı sözcüğün farklı yazımlarını tek terime indirir.
Üç yapılandırma karşılaştırılıyor. Ham zincir yalnız boşluktan böler; noktalama belirtece yapışık kalır, büyük harf korunur. Küçük harf zinciri harf ve rakam dışındaki her karakterde böler ve Türkçe yerel ayarıyla küçük harfe indirir — önceki iki dersin zinciri budur. Kök + durak zinciri buna iki süzgeç ekler: derlemin %60’ından çok belgede geçen terimler atılır, kalan belirteçlerden en uzun çekim eki bir kez çıkarılır.
DC1: derlem değişmedi — 600 kayıt, tohum 20250317, dizinlenen alanlar ad ve ozet. DC2:
bayt sabitleri önceki dersteki sabitlerdir ve üç zincirde de aynıdır. DC3: durak sözcük listesi
elle yazılmaz, ölçülür: belge sıklığı eşiği %60’tır. DC4: kök bulucu en uzun eki bir kez atar
ve gövdeyi üç harften kısa bırakmaz; ünsüz yumuşaması ve ünlü düşmesi bilinmez.
// arama/katalog.mjs — uc konunun paylastigi derlem: kutuphane katalogundan uretilmis // 600 kitap kaydi, tohum 20250317. Alanlar: ad, ozet, konu, yazar, yil, dil, raf. export const TOHUM = 20250317, BELGE = 600; let cekirdek = TOHUM; const rast = () => (cekirdek = (cekirdek * 1103515245 + 12345) % 2147483648) / 2147483648; const sec = (d) => d[Math.floor(rast() * d.length)]; const secZ = (d) => d[Math.floor(rast() ** 2 * d.length)]; // gercek metinde siklik carpiktir const ayir = (s) => s.split("|"); // govde sozcugu: yalin, tamlayan, cogul, yonelme, ayrilma, bulunma export const KOK = ayir("deniz denizin denizler denize denizden denizde|kitap kitabın kitaplar kitaba \ kitaptan kitapta|çocuk çocuğun çocuklar çocuğa çocuktan çocukta|şehir şehrin şehirler şehre \ şehirden şehirde|yol yolun yollar yola yoldan yolda|ada adanın adalar adaya adadan adada|bahçe \ bahçenin bahçeler bahçeye bahçeden bahçede|mektup mektubun mektuplar mektuba mektuptan mektupta|\ gemi geminin gemiler gemiye gemiden gemide|köprü köprünün köprüler köprüye köprüden köprüde|\ okul okulun okullar okula okuldan okulda|kuş kuşun kuşlar kuşa kuştan kuşta").map((s) => s.split(" ")); const KALIP = ayir("0 {} ve gündelik hayat üzerine notlar sunar|0 {} bu derlemenin ana izleğidir|\ 1 {} tarihine geniş yer ayırır|1 {} çevresinde gelişen olayları anlatır|2 {} üzerine derlenmiş \ yazılar içerir|2 {} hakkında kısa öyküler toplar|3 {} açılan bir yolculuğu izler|4 {} toplanmış \ belgeleri sıralar|5 {} tutulan günlüklerden seçmeler verir|5 {} geçen bölümleri İstanbul'un eski \ mahallelerine bağlar").map((s) => [Number(s[0]), s.slice(2)]); const KALIP2 = ayir("{Y} kütüphanesinde tutulan {N} üzerine kuruludur|{N} arasından seçilmiş \ örnekler taşır|{Y} ve çevresindeki {N} listesini verir|{Y} basımı bir {N} derlemesine dayanır"); const EK = ayir("denizci gelenekleri üzerine bir ek bölüm bulunur|Karadeniz kıyısındaki kasabaları \ anlatır|çocukluk anılarına yer verir|kitapçı raflarındaki dağılımı tartışır|yolculuk notlarıyla \ kapanır|adacıklardaki kuş türlerini sayar"); const YER = ayir("Ankara|İzmir|Trabzon|Kars|Bursa|Edirne|Sinop|Antakya"); const NESNE = ayir("harita|fotoğraf|söyleşi|günlük|arşiv belgesi|liman kaydı|kasaba adı|el yazması|gazete kupürü|şarkı sözü"); const ONEK = ayir("Uzak|Kayıp|Sessiz|Eski|Kısa|Büyük|Küçük|Unutulmuş|Beyaz|Yedi"); const SONEK = ayir("Günleri|Öyküleri|Üzerine Notlar|Anıları|Sözlüğü|Rehberi|Yılları|Defteri"); const KONU = ayir("çocuk edebiyatı|roman|kısa öykü|şiir|deniz tarihi|coğrafya|biyografi|gezi yazısı|halk bilimi|mimarlık|müzik|felsefe"); const AD = ayir("Ahmet|Ayşe|Zeynep|Cemal|Nuran|Selim|Elif|Kerem|Hatice|Bedri|Sevgi|Nazlı"); const SOYAD = ayir("Yılmaz|Kaya|Demir|Şahin|Çelik|Aydın|Doğan|Arslan|Koç|Ertem"); const DIL = ayir("Türkçe|Türkçe|Türkçe|İngilizce|Almanca|Fransızca"); const buyut = (s) => s[0].toLocaleUpperCase("tr") + s.slice(1); function ozetUret() { const parca = []; for (let i = 0; i < 3; i += 1) { const [d, k] = secZ(KALIP); parca.push(k.replace("{}", secZ(KOK)[d])); } parca.push(sec(KALIP2).replace("{Y}", sec(YER)).replace("{N}", sec(NESNE))); if (rast() < 0.45) parca.push(sec(EK)); return buyut(parca.join(", ")) + "."; } function adUret() { const k = secZ(KOK), o = sec(ONEK), s = sec(SONEK), t = rast(); if (t < 0.25) return `${o} ${buyut(k[2])}`; if (t < 0.5) return `${buyut(k[0])} ${s}`; if (t < 0.75) return `${o} ${buyut(k[0])} ${s}`; return `${buyut(k[1])} ${s}`; } export const derlem = []; for (let i = 1; i <= BELGE; i += 1) { const konu = [sec(KONU)]; if (rast() < 0.55) konu.push(sec(KONU)); if (rast() < 0.2) konu.push(sec(KONU)); derlem.push({ id: i, ad: adUret(), ozet: ozetUret(), konu: [...new Set(konu)], yazar: `${sec(AD)} ${sec(SOYAD)}`, yil: 1968 + Math.floor(rast() * 57), dil: sec(DIL), raf: `${sec(ayir("TR|EN|DE|FR"))}-${800 + Math.floor(rast() * 99)}.${Math.floor(rast() * 9)}`, }); }
// arama/dizin.mjs — kendi yazilan ters dizin: sozluk, gonderi listesi (belge kimligi, terim // sikligi, konum) ve bayt sayimi. Cozumleyici disaridan verilir; sonraki dersler bunu kullanir. export const BASIT = (s) => s.toLocaleLowerCase("tr").split(/[^\p{L}\p{N}]+/u).filter(Boolean); export const KIMLIK = 4, SIKLIK = 4, KONUM = 4, SOZLUK_EK = 8; // DC2: bayt sabitleri export class TersDizin { sozluk = new Map(); // terim -> gonderi listesi belge = 0; constructor({ coz = BASIT, siklik = true, konum = true } = {}) { Object.assign(this, { coz, siklik, konum }); } ekle(id, metin) { const yerel = new Map(); this.coz(metin).forEach((t, i) => (yerel.get(t) ?? yerel.set(t, []).get(t)).push(i)); for (const [t, k] of yerel) { if (!this.sozluk.has(t)) this.sozluk.set(t, []); this.sozluk.get(t).push({ id, tf: k.length, konum: this.konum ? k : [] }); } this.belge += 1; } liste(t) { return this.sozluk.get(t) ?? []; } ara(...terim) { // kesisim: listeler kimlik sirali oldugu icin tek gecis const l = terim.map((t) => this.liste(t)), p = l.map(() => 0), kume = []; let kars = 0; while (l.every((x, i) => p[i] < x.length)) { const en = Math.max(...l.map((x, i) => x[p[i]].id)); let ayni = true; for (let i = 0; i < l.length; i += 1) { while (p[i] < l[i].length && l[i][p[i]].id < en) { p[i] += 1; kars += 1; } kars += 1; if (p[i] >= l[i].length || l[i][p[i]].id !== en) { ayni = false; break; } } if (ayni) { kume.push(en); p.forEach((_, i) => (p[i] += 1)); } } return { kume, giris: l.reduce((t, x) => t + x.length, 0), kars }; } bayt() { // sozluk + gonderi + konum let s = 0, g = 0, k = 0, giris = 0, konum = 0; for (const [t, liste] of this.sozluk) { s += Buffer.byteLength(t) + SOZLUK_EK; for (const gr of liste) { g += KIMLIK + (this.siklik ? SIKLIK : 0); k += gr.konum.length * KONUM; giris += 1; konum += gr.konum.length; } } return { terim: this.sozluk.size, giris, konum, sozluk: s, gonderi: g, konumBayt: k, toplam: s + g + k }; } }
// arama/cozumleyici.mjs — ayni derlem uc cozumleyici zinciriyle dizinlenir: ham, kucuk harf, // kok bulan + durak sozcuk atan. Dizin boyutu, donen kume, ilk bes sira ve Turkce olculeri. import { derlem, BELGE, TOHUM, KOK as GOVDE } from "./katalog.mjs"; import { TersDizin } from "./dizin.mjs"; const metin = (b) => `${b.ad} ${b.ozet}`; const BOL = (s) => s.split(/[^\p{L}\p{N}]+/u).filter(Boolean); const HAM = (s) => s.split(/\s+/).filter(Boolean); // bolme yok, donusum yok const KUCUK = (s) => BOL(s.toLocaleLowerCase("tr")); const EKLER = ["ları", "leri", "lerin", "ların", "nin", "nın", "nun", "nün", "dan", "den", "tan", "ten", "ler", "lar", "ya", "ye", "da", "de", "ta", "te", "in", "ın", "un", "ün", "a", "e"] .sort((x, y) => y.length - x.length); const kokBul = (t) => { // en uzun eki bir kez atar, govde >= 3 for (const e of EKLER) if (t.length - e.length >= 3 && t.endsWith(e)) return t.slice(0, -e.length); return t; }; // durak sozcuk listesi olculerek secilir: belgelerin %60'indan cogunda gecen terim const on = new TersDizin({ coz: KUCUK, konum: false }); for (const b of derlem) on.ekle(b.id, metin(b)); const DURAK = new Set([...on.sozluk].filter(([, l]) => l.length > BELGE * 0.6).map(([t]) => t)); const KOKLU = (s) => KUCUK(s).filter((t) => !DURAK.has(t)).map(kokBul); const zincir = [["ham", HAM], ["kucuk harf", KUCUK], ["kok + durak", KOKLU]]; const dizin = new Map(zincir.map(([ad, coz]) => { const d = new TersDizin({ coz }); for (const b of derlem) d.ekle(b.id, metin(b)); return [ad, d]; })); const kaynak = derlem.reduce((t, b) => t + Buffer.byteLength(metin(b)), 0); console.log(`tohum ${TOHUM}; derlem ${BELGE} belge, ${kaynak} bayt metin`); console.log(`durak sozcuk (df > %60): ${[...DURAK].join(", ")}`); console.log(`\n${"cozumleyici".padEnd(14)}${"terim".padStart(7)}${"gonderi".padStart(9)}${"konum".padStart(7)}` + `${"dizin bayt".padStart(12)}${"metne oran".padStart(12)}`); for (const [ad, d] of dizin) { const b = d.bayt(); console.log(ad.padEnd(14) + String(b.terim).padStart(7) + String(b.giris).padStart(9) + String(b.konum).padStart(7) + String(b.toplam).padStart(12) + `%${(b.toplam * 100 / kaynak).toFixed(1)}`.padStart(12)); } console.log(`\n${"sorgu".padEnd(11)}${"cozumleyici".padEnd(13)}${"sorgu terimi".padEnd(15)}` + `${"donen belge".padStart(12)}${"ilk bes kimlik".padStart(22)}${"ilk beste degisen".padStart(19)}`); for (const soru of ["Denizler", "kitap", "İstanbul"]) { const satir = zincir.map(([ad, coz]) => { const terim = coz(soru), r = terim.length ? dizin.get(ad).ara(...terim) : { kume: [] }; return [ad, terim.join("+") || "-", r.kume]; }); const olcut = satir.find(([ad]) => ad === "kucuk harf")[2].slice(0, 5); for (const [ad, terim, kume] of satir) { const bes = kume.slice(0, 5); const degisen = ad === "kucuk harf" ? "-" : String(olcut.filter((x, i) => bes[i] !== x).length + Math.max(0, bes.length - olcut.length)); console.log(soru.padEnd(11) + ad.padEnd(13) + terim.padEnd(15) + String(kume.length).padStart(12) + (bes.join(",") || "-").padStart(22) + degisen.padStart(19)); } } // --- Turkce olcusu 1: harf katlamasi (Turkce yerel ayar ile genel kural) --- const genel = new TersDizin({ coz: (s) => BOL(s.toLowerCase()), konum: false }); for (const b of derlem) genel.ekle(b.id, metin(b)); const sapan = [...genel.sozluk.keys()].filter((t) => !on.sozluk.has(t)); console.log(`\nharf katlamasi: Turkce kural ${on.sozluk.size} terim, genel kural ${genel.sozluk.size} ` + `terim; ayrisan terim ${JSON.stringify(sapan)}`); console.log(` "istanbul" sorgusu: Turkce kuralla ${on.ara("istanbul").kume.length} belge, ` + `genel kuralla ${genel.ara("istanbul").kume.length} belge`); console.log(` kesme isareti: "İstanbul'un" -> ${JSON.stringify(KUCUK("İstanbul'un"))}`); // --- Turkce olcusu 2: kok bulucu 12 govde x 6 bicim uzerinde --- let tek = 0; const bolunen = []; for (const satir of GOVDE) { const k = new Set(satir.map(kokBul)); if (k.size === 1) tek += 1; else bolunen.push(`${satir[0]}: ${[...k].join("/")}`); } console.log(`\nkok bulma: ${GOVDE.length} govde x 6 bicim; ${tek} govde tek terime indi, ` + `${bolunen.length} govde bolundu -> ${bolunen.join(", ")}`); const kokKume = new Map(); for (const t of on.sozluk.keys()) (kokKume.get(kokBul(t)) ?? kokKume.set(kokBul(t), []).get(kokBul(t))).push(t); const birlesen = [...kokKume].filter(([, l]) => l.length > 1); console.log(`${on.sozluk.size} terim ${kokKume.size} koke indi; ${birlesen.length} kok birden cok terim topladi`); console.log(`ornek: ${birlesen.slice(0, 3).map(([k, l]) => `${k} <- ${l.join(", ")}`).join(" | ")}`);
tohum 20250317; derlem 600 belge, 135824 bayt metin durak sozcuk (df > %60): ve, gündelik, hayat, üzerine, notlar, sunar cozumleyici terim gonderi konum dizin bayt metne oran ham 261 14727 16489 187780 %138.3 kucuk harf 185 14609 16565 185982 %136.9 kok + durak 122 11722 12627 146080 %107.6 sorgu cozumleyici sorgu terimi donen belge ilk bes kimlik ilk beste degisen Denizler ham Denizler 80 15,16,18,20,22 5 Denizler kucuk harf denizler 122 3,15,16,18,20 - Denizler kok + durak deniz 463 2,3,4,7,8 5 kitap ham kitap 54 13,44,53,80,92 5 kitap kucuk harf kitap 107 7,10,13,28,30 - kitap kok + durak kitap 178 2,5,6,7,10 5 İstanbul ham İstanbul 0 - 5 İstanbul kucuk harf istanbul 73 2,21,30,32,35 - İstanbul kok + durak istanbul 73 2,21,30,32,35 0 harf katlamasi: Turkce kural 185 terim, genel kural 186 terim; ayrisan terim ["i","stanbul","zmir"] "istanbul" sorgusu: Turkce kuralla 73 belge, genel kuralla 0 belge kesme isareti: "İstanbul'un" -> ["istanbul","un"] kok bulma: 12 govde x 6 bicim; 7 govde tek terime indi, 5 govde bolundu -> kitap: kitap/kitab, çocuk: çocuk/çocuğ, şehir: şehir/şehr, bahçe: bahç/bahçe, mektup: mektup/mektub 185 terim 128 koke indi; 18 kok birden cok terim topladi ornek: gemi <- gemi, gemide, gemiye, gemiler, geminin, gemiden | öykü <- öyküleri, öyküler | mektup <- mektup, mektuptan, mektupta, mektuplar
Üç Zincir, Üç Küme
Birinci tablo zincirin dizine ne yaptığını gösteriyor. Ham zincir en çok terimi üretiyor: 261. Bunun nedeni zenginlik değil, aynı sözcüğün noktalamayla ve büyük harfle birden çok kez sayılması. Küçük harf zinciri terim sayısını 185’e indiriyor; kök ve durak süzgeçleri 122’ye. Gönderi girişi 14.727’den 11.722’ye, dizin 187.780 bayttan 146.080 bayta iniyor — süzgeçler dizinin %22’sini siliyor.
İkinci tablo asıl kararı gösteriyor. Denizler sorusu ham zincirde 80, küçük harf zincirinde 122,
kök zincirinde 463 belge döndürüyor. Aynı derlem, aynı soru, beş buçuk kat fark. Ham zincir
yalnız büyük harfle başlayan Denizler belirtecini bulur; küçük harf zinciri yazımdan bağımsız
olarak denizler biçimini bulur; kök zinciri sorguyu deniz gövdesine indirdiği için altı çekimli
biçimin geçtiği bütün belgeleri getirir. İlk derste sayılan 43 belgelik kaybın kaynağı buydu;
kök zinciri kaybın deniz gövdesindeki kısmını kapatıyor, karşılığında küme dörde katlanıyor.
Sıra sütunu bunun sonucunu ayrıca sayıyor. Küçük harf zinciri ölçüt alındığında, hem ham hem kök
zincirinde ilk beş kimliğin beşi de değişiyor. Sıra burada bir ilgililik sıralaması değil,
gönderi listesinin kimlik düzenidir; yine de kullanıcının gördüğü ilk sonuçlar zincir değiştiğinde
tamamen başkalaşır. İstanbul sorusu bunun uç örneğidir: ham zincir sıfır belge döndürür, çünkü
metindeki belirteç İstanbul'un biçiminde ve kesme işaretiyle birlikte tek parçadır.
Türkçenin Ölçüsü
Üçüncü bölüm Türkçenin üç somut zorluğunu sayıyor. Harf katlaması: aynı metin Türkçe yerel
ayarıyla 185, genel kuralla 186 terim üretiyor ve ayrışan terimler ["i", "stanbul", "zmir"].
Genel kural İ harfini i ile birleşik bir noktaya çevirdiği, bölücü de o noktayı harf saymadığı
için İstanbul sözcüğü i ve stanbul diye ikiye ayrılıyor. Sonuç ölçülü: istanbul sorusu
Türkçe kuralla 73 belge, genel kuralla 0 belge döndürüyor.
Kesme işareti: İstanbul'un belirteci ["istanbul", "un"] olarak ikiye bölünüyor. Bölme
sorguyu kurtarıyor, ama anlamı olmayan bir un terimi de üretiyor.
Kök bulma: ölçü 12 gövdenin 6’şar çekimli biçimi üzerinde alınıyor. Yedi gövde tek bir terime
iniyor, beş gövde bölünüyor: kitap/kitab, çocuk/çocuğ, şehir/şehr, bahç/bahçe,
mektup/mektub. İlk üçü ünsüz yumuşaması ve ünlü düşmesi, dördüncüsü ise gövdenin kendisinden ek
sanılan bir harfin atılması yüzündendir. Ters yönde de bir maliyet var: 185 terim 128 köke inerken
18 kök birden çok terimi topluyor ve bunların hepsi doğru birleşme değildir. Bu ders Türkçe kök
bulmayı çözmüyor, kuralın nerede tuttuğunu ve nerede tutmadığını sayıyor; sorgu kitap iken
kitabın geçen belgeler hâlâ dışarıda kalıyor.
Durak sözcük süzgeci de ölçülerek kuruldu: altı terim belgelerin %60’ından çoğunda geçtiği için atıldı. Listede bağlaç olmayan sözcüklerin bulunması, listenin elle değil sıklıkla belirlendiğini gösterir.
Özet
- Çözümleyici zinciri belirteçleme, normalleştirme ve süzgeçlerden oluşur ve hem dizinleme hem sorgu tarafında aynı biçimde çalışmak zorundadır.
- Üç zincir aynı derlemde 261, 185 ve 122 terim üretir; dizin 187.780, 185.982 ve 146.080 bayt tutar — süzgeçler dizinin yaklaşık %22’sini siler.
Denizlersorusu üç zincirde 80, 122 ve 463 belge döndürür; ölçüt zincire göre ilk beş kimliğin beşi de değişir. Zincir seçimi doğrudan kümeyi ve sırayı belirler.- Türkçe harf katlaması kararı çevirir:
istanbulsorusu Türkçe yerel ayarıyla 73, genel kuralla 0 belge döndürür; kesme işaretiİstanbul'unbelirtecini ikiye böler. - Kök bulucu 12 gövdenin 7’sini tek terime indirir, 5’ini ünsüz yumuşaması ve ünlü düşmesi yüzünden böler; 185 terim 128 köke inerken 18 kök birden çok terim toplar.
Sonraki Adım
Bu dersin üç zinciri de bütün metne aynı kuralı uyguladı. Oysa katalogda her alan metin değildir:
raf kodu TR-879.4, dil Türkçe, konu etiketi çocuk edebiyatı — bunlar sözcüklere bölünecek
metinler mi, yoksa tek parça kalması gereken değerler mi. Sonraki ders aynı alanı iki tiple
dizinliyor ve farkı ölçüyor: tam eşleme hangi tipte çalışıyor, sıralama hangisinde tanımlı,
toplama kaç kova üretiyor ve iki tipin dizin boyutu ne kadar ayrışıyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.