基于NestJS+PostgreSQL,如何在Typesense中高效排除已申请职位
解决方案:Typesense + PostgreSQL 实现求职者未申请职位的精准搜索
针对你遇到的「长UUID列表导致Typesense过滤失效、搜后过滤破坏分页」的问题,这里提供两个可落地的方案,均能在Typesense层面完成过滤,保证分页逻辑正常:
方案一:在职位集合中维护已申请用户ID数组
核心思路是给每个职位文档添加一个数组字段,存储所有申请过该职位的用户UUID,搜索时直接过滤掉包含当前用户ID的职位。
实现步骤
定义Typesense职位集合Schema
给jobs集合添加applied_user_ids数组字段,并设为可过滤(facet: true):const jobSchema = { name: 'jobs', fields: [ { name: 'id', type: 'string' }, { name: 'title', type: 'string' }, { name: 'description', type: 'string' }, { name: 'applied_user_ids', type: 'string[]', facet: true }, { name: 'created_at', type: 'int64' }, ], default_sorting_field: 'created_at' };同步申请记录到Typesense
当用户申请/取消职位时,同步更新对应职位文档的applied_user_ids数组:// 申请职位 async applyJob(userId: string, jobId: string) { // 先写入PostgreSQL申请记录 await this.jobApplicationRepo.create({ userId, jobId }); // 更新Typesense职位文档 const jobDoc = await this.typesenseClient.collections('jobs').documents(jobId).retrieve(); const updatedIds = [...(jobDoc.applied_user_ids || []), userId]; // 去重避免重复添加 const uniqueIds = [...new Set(updatedIds)]; await this.typesenseClient.collections('jobs').documents(jobId).update({ applied_user_ids: uniqueIds }); } // 取消申请 async cancelApplyJob(userId: string, jobId: string) { await this.jobApplicationRepo.delete({ userId, jobId }); const jobDoc = await this.typesenseClient.collections('jobs').documents(jobId).retrieve(); const updatedIds = (jobDoc.applied_user_ids || []).filter(id => id !== userId); await this.typesenseClient.collections('jobs').documents(jobId).update({ applied_user_ids: updatedIds }); }搜索时过滤未申请职位
使用not contains语法过滤掉当前用户已申请的职位:async searchJobs(userId: string, query: string, page: number = 1, perPage: number = 10) { const searchParams = { q: query, filter_by: `applied_user_ids: not contains '${userId}'`, page, per_page: perPage, sort_by: 'created_at:desc' }; return await this.typesenseClient.collections('jobs').documents().search(searchParams); }
优缺点
- ✅ 优点:查询语法简单,性能稳定,Typesense对数组字段的过滤效率高
- ❌ 缺点:职位文档会随申请用户增多而变大,但对于绝大多数招聘场景(单职位申请量不会极端庞大)来说完全可控
方案二:用次级集合存储申请记录,通过跨集合查询过滤
核心思路是创建专门存储申请记录的job_applications集合,搜索时通过子查询排除当前用户已申请的职位ID,类似SQL的左外连接排除逻辑。
实现步骤
创建申请记录集合Schema
确保user_id设为可过滤字段,方便子查询快速定位用户的申请记录:const jobApplicationSchema = { name: 'job_applications', fields: [ { name: 'user_id', type: 'string', facet: true }, { name: 'job_id', type: 'string' }, { name: 'applied_at', type: 'int64' }, ], default_sorting_field: 'applied_at' };同步申请记录到次级集合
用户申请/取消职位时,直接新增/删除job_applications集合的文档:// 申请职位 async applyJob(userId: string, jobId: string) { await this.jobApplicationRepo.create({ userId, jobId }); // 使用upsert避免重复创建 await this.typesenseClient.collections('job_applications').documents().upsert({ user_id: userId, job_id: jobId, applied_at: Date.now() }); } // 取消申请 async cancelApplyJob(userId: string, jobId: string) { await this.jobApplicationRepo.delete({ userId, jobId }); // 通过user_id和job_id定位文档删除 const docs = await this.typesenseClient.collections('job_applications').documents().search({ q: '*', filter_by: `user_id:='${userId}' && job_id:='${jobId}'`, per_page: 1 }); if (docs.hits.length > 0) { await this.typesenseClient.collections('job_applications').documents(docs.hits[0].document.id).delete(); } }搜索时用跨集合子查询过滤
使用not in结合子查询,排除当前用户已申请的职位:async searchJobs(userId: string, query: string, page: number = 1, perPage: number = 10) { const searchParams = { q: query, filter_by: `id: not in (select job_id from job_applications where user_id = '${userId}')`, page, per_page: perPage, sort_by: 'created_at:desc' }; return await this.typesenseClient.collections('jobs').documents().search(searchParams); }
优缺点
- ✅ 优点:职位集合结构干净,申请记录独立管理,无需修改原职位文档结构
- ❌ 缺点:依赖Typesense v0.24+的跨集合查询功能,子查询性能受用户申请数量影响(但单用户申请量通常不大,可忽略)
关键注意事项
- 数据一致性:必须保证PostgreSQL和Typesense的申请记录同步,建议用NestJS的事件监听或事务机制处理,避免出现数据不一致的情况。
- 去重与幂等性:处理申请/取消操作时要做幂等校验,避免重复添加或删除记录。
- 权限控制:搜索接口要确保用户只能查询自己的未申请职位,避免泄露其他用户的申请数据。
内容的提问来源于stack exchange,提问作者Stephen
相关产品推荐
相关产品推荐

