talent-search-meilisearch

基于 Meilisearch、按可见性收窄的人才搜索

youteacher 的教师档案存放在一个 Meilisearch 索引里(默认索引名 talents,主键 id)。每一次搜索都先经过同一个服务,在触及技能、地点、薪资这些条件之前,先按看的人是谁对结果做门禁。规则很朴素:只有当教师把自己对某一类查看者设为公开时,那类查看者才看得到这份档案。

可见性门禁排在最前面

服务把查看者类型翻译成一条由 Meilisearch 在服务端执行的过滤子句:

  • 雇主(employer) 查看者只看到 publicToSchools = true OR publicToEmployers = true 的档案。
  • 招聘方(recruiter) 查看者只看到 publicToRecruiters = true 的档案。
  • 未设置查看者类型时,不加任何可见性子句。

publicToSchools 是规范字段——在这个业务语境里雇主即学校——而 publicToEmployers 作为别名保留,使得携带任一字段的文档都能解析。因为这是索引上的过滤(不是取回后再裁剪),被隐藏的档案根本不会进入结果集、也不进入总数统计。单份档案的详情路径(GetTalentDetail)会重跑同一套检查:先取回文档,若查看者对应的可见性标记存在且为 false,就以 403 access_denied 拒绝(原因 profile_not_public_to_employers / _recruiters);档案不存在则返回 404 not_found。标记缺失时默认可见。

分面过滤

在可见性之上,调用方可以按分面收窄。每个分面构造成各自的一条子句,所有子句之间用 AND 连接;同一个多值分面内部的取值之间用 OR 连接:

  • 技能 —— 每个取值一条 skills = "…"
  • 可用状态 —— 每个取值一条 availability = "…"
  • 地点 —— 每个取值同时匹配 locationCitylocationCountry
  • 经验 —— experienceYears >= 下限 和/或 <= 上限。
  • 薪资 —— salaryExpectation <= 一个上限。

这些字段在索引设置里连同可见性标记一并声明为 filterableAttributes。全文查询则在 searchableAttributes(显示名、标题、简介、技能、语言、城市、国家)上、按 Meilisearch 默认排序规则运行。

排序、分页、映射

排序要么是 recentupdatedAt:desc),要么是 relevance——后者即 Meilisearch 的默认行为,不额外加排序。updatedAtexperienceYears 是仅有的两个 sortableAttributes。分页基于 offset:offset = (page - 1) * limit,默认第 1 页、每页 20 条。响应用 Meilisearch 的 estimatedTotalHits 作为 total,并由 ceil(total / limit) 推出 totalPages

每条命中只取回一份固定的属性清单,并映射成一个稳定的结果结构,缺失字段用 null / 空数组兜底,下游代码无需再逐一防御。搜索处理层在服务之外包了一层可选缓存,缓存键是对过滤条件、页码、每页数量做 SHA-256 后取值。索引写入(upsertdelete)是即发即忘——不等 Meilisearch 索引完成。

本节内容

  • 查询编排:application/search/queries/SearchTalents.tsGetTalentDetail.ts
  • 端口与结构:application/search/TalentSearchService.ts
  • Meilisearch 适配器:infrastructure/search/MeilisearchTalentSearchService.tsensureIndexSettings.ts
about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →