feat: hibrit uyum skoru (sozluksel + anlamsal bonus) ve firma esigi
Anlamsal bonus: profil metni sunucuda gomulur (multilingual-e5-small, ONNX q8, @huggingface/transformers — Python/GPU yok), havuzdaki her ihaleyle kosinus alinir, havuz ici z-skor → +0..20 puan. Sozluksel skor ana eksen kalir; bonus eklenir. Sozluksel 0 iken bonus yalniz ≥10'sa gorunur. NEDEN BONUS, ESIK DEGIL (olculdu): e5 kosinusleri 0,79-0,86 bandina sikisiyor (havuz ort 0,814 sd 0,026); kirtasiye ile rontgen arasi 0,06. kaanvp/kobi-ihale-eslestirme'nin 0,65 esigiyle trafo bakimi da "eslesti" sayilirdi. Ama siralama dogru ve sozlukselin kacirdigini yakaliyor: "GENEL TIBBI SARF" profilde birebir terim yokken kirtasiyenin ustune cikiyor. - server/uyum-gomme.mjs: acilista arka planda isitma (~45 sn soguk), ihale gommeleri bellekte (metin imzasi degisince yenilenir), model data/modeller volume'unda bir kez indirilir; paket/yukleme yoksa sessizce kapali - POST /api/uyum/gomme, GET /api/health/uyum - Dockerfile: calisma imajina @huggingface/transformers@3 (package.json'a degil — sunucu sifir-bagimlilik, calisma katmani ayri npm i) - Olculen: 3.2 bin ihale ilk cagri 7,6 sn (tumu gomuluyor), ikinci 17 ms; +20 alan 170, ≥10 alan 610 ihale Firma esigi: profilde uyumEsigi (varsayilan 60, kalici); "Uyumlu" hizli filtresi bu esigi kullanir; profil modalinda kaydirici + bonus durumu. Modaldaki "%98+ dogruluk" cumlesi kaldirildi — olculmemis iddiaydi. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -57,9 +57,9 @@ export const acikIhale = (t, bugun) => {
|
||||
export const GECMIS_FILTRELER = new Set(['arsiv', 'sonuclananlar', 'sozlesmeler', 'iptal']);
|
||||
|
||||
// Hızlı filtre (üst kartlar + sol menü) ölçütü. Bilinmeyen id → her şey geçer.
|
||||
export const hizliFiltreGecer = (t, id, { favoriteKurumlar = [], oncekiGirisMs = null } = {}) => {
|
||||
export const hizliFiltreGecer = (t, id, { favoriteKurumlar = [], oncekiGirisMs = null, uyumEsigi = 60 } = {}) => {
|
||||
switch (id) {
|
||||
case 'uyumlu': return (t.aiMatchScore ?? 0) >= 60; // profil terimleri ihalede geçiyor
|
||||
case 'uyumlu': return (t.aiMatchScore ?? 0) >= uyumEsigi; // firma eşiği (profil ayarı)
|
||||
case 'favori_kurumlar': return favoriteKurumlar.includes((t.kurum || '').trim());
|
||||
case 'dogrudan_temin': return t.alimTuru === 'Doğrudan Temin';
|
||||
case 'ihaleler': return t.alimTuru !== 'Doğrudan Temin' && t.durum !== 'Sonuçlandı';
|
||||
|
||||
+27
-3
@@ -75,12 +75,36 @@ export function uyumSkoru(tender, terimler) {
|
||||
return { skor: Math.max(0, Math.min(100, skor)), nedenler: nedenler.slice(0, 5) };
|
||||
}
|
||||
|
||||
/** Havuzun tamamını puanlar; alan adları kartın ve detayın okuduğu adlar. */
|
||||
export function havuzuPuanla(tenders, userProfile) {
|
||||
/** Sunucudaki gömme modeline gidecek profil metni (passage). */
|
||||
export function profilMetni(userProfile) {
|
||||
const c = userProfile?.scrapedCapabilities || {};
|
||||
const parca = [
|
||||
c.ozet,
|
||||
c.extractedSectors?.length ? 'Sektör: ' + c.extractedSectors.join(', ') : '',
|
||||
c.extractedProducts?.length ? 'Ürünler: ' + c.extractedProducts.join(', ') : '',
|
||||
c.ihaleKonulari?.length ? 'İhale konuları: ' + c.ihaleKonulari.join(', ') : '',
|
||||
].filter(Boolean);
|
||||
return parca.join('. ').slice(0, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Havuzun tamamını puanlar; alan adları kartın ve detayın okuduğu adlar.
|
||||
* bonus: sunucudan gelen anlamsal bonus { [id]: 0..20 } — sözlüksel skora
|
||||
* eklenir. Sözlüksel 0 ise bonus tek başına yalnızca ≥10'sa görünür
|
||||
* (havuz ortalamasının belirgin üstü), "anlamsal yakınlık" nedeniyle.
|
||||
*/
|
||||
export function havuzuPuanla(tenders, userProfile, bonus = null) {
|
||||
const terimler = profilTerimleri(userProfile);
|
||||
if (!terimler.length) return tenders.map((t) => (t.aiMatchScore == null && !t.uyumNedenleri ? t : { ...t, aiMatchScore: null, uyumNedenleri: [] }));
|
||||
return tenders.map((t) => {
|
||||
const { skor, nedenler } = uyumSkoru(t, terimler);
|
||||
return { ...t, aiMatchScore: skor, uyumNedenleri: nedenler };
|
||||
const b = bonus?.[t.id] || 0;
|
||||
let toplam = skor;
|
||||
const ned = [...nedenler];
|
||||
if (b > 0 && (skor > 0 || b >= 10)) {
|
||||
toplam = Math.min(100, skor + b);
|
||||
ned.push({ terim: 'anlamsal yakınlık', kaynak: 'gömme', alan: `+${b} puan`, katki: b / 20 });
|
||||
}
|
||||
return { ...t, aiMatchScore: toplam, uyumNedenleri: ned, uyumSozluksel: skor, uyumBonus: b };
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user