基于 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 = "…"。 - 地点 —— 每个取值同时匹配
locationCity与locationCountry。 - 经验 ——
experienceYears >=下限 和/或<=上限。 - 薪资 ——
salaryExpectation <=一个上限。
这些字段在索引设置里连同可见性标记一并声明为 filterableAttributes。全文查询则在 searchableAttributes(显示名、标题、简介、技能、语言、城市、国家)上、按 Meilisearch 默认排序规则运行。
排序、分页、映射
排序要么是 recent(updatedAt:desc),要么是 relevance——后者即 Meilisearch 的默认行为,不额外加排序。updatedAt 与 experienceYears 是仅有的两个 sortableAttributes。分页基于 offset:offset = (page - 1) * limit,默认第 1 页、每页 20 条。响应用 Meilisearch 的 estimatedTotalHits 作为 total,并由 ceil(total / limit) 推出 totalPages。
每条命中只取回一份固定的属性清单,并映射成一个稳定的结果结构,缺失字段用 null / 空数组兜底,下游代码无需再逐一防御。搜索处理层在服务之外包了一层可选缓存,缓存键是对过滤条件、页码、每页数量做 SHA-256 后取值。索引写入(upsert、delete)是即发即忘——不等 Meilisearch 索引完成。
本节内容
- 查询编排:
application/search/queries/SearchTalents.ts、GetTalentDetail.ts - 端口与结构:
application/search/TalentSearchService.ts - Meilisearch 适配器:
infrastructure/search/MeilisearchTalentSearchService.ts、ensureIndexSettings.ts