如何开发Node.js API实现用户获取匹配职业标签的职位?
问题描述
我正在开发一款求职应用,雇主可创建职位发布,用户可申请职位。希望实现:用户能获取职位描述中包含与其User模型内professionalTags字段关键词相似的职位。
User模型代码
const mongoose = require('mongoose'); const validator = require('validator'); const bcrypt = require('bcryptjs') const jwt = require('jsonwebtoken'); const crypto = require('crypto'); const userSchema = new mongoose.Schema({ username : { type : String, required : [true, 'Please enter username'], maxlength: [30, 'Your name cannot exceed 30 characters'] }, email : { type : String, required : [true, 'Please enter your email address'], unique : true, validate : [validator.isEmail, 'Please enter valid email address'] }, professionalTags: { type: String, required: [false, 'Please enter your custom words'], }, phoneNo: { type: String, required: false }, role : { type : String, enum : { values : ['user', 'employer', 'admin'], message : 'Please select correct role' }, default : 'user' }, password : { type : String, required : [true, 'Please enter password for your account'], minlength : [4, 'Your password must be at least 4 characters long'], select : false }, createdAt : { type : Date, default : Date.now }, resetPasswordToken : String, resetPasswordExpire : Date }); // Encrypting password before saving user userSchema.pre('save', async function (next) { if(!this.isModified('password')) { next() } this.password = await bcrypt.hash(this.password, 11) }) // Compare user password userSchema.methods.comparePassword = async function (enteredPassword) { return await bcrypt.compare(enteredPassword, this.password) } // Return JWT token userSchema.methods.getJwtToken = function () { return jwt.sign({ id: this._id }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_TIME }); } // Generate password reset token userSchema.methods.getResetPasswordToken = function () { // Generate token const resetToken = crypto.randomBytes(20).toString('hex'); // Hash and set to resetPasswordToken this.resetPasswordToken = crypto.createHash('sha256').update(resetToken).digest('hex') //set token expire time this.resetPasswordExpire = Date.now() + 30 * 60 * 1000 return resetToken } module.exports = mongoose.model('User', userSchema);
Job模型代码
const mongoose = require('mongoose') const jobSchema = new mongoose.Schema({ description: { type: String, required: [true, 'Please describe what you want'], }, images: [ { public_id: { type: String, required: true, }, url: { type: String, required: true, }, } ], numOfjobReactions: { type: Number, default: 0 }, jobReactions: [ { user: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'User' }, username: { type: String, required: true, }, comment: { type: String, required: true } } ], user: { type: mongoose.Schema.ObjectId, ref: 'User', required: true }, createdAt: { type: Date, default: Date.now } }) module.exports = mongoose.model('Job', jobSchema);
示例场景:当用户professionalTags包含Medicine、injection、surgery、hospital时,需获取描述为Hello, we are looking for a professional doctor who has a master's degree in Medicine and surgery的职位。
解决方案
1. 优化User模型的professionalTags字段
当前professionalTags是字符串类型,后续拆分关键词会很繁琐,建议改成字符串数组:
// 修改User模型中的professionalTags字段 professionalTags: { type: [String], required: false, default: [] },
如果已有存量数据,可执行一次迁移脚本将原字符串按逗号/空格拆分转成数组:
// 数据迁移示例(仅需执行一次) const User = require('./models/User'); async function migrateTags() { const users = await User.find({ professionalTags: { $type: 'string' } }); for (const user of users) { // 按逗号或空格拆分,过滤空字符串 const tags = user.professionalTags.split(/[, ]+/).filter(tag => tag.trim()); user.professionalTags = tags; await user.save(); } console.log('标签迁移完成'); } migrateTags().catch(err => console.error(err));
2. 为Job模型创建文本索引
要高效实现关键词相似匹配,给Job的description字段建立全文索引:
// 在Job模型schema定义后添加 jobSchema.index({ description: 'text' });
MongoDB的全文索引会自动处理大小写、词形变化,还支持部分匹配,适合快速检索相似内容。
3. 编写匹配职位的API逻辑
假设基于Express框架,API核心逻辑如下(需结合JWT验证获取当前用户ID):
const User = require('../models/User'); const Job = require('../models/Job'); // 获取匹配用户职业标签的职位 exports.getMatchingJobs = async (req, res) => { try { // 获取当前用户,仅返回professionalTags字段 const user = await User.findById(req.user.id).select('professionalTags'); if (!user || user.professionalTags.length === 0) { return res.status(200).json({ success: true, jobs: [] }); } // 用用户标签构建全文检索查询 const query = { $text: { $search: user.professionalTags.join(' ') } }; // 查询匹配职位,关联雇主信息并按创建时间倒序 const jobs = await Job.find(query) .populate('user', 'username email') .sort({ createdAt: -1 }); res.status(200).json({ success: true, count: jobs.length, jobs }); } catch (error) { res.status(500).json({ success: false, message: '获取匹配职位失败', error: error.message }); } };
4. 进阶:精准相似匹配
如果需要更细致的匹配(比如同义词、词干匹配),可选择以下方案:
方案A:正则匹配(精确关键词)
适合需要严格匹配关键词的场景,忽略大小写:
// 为每个标签生成不区分大小写的正则 const regexPatterns = user.professionalTags.map(tag => new RegExp(tag, 'i')); const jobs = await Job.find({ description: { $in: regexPatterns } }) .populate('user', 'username email') .sort({ createdAt: -1 });
方案B:自然语言处理优化
引入natural库做词干提取、同义词匹配,提升相似性识别能力:
npm install natural
const natural = require('natural'); const stemmer = natural.PorterStemmer; exports.getMatchingJobs = async (req, res) => { try { const user = await User.findById(req.user.id).select('professionalTags'); if (!user || user.professionalTags.length === 0) { return res.status(200).json({ success: true, jobs: [] }); } // 将用户标签转为词干(提取词根,统一处理词形变化) const processedTags = user.professionalTags.map(tag => stemmer.stem(tag.toLowerCase()) ); // 先通过全文索引缩小范围,再做精细化过滤 const candidateJobs = await Job.find({ $text: { $search: user.professionalTags.join(' ') } }).populate('user', 'username email'); // 过滤出真正匹配词干的职位 const matchingJobs = candidateJobs.filter(job => { // 将职位描述拆分为单词并提取词干 const descWords = job.description.toLowerCase().split(/\W+/).filter(word => word); const processedDesc = descWords.map(word => stemmer.stem(word)); // 只要有一个标签词干匹配就保留 return processedTags.some(tag => processedDesc.includes(tag)); }); res.status(200).json({ success: true, count: matchingJobs.length, jobs: matchingJobs.sort((a, b) => b.createdAt - a.createdAt) }); } catch (error) { res.status(500).json({ success: false, message: '获取匹配职位失败', error: error.message }); } };
内容的提问来源于stack exchange,提问作者pierre
相关产品推荐
相关产品推荐

