From 80bb4bc6c48360d8ed61cd79b5f4a671a7a2a3d6 Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 30 Aug 2026 01:52:31 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20dok=C3=BCman=20s=C4=B1n=C4=B1fland=C4=B1?= =?UTF-8?q?rmas=C4=B1=20ilan=C4=B1n=20tamam=C4=B1n=C4=B1=20taray=C4=B1p=20?= =?UTF-8?q?yanl=C4=B1=C5=9F=20sonu=C3=A7=20=C3=BCretiyordu=20=E2=80=94=20y?= =?UTF-8?q?aln=C4=B1zca=20i=C5=9Fin=20konusu=20taran=C4=B1yor,=20eski=20ka?= =?UTF-8?q?y=C4=B1tlar=20ge=C3=A7ersiz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/history-store.mjs | 8 ++++++++ server/index.js | 11 +++++++---- server/siniflandir.mjs | 36 ++++++++++++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/server/history-store.mjs b/server/history-store.mjs index fad951e..d3e1ef7 100644 --- a/server/history-store.mjs +++ b/server/history-store.mjs @@ -88,6 +88,14 @@ export function openDb({ create = false } = {}) { } const db = new DatabaseSync(DB_PATH); 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 try { const kolonlar = db.prepare('PRAGMA table_info(sonuclar)').all().map((c) => c.name); diff --git a/server/index.js b/server/index.js index c881b47..63b2e67 100644 --- a/server/index.js +++ b/server/index.js @@ -17,7 +17,7 @@ import crypto from 'node:crypto'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; 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 { emailConfigured, sendEmail, tenderDigestHtml, emailFrom } from './email.mjs'; import { aiConfigured, availableProviders, analyzeSpec } from './ai.mjs'; @@ -334,11 +334,14 @@ async function sektorZenginlestir() { await Promise.all(grup.map(async (t) => { try { const det = await getDetail(t.id.replace(/^ilan-/, '')); - const sek = sektorBul(t.baslik || '', det.aciklama || ''); - sonuc.push({ id: t.id, sektor: sek }); + // İlanın TAMAMI değil, yalnızca işin konusu taranır: standart + // 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) { // 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. diff --git a/server/siniflandir.mjs b/server/siniflandir.mjs index 91cfac0..77a55c6 100644 --- a/server/siniflandir.mjs +++ b/server/siniflandir.mjs @@ -33,7 +33,7 @@ export function normalizeTr(s) { // 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). 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'], [/\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'], @@ -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'], [/\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'], - [/\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'], [/\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'], @@ -74,6 +74,38 @@ export const INSTITUTION_RULES = [ [/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(/ /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, 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