feat: alan eşlemesine fakülte sinyali + il adımında tüm iller

Sihirbazın alan çipleri artık YÖK Atlas'ın kendi verisiyle süzülüyor:
kategoriBul program adı kurallarını fakülte kurallarıyla kesiştiriyor
(Siber Güvenlik Müh. artık Hukuk değil, Siyaset Bilimi artık Fen değil).
Aşırı geniş isim regexleri sıkılaştırıldı (bilim/güvenlik/programcı/çevre
+ gastronomi'nin içindeki "astronomi" tuzağı) — sayısalda Hukuk & Siyasal,
sözelde Mühendislik çipi tamamen kalktı.

İl adımındaki .slice(0,15) kırpması kaldırıldı: penceresinde programı olan
TÜM iller listelenir; KKTC/yurtdışı kampüsler ayrı çip grubunda. Haritanın
15-limit yüzünden yanlış "program yok" grilemesi kendiliğinden düzeldi.

Öncelik çiplerine şeffaflık notu eklendi (listeyi daraltmaz, AI'ya yön
verir). /meraklisina şeması ve anlatımı aynı davranışla senkronlandı.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-09 00:11:36 +03:00
parent ea85a053c5
commit 34c8f90e26
7 changed files with 176 additions and 36 deletions

View File

@@ -196,31 +196,38 @@ export function rankWindowFacets(
const satirlar = db
.prepare(
`SELECT isim, il, unitur FROM programs
`SELECT isim, fakulte, il, unitur FROM programs
WHERE tur = ? AND onlisans = ? AND ${EFEKTIF_SIRA} IS NOT NULL
AND ${EFEKTIF_SIRA} >= ?`
)
.all(tur, onlisans, altSinir) as {
isim: string;
fakulte: string | null;
il: string | null;
unitur: string | null;
}[];
// isim → kategori eşlemesi pahalı (12 regex); tekrar eden isimler için önbellek
// isim+fakülte → kategori eşlemesi pahalı (regexler); tekrar edenler için önbellek
const kategoriCache = new Map<string, string[]>();
const kategorileriGetir = (isim: string): string[] => {
let k = kategoriCache.get(isim);
const kategorileriGetir = (
isim: string,
fakulte: string | null
): string[] => {
const anahtar = `${isim}|${fakulte ?? ""}`;
let k = kategoriCache.get(anahtar);
if (!k) {
k = kategoriBul(isim);
kategoriCache.set(isim, k);
k = kategoriBul(isim, fakulte);
kategoriCache.set(anahtar, k);
}
return k;
};
const seciliKategoriler = secim.kategoriler ?? [];
const kategoriUyar = (isim: string) =>
const kategoriUyar = (isim: string, fakulte: string | null) =>
seciliKategoriler.length === 0 ||
kategorileriGetir(isim).some((k) => seciliKategoriler.includes(k));
kategorileriGetir(isim, fakulte).some((k) =>
seciliKategoriler.includes(k)
);
const seciliIller = new Set(secim.iller ?? []);
const ilUyar = (il: string | null) =>
seciliIller.size === 0 || (il != null && seciliIller.has(il));
@@ -230,12 +237,12 @@ export function rankWindowFacets(
let devlet = 0;
let vakif = 0;
for (const { isim, il, unitur } of satirlar) {
for (const { isim, fakulte, il, unitur } of satirlar) {
// 1. adım: kategoriler yalnızca pencereye bağlı (filtre uygulanmaz)
for (const k of kategorileriGetir(isim)) {
for (const k of kategorileriGetir(isim, fakulte)) {
kategoriSay.set(k, (kategoriSay.get(k) ?? 0) + 1);
}
if (!kategoriUyar(isim)) continue;
if (!kategoriUyar(isim, fakulte)) continue;
// 2. adım: iller seçili kategorilerle süzülür
if (il) ilSay.set(il, (ilSay.get(il) ?? 0) + 1);
if (!ilUyar(il)) continue;
@@ -251,10 +258,11 @@ export function rankWindowFacets(
.filter((k) => k.adet >= 3) // gürültüyü ele
.sort((a, b) => b.adet - a.adet);
// Penceredeki programı olan TÜM iller döner (81 il + KKTC/yurtdışı);
// sıralama/gruplama UI'da yapılır. Kırpma yok: kullanıcı her ili görebilmeli.
const iller = [...ilSay.entries()]
.map(([il, adet]) => ({ il, adet }))
.sort((a, b) => b.adet - a.adet)
.slice(0, 15);
.sort((a, b) => b.adet - a.adet);
const uniturler: { grup: UniturGrubu; adet: number }[] = [];
if (devlet > 0) uniturler.push({ grup: "devlet", adet: devlet });

View File

@@ -26,7 +26,7 @@ const KATEGORI_KURALLARI: { kategori: Kategori; kural: RegExp }[] = [
{
kategori: "Mühendislik & Teknoloji",
kural:
/mühendis|yazılım|bilgisayar|bilişim|elektrik|elektronik|mekatronik|makine|makina|otomotiv|inşaat|endüstri|metalurji|malzeme|maden|jeoloji|jeofizik|petrol|enerji|robot|yapay zeka|yapay zekâ|siber|ağ teknolojileri|kontrol|otomasyon|uçak|havacılık|uzay|gemi|deniz ulaştırma|teknoloji|teknolojisi|programcı|elektrik-elektronik|nükleer|tekstil müh/,
/mühendis|yazılım|bilgisayar|bilişim|elektrik|elektronik|mekatronik|makine|makina|otomotiv|inşaat|endüstri|metalurji|malzeme|maden|jeoloji|jeofizik|petrol|enerji|robot|yapay zeka|yapay zekâ|siber|ağ teknolojileri|kontrol|otomasyon|uçak|havacılık|uzay|gemi|deniz ulaştırma|teknoloji|teknolojisi|bilgisayar programcı|elektrik-elektronik|nükleer|tekstil müh/,
},
{
kategori: "Sağlık & Tıp",
@@ -35,13 +35,15 @@ const KATEGORI_KURALLARI: { kategori: Kategori; kural: RegExp }[] = [
},
{
kategori: "Hukuk & Siyasal",
// "güvenlik" bilinçli olarak dar: jenerik hali Siber/Sosyal/Bilgi
// Güvenliği'ni yanlışlıkla bu kategoriye alıyordu.
kural:
/hukuk|adalet|siyaset|kamu yönetim|uluslararası ilişki|siyasal|güvenlik|ceza infaz|milletlerarası|diplomasi/,
/hukuk|adalet|siyaset|kamu yönetim|uluslararası ilişki|siyasal|özel güvenlik|ceza infaz|milletlerarası|diplomasi/,
},
{
kategori: "İktisat & İşletme",
kural:
/işletme|iktisat|ekonomi|maliye|muhasebe|finans|bankacılık|sigortacılık|dış ticaret|lojistik|pazarlama|yönetim bilişim|insan kaynak|çalışma ekonomisi|uluslararası ticaret|bankacılık ve sigortacılık|halkla ilişkiler|reklamcılık|emlak|menkul|gümrük|büro yönetim|yönetici asistan/,
/işletme|iktisat|iktisadi|ekonomi|maliye|muhasebe|finans|bankacılık|sigortacılık|sosyal güvenlik|dış ticaret|elektronik ticaret|lojistik|pazarlama|yönetim bilişim|havacılık yönetimi|enerji yönetimi|bilgi yönetimi|insan kaynak|çalışma ekonomisi|uluslararası ticaret|bankacılık ve sigortacılık|halkla ilişkiler|reklamcılık|emlak|menkul|gümrük|büro yönetim|yönetici asistan/,
},
{
kategori: "Eğitim & Öğretmenlik",
@@ -55,13 +57,15 @@ const KATEGORI_KURALLARI: { kategori: Kategori; kural: RegExp }[] = [
},
{
kategori: "Fen & Temel Bilimler",
// Jenerik "bilim" yok: "Siyaset Bilimi" / "İletişim Bilimleri" /
// "Dilbilimi" gibi programları yanlışlıkla Fen yapıyordu.
kural:
/matematik|fizik|kimya|biyoloji|istatistik|moleküler|genetik|biyoteknoloji|astronomi|aktüerya|fen bilim|bilim/,
/matematik|fizik|kimya|biyoloji|istatistik|moleküler|genetik|biyoteknoloji|\bastronomi|aktüerya|fen bilim|veri bilim|adli bilim/,
},
{
kategori: "Sosyal & Beşeri Bilimler",
kural:
/sosyoloji|tarih|coğrafya|felsefe|edebiyat|dil ve edebiyat|filoloji|arkeoloji|antropoloji|ilahiyat|din kültürü|dilbilim|sosyal hizmet|mütercim|çeviribilim|tercümanlık|türk dili|dünya dilleri|sanat tarihi|halkbilim|dinî/,
/sosyoloji|tarih|coğrafya|felsefe|edebiyat|dil ve edebiyat|filoloji|arkeoloji|antropoloji|ilahiyat|din kültürü|dilbilim|sosyal hizmet|sosyal bilim|islam|mütercim|çeviribilim|tercümanlık|türk dili|dünya dilleri|sanat tarihi|halkbilim|dinî/,
},
{
kategori: "Sanat, Medya & İletişim",
@@ -79,24 +83,108 @@ const KATEGORI_KURALLARI: { kategori: Kategori; kural: RegExp }[] = [
},
{
kategori: "Tarım & Doğa",
// "çevre" bağlamlı: jenerik hali "İç Mimarlık ve Çevre Tasarımı"nı
// yanlışlıkla Tarım'a alıyordu.
kural:
/ziraat|tarım|tarla|bahçe|bitki|hayvan|su ürünleri|gıda|orman|çevre|balıkçılık|peyzaj|tohum|toprak|zootekni/,
/ziraat|tarım|tarla|bahçe|bitki|hayvan|su ürünleri|gıda|orman|çevre müh|çevre sağlığı|çevre koruma|çevre temizli|çevresel|balıkçılık|peyzaj|tohum|toprak|zootekni/,
},
];
/** Verilen program adının girdiği tüm kategorileri döner (0..n). */
export function kategoriBul(isim: string): Kategori[] {
const lower = isim.toLocaleLowerCase("tr-TR");
// YÖK Atlas'ın kendi alan sinyali: programın bağlı olduğu fakülte adı.
// Lisansta güçlü ("Mühendislik Fakültesi", "Hukuk Fakültesi"); önlisansta
// jenerik ("... Meslek Yüksekokulu") olduğundan hiçbir kurala eşleşmez ve
// isim kuralları tek başına devrede kalır. Bir fakülte birden çok kategoriye
// eşlenebilir (ör. Fen-Edebiyat → Fen + Sosyal); program bazında ayrım
// isim∩fakülte kesişimiyle yapılır (bkz. kategoriBul).
const FAKULTE_KURALLARI: { kategori: Kategori; kural: RegExp }[] = [
{
kategori: "Mühendislik & Teknoloji",
kural: /mühendis|teknoloji|bilişim|havacılık ve uzay|denizcilik|uygulamalı bilimler/,
},
{
kategori: "Sağlık & Tıp",
kural: /tıp|sağlık|diş|eczacılık|hemşirelik|veteriner|spor bilim/,
},
{
kategori: "Hukuk & Siyasal",
kural: /hukuk|siyasal|iktisadi|idari|güvenlik/,
},
{
kategori: "İktisat & İşletme",
kural: /iktisadi|işletme|yönetim|ticaret|bankacılık|uygulamalı bilimler|finans/,
},
{ kategori: "Eğitim & Öğretmenlik", kural: /eğitim/ },
{
kategori: "Mimarlık & Tasarım",
kural: /mimarlık|tasarım|güzel sanatlar/,
},
{
kategori: "Fen & Temel Bilimler",
kural: /\bfen\b|fen-edebiyat|doğa bilimleri|temel bilimler/,
},
{
kategori: "Sosyal & Beşeri Bilimler",
kural:
/edebiyat|fen-edebiyat|insan ve toplum|sosyal bilim|ilahiyat|islami|beşeri/,
},
{
kategori: "Sanat, Medya & İletişim",
kural: /iletişim|güzel sanatlar|sanat|konservatuvar|müzik/,
},
{
kategori: "Psikoloji & Danışmanlık",
kural: /insan ve toplum|edebiyat|fen-edebiyat|sosyal bilim|eğitim/,
},
{ kategori: "Turizm & Gastronomi", kural: /turizm|gastronomi/ },
{
kategori: "Tarım & Doğa",
kural: /ziraat|orman|su ürünleri|tarım|doğa bilimleri|veteriner/,
},
];
function eslesenKategoriler(
kurallar: { kategori: Kategori; kural: RegExp }[],
metin: string,
): Kategori[] {
const lower = metin.toLocaleLowerCase("tr-TR");
const sonuc: Kategori[] = [];
for (const { kategori, kural } of KATEGORI_KURALLARI) {
for (const { kategori, kural } of kurallar) {
if (kural.test(lower)) sonuc.push(kategori);
}
return sonuc;
}
/**
* Verilen programın girdiği tüm kategorileri döner (0..n).
*
* Fakülte verilirse isim∩fakülte kesişimi alınır: "Siber Güvenlik
* Mühendisliği" isimden {Mühendislik, Hukuk} çıkarken Mühendislik
* Fakültesi'nde olduğu için Hukuk elenir. Kesişim boşsa ya da fakülte
* hiçbir kurala eşleşmiyorsa (jenerik MYO adları) isim kuralları geçerlidir;
* fakültesiz çağrı bugüne kadarki davranışla birebir aynıdır.
*/
export function kategoriBul(
isim: string,
fakulte?: string | null,
): Kategori[] {
const isimKats = eslesenKategoriler(KATEGORI_KURALLARI, isim);
if (fakulte && isimKats.length > 0) {
const fakulteKats = eslesenKategoriler(FAKULTE_KURALLARI, fakulte);
if (fakulteKats.length > 0) {
const kesisim = isimKats.filter((k) => fakulteKats.includes(k));
if (kesisim.length > 0) return kesisim;
}
}
return isimKats;
}
/** Bir programın seçili kategorilerden en az biriyle eşleşip eşleşmediği. */
export function kategoriEslesir(isim: string, secililer: string[]): boolean {
export function kategoriEslesir(
isim: string,
secililer: string[],
fakulte?: string | null,
): boolean {
if (secililer.length === 0) return true;
const kats = kategoriBul(isim);
const kats = kategoriBul(isim, fakulte);
return kats.some((k) => secililer.includes(k));
}

View File

@@ -106,7 +106,7 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc {
const kategoriFiltre = (havuz: AdayProgram[]) =>
kategoriler.length > 0
? havuz.filter((p) => kategoriEslesir(p.isim, kategoriler))
? havuz.filter((p) => kategoriEslesir(p.isim, kategoriler, p.fakulte))
: havuz;
// 1. Tam filtre: iller + üniversite tipi (SQL), kategori (JS)

View File

@@ -29,6 +29,8 @@ export function tercihDegerlendir(
const olumlu: string[] = [];
const celiskiler: string[] = [];
// Manuel liste satırında fakülte taşınmıyor (ProgramListeSatiri bilinçli
// omit ediyor); eşleşme yalnızca isim kurallarıyla yapılır.
if (kategoriEslesir(program.isim, profil.secimler.kategoriler)) {
olumlu.push("İlgi alanlarınla uyumlu");
} else {