You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何开发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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 09:44:53