Files
kolaytercih/scripts/kesif-openalex.ts
bilalgursen 0ab4dbe88c
All checks were successful
Deploy / deploy (push) Successful in 7m18s
Enhance Traefik configuration for www.kolaytercih.com
Added a new router and middleware for handling www to apex domain redirection, ensuring that Let's Encrypt certificates are obtained for both domains. Updated the docker-compose.yml file to include necessary rules and middleware for proper redirection and TLS handling.

Also, updated the CEO agent description to remove redundant details and clarified the QA engineer's responsibilities regarding testing critical flows and error reporting. Adjusted the CTO agent's section for consistency in expert guidance and clarified the designer's tools to include 'Skill' for better design resource management.
2026-09-21 20:37:12 +03:00

453 lines
15 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 <dosya.json>] [--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<string, string> };
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<Kurum[]> {
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<string>([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<string, Eslesme>();
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<string, Kurum[]>();
const gevsekDizin = new Map<string, Kurum[]>();
const ceviriDizin = new Map<string, Kurum[]>();
const ekle = (m: Map<string, Kurum[]>, 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<string>();
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);
});