feat: doküman metninden sektör zenginleştirme — başlığı sinyalsiz ilanlar ilan metninden sınıflandırılıyor (kalıcı önbellekli)
This commit is contained in:
@@ -50,6 +50,13 @@ CREATE INDEX IF NOT EXISTS idx_idare ON sonuclar(idare_adi);
|
|||||||
CREATE INDEX IF NOT EXISTS idx_il ON sonuclar(il);
|
CREATE INDEX IF NOT EXISTS idx_il ON sonuclar(il);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tarih ON sonuclar(ihale_tarihi);
|
CREATE INDEX IF NOT EXISTS idx_tarih ON sonuclar(ihale_tarihi);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sektor_zengin (
|
||||||
|
ilan_id TEXT PRIMARY KEY, -- ör. ilan-12345
|
||||||
|
sektor TEXT NOT NULL, -- doküman metninden çıkarılan sektör
|
||||||
|
kaynak TEXT, -- 'dokuman' | 'okas'
|
||||||
|
zaman TEXT
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS havuz_olcum (
|
CREATE TABLE IF NOT EXISTS havuz_olcum (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
zaman TEXT NOT NULL,
|
zaman TEXT NOT NULL,
|
||||||
@@ -416,3 +423,80 @@ export function havuzOzet() {
|
|||||||
db.close();
|
db.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Sektör zenginleştirme (doküman metninden) -----------------------------
|
||||||
|
// Başlığı sinyalsiz ihaleler ("MUHTELİF MALZEME ALIMI") ilan metninden
|
||||||
|
// sınıflandırılır. Sonuç kalıcı saklanır: aynı ilan bir daha indirilmez.
|
||||||
|
|
||||||
|
/** id → sektör haritası. Yalnızca istenen id'ler okunur. */
|
||||||
|
export function sektorZenginOku(idler) {
|
||||||
|
const db = openDb();
|
||||||
|
if (!db || !idler?.length) return new Map();
|
||||||
|
try {
|
||||||
|
const harita = new Map();
|
||||||
|
// SQLite parametre sınırına takılmamak için parçalara böl.
|
||||||
|
for (let i = 0; i < idler.length; i += 400) {
|
||||||
|
const parca = idler.slice(i, i + 400);
|
||||||
|
const yer = parca.map(() => '?').join(',');
|
||||||
|
const satirlar = db
|
||||||
|
.prepare(`SELECT ilan_id, sektor FROM sektor_zengin WHERE ilan_id IN (${yer})`)
|
||||||
|
.all(...parca);
|
||||||
|
for (const r of satirlar) harita.set(r.ilan_id, r.sektor);
|
||||||
|
}
|
||||||
|
return harita;
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sektorZenginYaz(kayitlar) {
|
||||||
|
const db = openDb({ create: true });
|
||||||
|
if (!db || !kayitlar?.length) return 0;
|
||||||
|
try {
|
||||||
|
const st = db.prepare(
|
||||||
|
`INSERT INTO sektor_zengin (ilan_id, sektor, kaynak, zaman) VALUES (?,?,?,?)
|
||||||
|
ON CONFLICT(ilan_id) DO UPDATE SET sektor=excluded.sektor, kaynak=excluded.kaynak, zaman=excluded.zaman`
|
||||||
|
);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
let n = 0;
|
||||||
|
for (const k of kayitlar) {
|
||||||
|
if (!k?.id || !k?.sektor) continue;
|
||||||
|
st.run(k.id, k.sektor, k.kaynak || 'dokuman', now);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hangi id'ler daha önce denenmiş? ('Genel' kaydı da denenmiş sayılır) */
|
||||||
|
export function sektorZenginDenenmis(idler) {
|
||||||
|
const db = openDb();
|
||||||
|
if (!db || !idler?.length) return new Set();
|
||||||
|
try {
|
||||||
|
const küme = new Set();
|
||||||
|
for (let i = 0; i < idler.length; i += 400) {
|
||||||
|
const parca = idler.slice(i, i + 400);
|
||||||
|
const yer = parca.map(() => '?').join(',');
|
||||||
|
for (const r of db.prepare(`SELECT ilan_id FROM sektor_zengin WHERE ilan_id IN (${yer})`).all(...parca))
|
||||||
|
küme.add(r.ilan_id);
|
||||||
|
}
|
||||||
|
return küme;
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sektorZenginSayi() {
|
||||||
|
const db = openDb();
|
||||||
|
if (!db) return { toplam: 0, cozulen: 0 };
|
||||||
|
try {
|
||||||
|
const t = db.prepare("SELECT COUNT(*) c FROM sektor_zengin").get().c;
|
||||||
|
const c = db.prepare("SELECT COUNT(*) c FROM sektor_zengin WHERE sektor <> 'Genel'").get().c;
|
||||||
|
return { toplam: t, cozulen: c };
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+80
-1
@@ -25,7 +25,8 @@ import { billingConfigured, webhookConfigured, planCatalog, createCheckoutSessio
|
|||||||
import * as store from './user-store.mjs';
|
import * as store from './user-store.mjs';
|
||||||
import { planOf, haklar, kullanim, kotaKontrol, ozellikVar } from './entitlements.mjs';
|
import { planOf, haklar, kullanim, kotaKontrol, ozellikVar } from './entitlements.mjs';
|
||||||
import { searchTed } from './ted.mjs';
|
import { searchTed } from './ted.mjs';
|
||||||
import { readHistory, historyStats, havuzOlc, havuzGecmisi, havuzOzet } from './history-store.mjs';
|
import { readHistory, historyStats, havuzOlc, havuzGecmisi, havuzOzet,
|
||||||
|
sektorZenginOku, sektorZenginYaz, sektorZenginDenenmis, sektorZenginSayi } from './history-store.mjs';
|
||||||
import { startScheduler, schedulerStatus } from './scheduler.mjs';
|
import { startScheduler, schedulerStatus } from './scheduler.mjs';
|
||||||
import { searchKik, kararMetni } from './kik.mjs';
|
import { searchKik, kararMetni } from './kik.mjs';
|
||||||
import { documentUrl, ilanMetni, documentText, browserAvailable } from './documents.mjs';
|
import { documentUrl, ilanMetni, documentText, browserAvailable } from './documents.mjs';
|
||||||
@@ -299,6 +300,8 @@ async function arkaPlandaTamamla(numFound, ilkListe) {
|
|||||||
}
|
}
|
||||||
console.log(`[ihaleist-api] havuz hazır: ${cache.tenders.length}/${numFound} ilan`);
|
console.log(`[ihaleist-api] havuz hazır: ${cache.tenders.length}/${numFound} ilan`);
|
||||||
havuzKaydet(numFound, cache.tenders.length);
|
havuzKaydet(numFound, cache.tenders.length);
|
||||||
|
zenginUygula(); // önceden çözülmüşleri hemen uygula
|
||||||
|
setTimeout(() => sektorZenginlestir().catch(() => {}), 5000); // kalanları arka planda
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[ihaleist-api] arka plan doldurma hatası:', e.message);
|
console.warn('[ihaleist-api] arka plan doldurma hatası:', e.message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -306,6 +309,67 @@ async function arkaPlandaTamamla(numFound, ilkListe) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Sektör zenginleştirme -------------------------------------------------
|
||||||
|
// Başlığında sektör sinyali olmayan ilanların ("MUHTELİF MALZEME ALIMI")
|
||||||
|
// ilan METNİ indirilip sınıflandırılır. Sonuç kalıcı; aynı ilan bir daha
|
||||||
|
// indirilmez. Kaynağı yormamak için turda sınırlı sayıda ilan işlenir.
|
||||||
|
const ZENGIN_TUR_LIMIT = Number(process.env.SEKTOR_ZENGIN_LIMIT || 300);
|
||||||
|
const ZENGIN_PARALEL = Number(process.env.SEKTOR_ZENGIN_PARALEL || 4);
|
||||||
|
let zenginCalisiyor = false;
|
||||||
|
|
||||||
|
async function sektorZenginlestir() {
|
||||||
|
if (zenginCalisiyor || !cache.tenders?.length) return;
|
||||||
|
zenginCalisiyor = true;
|
||||||
|
try {
|
||||||
|
const adaylar = cache.tenders.filter((t) => t.sektor === 'Genel' && t.isLiveFeed && t.id?.startsWith('ilan-'));
|
||||||
|
if (!adaylar.length) return;
|
||||||
|
const denenmis = sektorZenginDenenmis(adaylar.map((t) => t.id));
|
||||||
|
const kuyruk = adaylar.filter((t) => !denenmis.has(t.id)).slice(0, ZENGIN_TUR_LIMIT);
|
||||||
|
if (!kuyruk.length) return;
|
||||||
|
|
||||||
|
console.log(`[sektor-zengin] ${kuyruk.length} ilan metni sınıflandırılacak (kalan aday ${adaylar.length - denenmis.size})`);
|
||||||
|
const sonuc = [];
|
||||||
|
for (let i = 0; i < kuyruk.length; i += ZENGIN_PARALEL) {
|
||||||
|
const grup = kuyruk.slice(i, i + ZENGIN_PARALEL);
|
||||||
|
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 });
|
||||||
|
} catch (e) {
|
||||||
|
// Metin alınamadıysa 'Genel' yaz — sonsuz yeniden deneme olmasın.
|
||||||
|
sonuc.push({ id: t.id, sektor: 'Genel' });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
// Kaynağı yormamak için gruplar arasında kısa duraklama.
|
||||||
|
if (i + ZENGIN_PARALEL < kuyruk.length) await new Promise((r) => setTimeout(r, 250));
|
||||||
|
}
|
||||||
|
const yazilan = sektorZenginYaz(sonuc);
|
||||||
|
const cozulen = sonuc.filter((r) => r.sektor !== 'Genel').length;
|
||||||
|
console.log(`[sektor-zengin] ${yazilan} kayıt işlendi, ${cozulen} tanesi sınıflandırıldı`);
|
||||||
|
zenginUygula();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[sektor-zengin] hata:', e.message);
|
||||||
|
} finally {
|
||||||
|
zenginCalisiyor = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kayıtlı zenginleştirmeyi havuzdaki 'Genel' ilanlara uygular. */
|
||||||
|
function zenginUygula() {
|
||||||
|
if (!cache.tenders?.length) return 0;
|
||||||
|
const genel = cache.tenders.filter((t) => t.sektor === 'Genel');
|
||||||
|
if (!genel.length) return 0;
|
||||||
|
const harita = sektorZenginOku(genel.map((t) => t.id));
|
||||||
|
let n = 0;
|
||||||
|
for (const t of genel) {
|
||||||
|
const s = harita.get(t.id);
|
||||||
|
if (s && s !== 'Genel') { t.sektor = s; n++; }
|
||||||
|
}
|
||||||
|
if (n) console.log(`[sektor-zengin] ${n} ilan doküman metninden sınıflandırıldı`);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
// Havuz büyüklüğünü kaydeder; ani düşüş / eksik yükleme varsa yöneticiyi uyarır.
|
// Havuz büyüklüğünü kaydeder; ani düşüş / eksik yükleme varsa yöneticiyi uyarır.
|
||||||
let sonHavuzUyarisi = 0;
|
let sonHavuzUyarisi = 0;
|
||||||
function dtSayisiGuvenli() {
|
function dtSayisiGuvenli() {
|
||||||
@@ -939,6 +1003,21 @@ const server = http.createServer(async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (url.pathname === '/api/health') return json(res, 200, { ok: true });
|
if (url.pathname === '/api/health') return json(res, 200, { ok: true });
|
||||||
|
if (url.pathname === '/api/health/sektor') {
|
||||||
|
const say = sektorZenginSayi();
|
||||||
|
const genel = (cache.tenders || []).filter((t) => t.sektor === 'Genel').length;
|
||||||
|
const toplam = (cache.tenders || []).length;
|
||||||
|
return json(res, 200, {
|
||||||
|
success: true,
|
||||||
|
havuz: toplam,
|
||||||
|
genelKalan: genel,
|
||||||
|
genelOran: toplam ? Number((genel / toplam).toFixed(3)) : null,
|
||||||
|
dokumandanDenenen: say.toplam,
|
||||||
|
dokumandanCozulen: say.cozulen,
|
||||||
|
calisiyor: zenginCalisiyor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (url.pathname === '/api/meta/filtreler') {
|
if (url.pathname === '/api/meta/filtreler') {
|
||||||
return json(res, 200, {
|
return json(res, 200, {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user