/** * KEŞİF BETİĞİ — OpenAlex kurum verisi × KolayTercih üniversiteleri. * * Amaç: /universite/[slug] sayfalarına yayın/atıf gibi akademik göstergeler * eklemenin fizibilitesini ölçmek. Üretim hattının parçası DEĞİLDİR; DB'ye * yazmaz, src/ altına dokunmaz. yokatlas.db salt okunur açılır. * * Kaynak: https://api.openalex.org/institutions (ücretsiz, anahtarsız, * CC0). "polite pool" için mailto parametresi gönderilir (OPENALEX_MAILTO * ortam değişkeni; yoksa parametre eklenmez). * * Kullanım: * pnpm tsx scripts/kesif-openalex.ts [--cikti ] [--db data/yokatlas.db] * * Çıktı: eşleşme özeti (stdout) + tam JSON (varsayılan: scratchpad yoksa * ./kesif-openalex.json). */ import { writeFileSync } from "node:fs"; import path from "node:path"; import Database from "better-sqlite3"; import { turkishSlugify } from "../src/lib/slug"; // ---------- argümanlar ---------- function arg(ad: string): string | undefined { const i = process.argv.indexOf(`--${ad}`); return i >= 0 ? process.argv[i + 1] : undefined; } const DB_YOLU = arg("db") ?? path.join(process.cwd(), "data", "yokatlas.db"); const CIKTI = arg("cikti") ?? path.join(process.cwd(), "kesif-openalex.json"); const MAILTO = process.env.OPENALEX_MAILTO; // ---------- OpenAlex tipleri (yalnızca kullandığımız alanlar) ---------- type Sayim = { year: number; works_count: number; oa_works_count: number; cited_by_count: number; }; type Kurum = { id: string; ror: string | null; display_name: string; display_name_alternatives: string[]; display_name_acronyms: string[]; international?: { display_name?: Record }; country_code: string; type: string; homepage_url: string | null; image_url: string | null; works_count: number; cited_by_count: number; summary_stats: { "2yr_mean_citedness": number; h_index: number; i10_index: number; }; counts_by_year: Sayim[]; geo: { city: string | null; latitude: number | null; longitude: number | null }; updated_date: string; }; const SELECT = [ "id", "ror", "display_name", "display_name_alternatives", "display_name_acronyms", "international", "country_code", "type", "homepage_url", "image_url", "works_count", "cited_by_count", "summary_stats", "counts_by_year", "geo", "updated_date", ].join(","); async function openAlexKurumlar(ulke: string): Promise { const hepsi: Kurum[] = []; let cursor = "*"; while (cursor) { const url = new URL("https://api.openalex.org/institutions"); url.searchParams.set("filter", `country_code:${ulke},type:education`); url.searchParams.set("per-page", "200"); url.searchParams.set("cursor", cursor); url.searchParams.set("select", SELECT); if (MAILTO) url.searchParams.set("mailto", MAILTO); const res = await fetch(url, { headers: { "User-Agent": "kolaytercih-kesif/0.1" }, }); if (!res.ok) throw new Error(`OpenAlex ${ulke}: HTTP ${res.status}`); const j = (await res.json()) as { meta: { next_cursor: string | null }; results: Kurum[]; }; hepsi.push(...j.results); cursor = j.meta.next_cursor ?? ""; } return hepsi; } // ---------- ad normalizasyonu ---------- /** katalog.ts/uniMapGetir ile aynı: sondaki "(İL)" atılır, slug'lanır. */ function siteSlug(ad: string): string { return turkishSlugify(ad.replace(/\s*\([^)]*\)\s*$/, "").trim()); } const GURULTU = new Set([ "universitesi", "universite", "university", "of", "the", "and", "ve", "vakif", "foundation", ]); /** Gevşek anahtar: gürültü kelimeleri atılmış, sıralı slug parçaları. */ function gevsekAnahtar(ad: string): string { return siteSlug(ad) .split("-") .filter((p) => p && !GURULTU.has(p)) .sort() .join("-"); } /** OpenAlex kurumunun eşleşmede denenecek tüm adları. */ function kurumAdlari(k: Kurum): string[] { const s = new Set([k.display_name, ...(k.display_name_alternatives ?? [])]); const tr = k.international?.display_name?.tr; if (tr) s.add(tr); return [...s]; } /** İngilizce → Türkçe kalıp çevirisi (yalnız eşleşme anahtarı için). */ function ingilizceyiTurkceye(ad: string): string { return ad .replace(/\bTechnical University\b/i, "Teknik Üniversitesi") .replace(/\bUniversity of Technology\b/i, "Teknik Üniversitesi") .replace(/\bUniversity of Health Sciences\b/i, "Sağlık Bilimleri Üniversitesi") .replace(/\bMedical University\b/i, "Tıp Üniversitesi") .replace(/\bUniversity of Economics\b/i, "Ekonomi Üniversitesi") .replace(/\bUniversity of Science and Technology\b/i, "Bilim ve Teknoloji Üniversitesi") .replace(/\bUniversity of Social Sciences\b/i, "Sosyal Bilimler Üniversitesi") .replace(/\bUniversity of Applied Sciences\b/i, "Uygulamalı Bilimler Üniversitesi") .replace(/\bUniversity of Fine Arts\b/i, "Güzel Sanatlar Üniversitesi") .replace(/\bUniversity of\b/i, "") .replace(/\bUniversity\b/i, "Üniversitesi") .replace(/\bFoundation\b/i, "Vakıf") .replace(/\bMiddle East\b/i, "Orta Doğu") .replace(/\bEast\b/i, "Doğu") .replace(/\bMediterranean\b/i, "Akdeniz") .replace(/\bEastern\b/i, "Doğu") .replace(/\bNear\b/i, "Yakın") .replace(/\bCyprus\b/i, "Kıbrıs") .replace(/\bInternational\b/i, "Uluslararası") .replace(/\bScience and Technology\b/i, "Bilim ve Teknoloji"); } // ---------- ana akış ---------- type Eslesme = { slug: string; siteAd: string; hamAdlar: string[]; unitur: string | null; il: string | null; programSayisi: number; yontem: "slug" | "gevsek" | "ceviri" | "arama" | "yok"; openalex: null | { id: string; ror: string | null; display_name: string; country_code: string; homepage_url: string | null; image_url: string | null; works_count: number; cited_by_count: number; h_index: number; i10_index: number; iki_yil_ort_atif: number; yillik: Sayim[]; updated_date: string; }; }; async function main() { // 1) Sitedeki üniversiteler (katalog.ts ile aynı slug haritası) const db = new Database(DB_YOLU, { readonly: true, fileMustExist: true }); const rows = db .prepare( `SELECT universite, MAX(unitur) AS unitur, il, COUNT(*) AS adet FROM programs GROUP BY universite`, ) .all() as { universite: string; unitur: string | null; il: string | null; adet: number }[]; db.close(); const site = new Map(); for (const r of rows) { const slug = siteSlug(r.universite); if (!slug) continue; const e = site.get(slug); if (e) { e.hamAdlar.push(r.universite); e.programSayisi += r.adet; if (!e.unitur && r.unitur) e.unitur = r.unitur; } else { site.set(slug, { slug, siteAd: r.universite.replace(/\s*\([^)]*\)\s*$/, "").trim(), hamAdlar: [r.universite], unitur: r.unitur, il: r.il, programSayisi: r.adet, yontem: "yok", openalex: null, }); } } console.log(`Site: ${site.size} üniversite (slug), ${rows.length} ham ad`); // 2) OpenAlex: TR + CY (KKTC kurumları ROR'da genelde CY olarak kayıtlı) const [tr, cy] = await Promise.all([openAlexKurumlar("TR"), openAlexKurumlar("CY")]); const kurumlar = [...tr, ...cy]; console.log(`OpenAlex: TR ${tr.length} + CY ${cy.length} eğitim kurumu`); // 3) Eşleme dizinleri const slugDizin = new Map(); const gevsekDizin = new Map(); const ceviriDizin = new Map(); const ekle = (m: Map, k: string, kurum: Kurum) => { if (!k) return; const l = m.get(k); if (l) { if (!l.includes(kurum)) l.push(kurum); } else m.set(k, [kurum]); }; for (const k of kurumlar) { for (const ad of kurumAdlari(k)) { ekle(slugDizin, siteSlug(ad), k); ekle(gevsekDizin, gevsekAnahtar(ad), k); const cev = ingilizceyiTurkceye(ad); ekle(ceviriDizin, siteSlug(cev), k); ekle(ceviriDizin, gevsekAnahtar(cev), k); } } const kullanilan = new Set(); const sec = (adaylar: Kurum[] | undefined): Kurum | null => { if (!adaylar) return null; // Birden çok aday: en çok yayını olanı al (ana kampüs), ama daha önce // başka bir üniversiteye verilmişse geç. const serbest = adaylar.filter((k) => !kullanilan.has(k.id)); if (serbest.length === 0) return null; return serbest.sort((a, b) => b.works_count - a.works_count)[0]; }; for (const e of site.values()) { let k = sec(slugDizin.get(e.slug)); let yontem: Eslesme["yontem"] = "slug"; if (!k) { k = sec(gevsekDizin.get(gevsekAnahtar(e.siteAd))); yontem = "gevsek"; } if (!k) { k = sec(ceviriDizin.get(e.slug)) ?? sec(ceviriDizin.get(gevsekAnahtar(e.siteAd))); yontem = "ceviri"; } if (!k) continue; kullanilan.add(k.id); e.yontem = yontem; e.openalex = { id: k.id, ror: k.ror, display_name: k.display_name, country_code: k.country_code, homepage_url: k.homepage_url, image_url: k.image_url, works_count: k.works_count, cited_by_count: k.cited_by_count, h_index: k.summary_stats?.h_index ?? 0, i10_index: k.summary_stats?.i10_index ?? 0, iki_yil_ort_atif: k.summary_stats?.["2yr_mean_citedness"] ?? 0, yillik: (k.counts_by_year ?? []) .filter((y) => y.year >= 2021) .sort((a, b) => a.year - b.year), updated_date: k.updated_date, }; } // 3b) İkinci geçiş: OpenAlex'te country_code=null kalan (çoğu 2018 sonrası // kurulan) üniversiteler TR filtresine düşmez; ad ile arama yapılır. Aday // yalnızca slug/gevşek anahtarı birebir tutuyorsa kabul edilir. const adayKabul = (e: Eslesme, k: Kurum): boolean => { if (kullanilan.has(k.id) || k.type !== "education") return false; const hedefSlug = e.slug; const hedefGevsek = gevsekAnahtar(e.siteAd); return kurumAdlari(k).some((ad) => { const c = ingilizceyiTurkceye(ad); return ( siteSlug(ad) === hedefSlug || siteSlug(c) === hedefSlug || gevsekAnahtar(ad) === hedefGevsek || gevsekAnahtar(c) === hedefGevsek ); }); }; for (const e of site.values()) { if (e.openalex) continue; const aranan = e.siteAd .replace(/\s*(ÜNİVERSİTESİ|MESLEK YÜKSEKOKULU)\s*$/u, "") .trim(); if (!aranan) continue; const url = new URL("https://api.openalex.org/institutions"); url.searchParams.set("search", aranan); url.searchParams.set("filter", "type:education"); url.searchParams.set("per-page", "5"); url.searchParams.set("select", SELECT); if (MAILTO) url.searchParams.set("mailto", MAILTO); const res = await fetch(url, { headers: { "User-Agent": "kolaytercih-kesif/0.1" } }); if (!res.ok) { console.warn(` arama HTTP ${res.status}: ${aranan}`); continue; } const j = (await res.json()) as { results: Kurum[] }; const k = j.results.find((aday) => adayKabul(e, aday)); if (!k) continue; kullanilan.add(k.id); e.yontem = "arama"; e.openalex = { id: k.id, ror: k.ror, display_name: k.display_name, country_code: k.country_code, homepage_url: k.homepage_url, image_url: k.image_url, works_count: k.works_count, cited_by_count: k.cited_by_count, h_index: k.summary_stats?.h_index ?? 0, i10_index: k.summary_stats?.i10_index ?? 0, iki_yil_ort_atif: k.summary_stats?.["2yr_mean_citedness"] ?? 0, yillik: (k.counts_by_year ?? []) .filter((y) => y.year >= 2021) .sort((a, b) => a.year - b.year), updated_date: k.updated_date, }; } // 4) Özet const liste = [...site.values()]; const esl = liste.filter((e) => e.openalex); const say = (y: Eslesme["yontem"]) => liste.filter((e) => e.yontem === y).length; const turBazinda = (tur: string) => { const t = liste.filter((e) => (e.unitur ?? "").startsWith(tur)); return `${t.filter((e) => e.openalex).length}/${t.length}`; }; console.log( `\nEşleşme: ${esl.length}/${liste.length} (%${((100 * esl.length) / liste.length).toFixed(1)})` + ` — slug ${say("slug")}, gevşek ${say("gevsek")}, çeviri ${say("ceviri")}, arama ${say("arama")}, yok ${say("yok")}`, ); console.log( ` DEVLET ${turBazinda("DEVLET")} · VAKIF ${turBazinda("VAKIF")} · KKTC ${turBazinda("KKTC")} · YURTDIŞI ${turBazinda("YURTDISI")}`, ); const gevsekler = liste.filter((e) => e.yontem !== "slug" && e.openalex); console.log(`\nBirebir slug dışı eşleşmeler — göz kontrolü (${gevsekler.length}):`); for (const e of gevsekler) { console.log(` - [${e.yontem}] ${e.siteAd} → ${e.openalex!.display_name} (${e.openalex!.homepage_url ?? "-"})`); } const eslesmeyen = liste.filter((e) => !e.openalex); console.log(`\nEşleşmeyen (${eslesmeyen.length}):`); for (const e of eslesmeyen) { console.log(` - ${e.siteAd} [${e.unitur ?? "?"}, ${e.programSayisi} program]`); } // OpenAlex'te olup sitede karşılığı bulunmayan TR kurumları (ipucu) const bosTr = tr .filter((k) => !kullanilan.has(k.id) && k.works_count >= 500) .sort((a, b) => b.works_count - a.works_count) .slice(0, 25); console.log(`\nOpenAlex TR'de eşleşmeden kalan, ≥500 yayınlı ilk ${bosTr.length}:`); for (const k of bosTr) { console.log(` - ${k.display_name} (${k.works_count} yayın) alt: ${k.display_name_alternatives?.slice(0, 3).join(" | ")}`); } // 5) Örnek 10 üniversite tablosu (markdown) const ornekSluglar = [ "bogazici-universitesi", "orta-dogu-teknik-universitesi", "hacettepe-universitesi", "istanbul-teknik-universitesi", "koc-universitesi", "ege-universitesi", "ataturk-universitesi", "bahcesehir-universitesi", "mus-alparslan-universitesi", "dogu-akdeniz-universitesi", ]; const fmt = (n: number) => n.toLocaleString("tr-TR"); console.log("\n| Üniversite | Eşleşme | Toplam yayın | Toplam atıf | Yayın 2024 | Yayın 2025 | Atıf 2025 | h-index | 2y ort. atıf |"); console.log("|---|---|---:|---:|---:|---:|---:|---:|---:|"); for (const slug of ornekSluglar) { const e = site.get(slug); if (!e) { console.log(`| ${slug} | (sitede yok) | | | | | | | |`); continue; } const o = e.openalex; if (!o) { console.log(`| ${e.siteAd} | yok | — | — | — | — | — | — | — |`); continue; } const y = (yil: number) => o.yillik.find((s) => s.year === yil); console.log( `| ${e.siteAd} | ${e.yontem} → ${o.display_name} | ${fmt(o.works_count)} | ${fmt(o.cited_by_count)} | ${fmt(y(2024)?.works_count ?? 0)} | ${fmt(y(2025)?.works_count ?? 0)} | ${fmt(y(2025)?.cited_by_count ?? 0)} | ${o.h_index} | ${o.iki_yil_ort_atif.toFixed(2)} |`, ); } // 6) JSON çıktı writeFileSync( CIKTI, JSON.stringify( { uretim: new Date().toISOString(), kaynak: "https://api.openalex.org/institutions?filter=country_code:TR|CY,type:education", ozet: { site: liste.length, openalexTr: tr.length, openalexCy: cy.length, eslesen: esl.length, yontem: { slug: say("slug"), gevsek: say("gevsek"), ceviri: say("ceviri"), arama: say("arama"), yok: say("yok") }, }, universiteler: liste.sort((a, b) => a.slug.localeCompare(b.slug)), }, null, 2, ), ); console.log(`\nJSON: ${CIKTI}`); } main().catch((e) => { console.error(e); process.exit(1); });