Files
ICON-CRM/server/src/scanner/matching.js
T
schakal00783andClaude Opus 5.5 b34a1968df Scanner: 0 Treffer durch Anführungszeichen in Schlagwörtern behoben
- Anführungszeichen um Begriffe beim Eingeben, Speichern und Lesen
  entfernen; der Agent vom 29.09. suchte nach "Interim" samt Zeichen
- Umlaute in Such-URLs umschreiben, karriere.at-404 = keine Treffer
- Filterleiste mit festen Breiten, bricht um statt überzulaufen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:19:16 +02:00

104 lines
3.6 KiB
JavaScript

/*
* Bewertung einer Anzeige gegen einen Suchagenten - ohne KI.
*
* Wunsch vom 29.09.2026: kein Abgleich mit Lebenslaeufen, sondern "je mehr
* Schlagwoerter gefunden werden, desto besser der Treffer". Gezaehlt wird in
* Titel und Text; ein Schlagwort im Titel zaehlt doppelt, weil der Titel die
* Rolle benennt und der Text oft nur Umfeld beschreibt.
*
* score = Summe(Gewicht je gefundenem Schlagwort) / (2 * Anzahl Schlagwoerter)
* Gewicht 2 = im Titel, 1 = nur im Text
*/
/*
* Anfuehrungszeichen um Begriffe entfernen: wer "Interim", "Vakanz" eingibt,
* meint die Woerter, nicht die Zeichen. Mit Zeichen fand die Bewertung am
* 29.09.2026 in 178 Anzeigen keinen einzigen Treffer.
*/
export const stripQuotes = (text) =>
String(text ?? '')
.trim()
.replace(/^["'„“”‚‘’«»]+|["'„“”‚‘’«»]+$/g, '')
.trim();
const normalize = (text) => String(text ?? '').normalize('NFC').toLowerCase();
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
/**
* Kurze Begriffe ("IT", "SAP", "HR") nur als ganzes Wort - sonst steckt "it" in
* jedem "Mitarbeiter". Laengere als Teilwort, damit "Projektleiter" auch in
* "IT-Projektleiterin" und "Projektleiterstelle" trifft.
*/
export function containsTerm(text, term) {
const needle = normalize(term).trim();
if (!needle) return false;
const haystack = normalize(text);
if (needle.length <= 3) {
return new RegExp(`(?<![\\p{L}\\p{N}])${escape(needle)}(?![\\p{L}\\p{N}])`, 'u').test(haystack);
}
return haystack.includes(needle);
}
/**
* @returns {{ score: number, hits: string[] } | null} null = ausgeschlossen
* oder kein einziges Schlagwort gefunden
*/
export function scoreJob(agent, job) {
const title = job.title ?? '';
const all = `${title}\n${job.company ?? ''}\n${job.description ?? ''}`;
if ((agent.excludes ?? []).some((term) => containsTerm(all, term))) return null;
const keywords = agent.keywords ?? [];
if (keywords.length === 0) return null;
let points = 0;
const hits = [];
for (const keyword of keywords) {
if (containsTerm(title, keyword)) {
points += 2;
hits.push(keyword);
} else if (containsTerm(all, keyword)) {
points += 1;
hits.push(keyword);
}
}
if (hits.length === 0) return null;
return { score: Math.round((100 * points) / (2 * keywords.length)), hits };
}
/*
* Firmennamen der Boersen und des CRM auf einen Nenner bringen:
* "Müller Holding GmbH & Co. KG" -> "müller holding".
*/
const LEGAL_FORMS =
/\b(gmbh|gesmbh|mbh|ag|se|kg|kgaa|ohg|gbr|ug|e\.?\s?v|e\.?\s?k|co|inc|ltd|llc|plc|sa|sarl|bv|nv|haftungsbeschränkt|gesellschaft mit beschränkter haftung|aktiengesellschaft)\b\.?/g;
export function normalizeCompany(name) {
return normalize(name)
.replace(/\(.*?\)/g, ' ')
.replace(/&/g, ' ')
.replace(LEGAL_FORMS, ' ')
.replace(/[^\p{L}\p{N}]+/gu, ' ')
.replace(/\s+/g, ' ')
.trim();
}
/**
* Sucht das Unternehmen im CRM. Gleicher Name gewinnt; sonst darf der Name der
* Anzeige mit dem CRM-Namen beginnen ("Siemens" -> "Siemens Mobility"), aber
* nur ab fuenf Zeichen, sonst wird aus "ABC" jeder, der mit "ABC" anfaengt.
*
* @param {Array<{id:number, key:string}>} companies vorberechnet, laengste zuerst
*/
export function findCompany(companies, name) {
const key = normalizeCompany(name);
if (!key) return null;
const exact = companies.find((company) => company.key === key);
if (exact) return exact.id;
const prefix = companies.find((company) => company.key.length >= 5 && key.startsWith(`${company.key} `));
return prefix ? prefix.id : null;
}