fix: doküman sınıflandırması ilanın tamamını tarayıp yanlış sonuç üretiyordu — yalnızca işin konusu taranıyor, eski kayıtlar geçersiz

This commit is contained in:
2026-08-30 01:52:31 +03:00
parent 495118ef9e
commit 80bb4bc6c4
3 changed files with 49 additions and 6 deletions
+8
View File
@@ -88,6 +88,14 @@ export function openDb({ create = false } = {}) {
} }
const db = new DatabaseSync(DB_PATH); const db = new DatabaseSync(DB_PATH);
db.exec(SCHEMA); db.exec(SCHEMA);
// Geçiş: sektör zenginleştirmesi ilanın TAMAMINI tarıyordu ve standart
// hukuki metin ("e-imza", "numune sunulması") yanlış sınıflandırma üretiyordu.
// v2 yalnızca işin konusunu tarar; eski kayıtlar silinip yeniden hesaplanır.
try {
const silinen = db.prepare("DELETE FROM sektor_zengin WHERE kaynak IS NULL OR kaynak <> 'dokuman-v2'").run();
if (silinen?.changes) console.log(`[history-store] ${silinen.changes} eski sektör kaydı geçersiz kılındı (v2)`);
} catch { /* tablo henüz yoksa sorun değil */ }
// Geçiş: eski şemada okas_kalemler yoksa ekle // Geçiş: eski şemada okas_kalemler yoksa ekle
try { try {
const kolonlar = db.prepare('PRAGMA table_info(sonuclar)').all().map((c) => c.name); const kolonlar = db.prepare('PRAGMA table_info(sonuclar)').all().map((c) => c.name);
+7 -4
View File
@@ -17,7 +17,7 @@ import crypto from 'node:crypto';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { readDtTenders } from './dt-store.mjs'; import { readDtTenders } from './dt-store.mjs';
import { SECTOR_RULES, INSTITUTION_RULES, classify, sektorBul, SEKTOR_ETIKETLERI, KURUM_ETIKETLERI } from './siniflandir.mjs'; import { SECTOR_RULES, INSTITUTION_RULES, classify, sektorBul, SEKTOR_ETIKETLERI, KURUM_ETIKETLERI, ilanKonusu } from './siniflandir.mjs';
import { loginWithGoogle, userFromAuthHeader } from './auth.mjs'; import { loginWithGoogle, userFromAuthHeader } from './auth.mjs';
import { emailConfigured, sendEmail, tenderDigestHtml, emailFrom } from './email.mjs'; import { emailConfigured, sendEmail, tenderDigestHtml, emailFrom } from './email.mjs';
import { aiConfigured, availableProviders, analyzeSpec } from './ai.mjs'; import { aiConfigured, availableProviders, analyzeSpec } from './ai.mjs';
@@ -334,11 +334,14 @@ async function sektorZenginlestir() {
await Promise.all(grup.map(async (t) => { await Promise.all(grup.map(async (t) => {
try { try {
const det = await getDetail(t.id.replace(/^ilan-/, '')); const det = await getDetail(t.id.replace(/^ilan-/, ''));
const sek = sektorBul(t.baslik || '', det.aciklama || ''); // İlanın TAMAMI değil, yalnızca işin konusu taranır: standart
sonuc.push({ id: t.id, sektor: sek }); // hukuki metin ("e-imza ile imzalanarak", "numune sunulması")
// sınıflandırmayı zehirliyordu.
const sek = sektorBul(t.baslik || '', ilanKonusu(det.aciklama || ''));
sonuc.push({ id: t.id, sektor: sek, kaynak: 'dokuman-v2' });
} catch (e) { } catch (e) {
// Metin alınamadıysa 'Genel' yaz — sonsuz yeniden deneme olmasın. // Metin alınamadıysa 'Genel' yaz — sonsuz yeniden deneme olmasın.
sonuc.push({ id: t.id, sektor: 'Genel' }); sonuc.push({ id: t.id, sektor: 'Genel', kaynak: 'dokuman-v2' });
} }
})); }));
// Kaynağı yormamak için gruplar arasında kısa duraklama. // Kaynağı yormamak için gruplar arasında kısa duraklama.
+34 -2
View File
@@ -33,7 +33,7 @@ export function normalizeTr(s) {
// Sıra ÖNEMLİ: ilk eşleşen kazanır, dar kalıplar geniş olanlardan önce gelir // Sıra ÖNEMLİ: ilk eşleşen kazanır, dar kalıplar geniş olanlardan önce gelir
// (ör. "tibbi gaz" → Sağlık, "dogalgaz" → Isıtma). // (ör. "tibbi gaz" → Sağlık, "dogalgaz" → Isıtma).
export const SECTOR_RULES = [ export const SECTOR_RULES = [
[/\btibbi|\bmedikal|\bsaglik|\bhastane|\bilac\b|\bpoliklinik|\bcerrahi|\bameliyat|\bdiyaliz|\bradyoloji|\brontgen|\btomografi|\bultrason|\bsteril|\benjektor|\beldiven|\bmaske|\bpansuman|\bped\b|\bsonda|\bkateter|\bprotez|\bortez|\bisitme cihaz|\bdis unit|\bendo motor|\breaktif|\bhemogram|\bserum|\basi alimi|\basilama|\bsedye|\bligasyon|\btest kiti|\bkit alimi|\bmanometre|\boksijen regulator|\bmedikal gaz|\bisitme engelli|\binduksiyon dongu|\bhasta yatak|\btekerlekli sandalye|\botoklav|\baspirator|\bnebulizator|\bpuls oksimetre/i, 'Sağlık / Tıbbi Malzeme'], [/\btibbi|\bmedikal|\bsaglik|\bhastane|\bilac\b|\bpoliklinik|\bcerrahi|\bameliyat|\bdiyaliz|\bradyoloji|\brontgen|\btomografi|\bultrason|\bsteril|\benjektor|\beldiven|\bmaske|\bpansuman|\bped\b|\bsonda|\bkateter|\bprotez|\bortez|\bisitme cihaz|\bdis unit|\bdental|\bdis hekim|\bimplant|\bendo motor|\breaktif|\bhemogram|\bserum|\basi alimi|\basilama|\bsedye|\bligasyon|\btest kiti|\bkit alimi|\bmanometre|\boksijen regulator|\bmedikal gaz|\bisitme engelli|\binduksiyon dongu|\bhasta yatak|\btekerlekli sandalye|\botoklav|\baspirator|\bnebulizator|\bpuls oksimetre/i, 'Sağlık / Tıbbi Malzeme'],
[/\byazilim|\bbilisim|\bsunucu|\bdonanim|\bnetwork|\bbilgisayar|\blisans|\bveri merkezi|\bsiber|\byazici alim|\btoner alim|\btablet|\bprojeksiyon|\bkart okuyucu|\bnitelikli sertifika|\be-imza sertifika|\bbarkod|\be-imza|\botomasyon sistem/i, 'Bilişim & Yazılım'], [/\byazilim|\bbilisim|\bsunucu|\bdonanim|\bnetwork|\bbilgisayar|\blisans|\bveri merkezi|\bsiber|\byazici alim|\btoner alim|\btablet|\bprojeksiyon|\bkart okuyucu|\bnitelikli sertifika|\be-imza sertifika|\bbarkod|\be-imza|\botomasyon sistem/i, 'Bilişim & Yazılım'],
[/\bguvenlik hizmet|\bozel guvenlik|\bkamera|\bcctv|\balarm|\bturnike|\bx-ray|\bguvenlik sistem|\byangin sondur|\byangin tup|\byangin algila|\bitfaiye/i, 'Güvenlik Sistemleri'], [/\bguvenlik hizmet|\bozel guvenlik|\bkamera|\bcctv|\balarm|\bturnike|\bx-ray|\bguvenlik sistem|\byangin sondur|\byangin tup|\byangin algila|\bitfaiye/i, 'Güvenlik Sistemleri'],
[/\bgida|\byemek|\bcatering|\bekmek|\bet alim|\bkirmizi et|\bbeyaz et|\bsebze|\bmeyve|\bkumanya|\bsut\b|\bpeynir|\btavuk|\bbakliyat|\bmutfak|\bgazoz|\bicecek|\bsu alimi|\bcips|\bkuru gida|\bdana\b|\bkuzu\b|\bfritoz|\breso\b|\bbenmari|\bbulasik makinesi|\bmutfak ekipman|\bkantin/i, 'Gıda & Catering'], [/\bgida|\byemek|\bcatering|\bekmek|\bet alim|\bkirmizi et|\bbeyaz et|\bsebze|\bmeyve|\bkumanya|\bsut\b|\bpeynir|\btavuk|\bbakliyat|\bmutfak|\bgazoz|\bicecek|\bsu alimi|\bcips|\bkuru gida|\bdana\b|\bkuzu\b|\bfritoz|\breso\b|\bbenmari|\bbulasik makinesi|\bmutfak ekipman|\bkantin/i, 'Gıda & Catering'],
@@ -50,7 +50,7 @@ export const SECTOR_RULES = [
[/\bdogalgaz|\bkalorifer|\bkazan\b|\bisitma|\bsogutma|\bklima|\bhavalandirma|\biklimlendirme|\bmekanik tesisat|\bradyator|\bbuzdolabi|\bsogutucu/i, 'Isıtma & İklimlendirme'], [/\bdogalgaz|\bkalorifer|\bkazan\b|\bisitma|\bsogutma|\bklima|\bhavalandirma|\biklimlendirme|\bmekanik tesisat|\bradyator|\bbuzdolabi|\bsogutucu/i, 'Isıtma & İklimlendirme'],
[/\basansor|\byuruyen merdiven|\bplatform lift/i, 'Asansör & Yürüyen Sistem'], [/\basansor|\byuruyen merdiven|\bplatform lift/i, 'Asansör & Yürüyen Sistem'],
[/\bsu aritma|\baritma tesis|\bpompa|\bsondaj|\bkuyu\b|\bsu deposu|\bklorlama|\bdalgic|\bboru\b|\bvana\b/i, 'Su & Arıtma'], [/\bsu aritma|\baritma tesis|\bpompa|\bsondaj|\bkuyu\b|\bsu deposu|\bklorlama|\bdalgic|\bboru\b|\bvana\b/i, 'Su & Arıtma'],
[/\btarim|\btohum|\bgubre|\bfide|\bzirai|\bhayvan|\byem\b|\bveteriner|\bsera\b|\bilaclama zirai|\borman|\bagaclandirma/i, 'Tarım & Hayvancılık'], [/\btarim|\btohum|\bgubre|\bfide|\bzirai|\bhayvan|\byem\b|\bveteriner|\bmama\b|\bkedi\b|\bkopek\b|\bevcil|\bsera\b|\bilaclama zirai|\borman|\bagaclandirma/i, 'Tarım & Hayvancılık'],
[/\bpeyzaj|\bpark bahce|\bagac\b|\bcim\b|\bfidan|\boyun grubu|\bkent mobilya|\bbank alim|\bcicek/i, 'Peyzaj & Park'], [/\bpeyzaj|\bpark bahce|\bagac\b|\bcim\b|\bfidan|\boyun grubu|\bkent mobilya|\bbank alim|\bcicek/i, 'Peyzaj & Park'],
[/\begitim hizmet|\bkurs\b|\bseminer|\bdanismanlik|\bmusavirlik|\betut proje|\bmuhendislik hizmet|\bkontrolluk|\barastirma hizmet|\bsaha arastirma|\banket/i, 'Danışmanlık & Proje'], [/\begitim hizmet|\bkurs\b|\bseminer|\bdanismanlik|\bmusavirlik|\betut proje|\bmuhendislik hizmet|\bkontrolluk|\barastirma hizmet|\bsaha arastirma|\banket/i, 'Danışmanlık & Proje'],
[/\breklam|\btanitim|\borganizasyon|\bfuar\b|\bbasin|\bmedya|\bproduksiyon|\bafis|\bbrosur|\bdavetiye|\bpromosyon|\bporselen kupa|\bhediyelik|\bbayrak alim|\bflama/i, 'Reklam & Organizasyon'], [/\breklam|\btanitim|\borganizasyon|\bfuar\b|\bbasin|\bmedya|\bproduksiyon|\bafis|\bbrosur|\bdavetiye|\bpromosyon|\bporselen kupa|\bhediyelik|\bbayrak alim|\bflama/i, 'Reklam & Organizasyon'],
@@ -74,6 +74,38 @@ export const INSTITUTION_RULES = [
[/a\.s\.|\bgenel mudurlugu|\bisletme/i, 'Kamu Şirketleri'], [/a\.s\.|\bgenel mudurlugu|\bisletme/i, 'Kamu Şirketleri'],
]; ];
/**
* İlan metninden YALNIZCA işin konusunu ayıklar.
*
* İlanın tamamı standart hukuki metinle doludur ("e-imza ile imzalanarak",
* "numune sunulması istenmektedir", "…Sağlık Bakanlığı", "lisans belgesi") ve
* bunlar sınıflandırmayı zehirler: "Kedi ve köpek maması" ilanı e-imza
* cümlesi yüzünden Bilişim çıkar. Bu yüzden yalnızca iki güvenilir bölgeyi
* okuruz:
* 1) Başlangıç → "4734 sayılı" (ilanın konu cümlesi)
* 2) "Niteliği, türü ve miktarı" alanının değeri
*/
export function ilanKonusu(metin) {
if (!metin) return '';
// Arama da dönüş de NORMALLEŞTİRİLMİŞ metin üzerinde: "4734 sayılı" gibi
// işaretleri Türkçe karakterden bağımsız bulabilelim.
const t = normalizeTr(
String(metin).replace(/\{[^}]*\}/g, ' ').replace(/&nbsp;/g, ' ')
);
const parcalar = [];
// 1) Konu cümlesi: metnin başından "4734 sayılı"ya kadar
const kanun = t.search(/4734\s*sayili|sayili kamu ihale kanunu/i);
parcalar.push(kanun > 0 ? t.slice(0, kanun) : t.slice(0, 300));
// 2) "Niteliği, türü ve miktarı" alanının değeri (bir sonraki madde numarasına kadar)
const nitelik = t.match(/nitelig[iı][^:]{0,40}:\s*([\s\S]{0,400}?)(?=\d+\.\d|\d+-\s|$)/i);
if (nitelik) parcalar.push(nitelik[1]);
return parcalar.join(' ').slice(0, 800);
}
// --- OKAS (CPV) kod sınıflandırması --------------------------------------- // --- OKAS (CPV) kod sınıflandırması ---------------------------------------
// OKAS, AB'nin CPV numaralandırmasını kullanır ve kodu ihaleyi AÇAN İDARE atar. // OKAS, AB'nin CPV numaralandırmasını kullanır ve kodu ihaleyi AÇAN İDARE atar.
// Yani başlıktan tahmin etmekten çok daha güvenilir bir sinyaldir; kod varsa // Yani başlıktan tahmin etmekten çok daha güvenilir bir sinyaldir; kod varsa