/** * 关键词匹配工具 * 提供职位描述的关键词匹配和评分功能 */ class KeywordMatcher { /** * 检查是否包含排除关键词 * @param {string} text - 待检查的文本 * @param {string[]} excludeKeywords - 排除关键词列表 * @returns {{matched: boolean, keywords: string[]}} 匹配结果 */ static matchExcludeKeywords(text, excludeKeywords = []) { if (!text || !excludeKeywords || excludeKeywords.length === 0) { return { matched: false, keywords: [] }; } const matched = []; const lowerText = text.toLowerCase(); for (const keyword of excludeKeywords) { if (!keyword || !keyword.trim()) continue; const lowerKeyword = keyword.toLowerCase().trim(); if (lowerText.includes(lowerKeyword)) { matched.push(keyword); } } return { matched: matched.length > 0, keywords: matched }; } /** * 检查是否包含过滤关键词(必须匹配) * @param {string} text - 待检查的文本 * @param {string[]} filterKeywords - 过滤关键词列表 * @returns {{matched: boolean, keywords: string[], matchCount: number}} 匹配结果 */ static matchFilterKeywords(text, filterKeywords = []) { if (!text) { return { matched: false, keywords: [], matchCount: 0 }; } if (!filterKeywords || filterKeywords.length === 0) { return { matched: true, keywords: [], matchCount: 0 }; } const matched = []; const lowerText = text.toLowerCase(); for (const keyword of filterKeywords) { if (!keyword || !keyword.trim()) continue; const lowerKeyword = keyword.toLowerCase().trim(); if (lowerText.includes(lowerKeyword)) { matched.push(keyword); } } // 只要匹配到至少一个过滤关键词即可通过 return { matched: matched.length > 0, keywords: matched, matchCount: matched.length }; } /** * 计算关键词匹配奖励分数 * @param {string} text - 待检查的文本 * @param {string[]} keywords - 关键词列表 * @param {object} options - 选项 * @returns {{score: number, matchedKeywords: string[], matchCount: number}} */ static calculateBonus(text, keywords = [], options = {}) { const { baseScore = 10, // 每个关键词的基础分 maxBonus = 50, // 最大奖励分 caseSensitive = false // 是否区分大小写 } = options; if (!text || !keywords || keywords.length === 0) { return { score: 0, matchedKeywords: [], matchCount: 0 }; } const matched = []; const searchText = caseSensitive ? text : text.toLowerCase(); for (const keyword of keywords) { if (!keyword || !keyword.trim()) continue; const searchKeyword = caseSensitive ? keyword.trim() : keyword.toLowerCase().trim(); if (searchText.includes(searchKeyword)) { matched.push(keyword); } } const score = Math.min(matched.length * baseScore, maxBonus); return { score, matchedKeywords: matched, matchCount: matched.length }; } /** * 高亮匹配的关键词(用于展示) * @param {string} text - 原始文本 * @param {string[]} keywords - 关键词列表 * @param {string} prefix - 前缀标记(默认 ) * @param {string} suffix - 后缀标记(默认 ) * @returns {string} 高亮后的文本 */ static highlight(text, keywords = [], prefix = '', suffix = '') { if (!text || !keywords || keywords.length === 0) { return text; } let result = text; for (const keyword of keywords) { if (!keyword || !keyword.trim()) continue; const regex = new RegExp(`(${this.escapeRegex(keyword.trim())})`, 'gi'); result = result.replace(regex, `${prefix}$1${suffix}`); } return result; } /** * 转义正则表达式特殊字符 * @param {string} str - 待转义的字符串 * @returns {string} 转义后的字符串 */ static escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * 综合匹配(排除 + 过滤 + 奖励) * @param {string} text - 待检查的文本 * @param {object} config - 配置 * @param {string[]} config.excludeKeywords - 排除关键词 * @param {string[]} config.filterKeywords - 过滤关键词 * @param {string[]} config.bonusKeywords - 奖励关键词 * @returns {{pass: boolean, reason?: string, score: number, details: object}} */ static match(text, config = {}) { const { excludeKeywords = [], filterKeywords = [], bonusKeywords = [] } = config; // 1. 检查排除关键词 const excludeResult = this.matchExcludeKeywords(text, excludeKeywords); if (excludeResult.matched) { return { pass: false, reason: `包含排除关键词: ${excludeResult.keywords.join(', ')}`, score: 0, details: { exclude: excludeResult } }; } // 2. 检查过滤关键词(必须匹配) const filterResult = this.matchFilterKeywords(text, filterKeywords); if (filterKeywords.length > 0 && !filterResult.matched) { return { pass: false, reason: '不包含任何必需关键词', score: 0, details: { filter: filterResult } }; } // 3. 计算奖励分数 const bonusResult = this.calculateBonus(text, bonusKeywords); return { pass: true, score: bonusResult.score, details: { exclude: excludeResult, filter: filterResult, bonus: bonusResult } }; } /** * 批量匹配职位列表 * @param {Array} jobs - 职位列表 * @param {object} config - 匹配配置 * @param {Function} textExtractor - 文本提取函数 (job) => string * @returns {Array} 匹配通过的职位(带匹配信息) */ static filterJobs(jobs, config, textExtractor = (job) => `${job.name || ''} ${job.description || ''}`) { if (!jobs || jobs.length === 0) { return []; } const filtered = []; for (const job of jobs) { const text = textExtractor(job); const matchResult = this.match(text, config); if (matchResult.pass) { filtered.push({ ...job, _matchInfo: matchResult }); } } return filtered; } } module.exports = KeywordMatcher;