Meilisearch 搜索、排序与结果混排
这是 YouTeacher 职位服务的读取路径:一次搜索查询如何变成一页已排序、已去重、平台多样的职位。它位于职位搜索接口之后,索引交给 Meilisearch,短时缓存交给 Redis,排序与混排则由少量应用层逻辑完成。
请求流程
搜索请求先进入职位搜索控制器。控制器解析可选的用户身份,用 zod schema 校验查询(自由文本 q 与 location 限 240 字符、pageSize 上限 100、薪资上下界为数字、languages 按逗号拆分、日期区间,以及平台/招聘者/雇主过滤项),并按调用方施加限流。随后它把一个扁平的过滤对象交给搜索处理器,响应之后再触发一次尽力而为的清理。
处理器先按默认值与上限归一化 page 和 pageSize,然后先查缓存。命中则原样返回缓存页;未命中则执行 Meilisearch 查询,打上新的 lastUpdatedAt,可选地做平台多样化,最后把补全后的结果写回缓存再返回。
查询 Meilisearch
Meilisearch 适配器根据请求构建过滤列表:职位类型、招聘者、雇主、科目、每种语言一条子句、来源平台、薪资下限与上限、投递日期区间(与存储的毫秒时间戳比较),以及一条按城市或省份或国家匹配的地点子句。末尾始终追加 status = "active",因此只返回在线职位。自由文本搜索串由查询与地点拼接而成。
薪资下限过滤在有汇率服务时按币种感知:适配器刷新汇率并据此构建最低薪资子句,而不是跨币种直接比较原始数字。
排序
排序表达为一组按序生效的 Meilisearch sort:
locationCountryRank升序——按国家的排名,把优先市场顶到前面;postedAt降序——新近度;id降序——一个稳定、确定的并列打破规则,让同排名、同新近度的职位在翻页时保持固定顺序。
Meilisearch 返回命中后,适配器再做一次索引无法表达的运行时可见性检查:对直发(direct)职位剔除已过期者;聚合职位跳过此检查(其生命周期在别处管控)。总数取自响应的估计命中总数。
分面与索引契约
每次搜索还会向 Meilisearch 请求国家、科目、职位类型、语言、招聘者名称、来源平台这几个维度的分面分布,适配器把它们转成按计数排序的桶。可搜索、可过滤、可排序的属性集合只声明一次,由一个幂等的设置步骤推送到索引:只有当索引现有设置与目标不同才重写,若索引不存在则以 id 为主键创建。
平台多样化的结果混排
聚合职位来自多个来源平台,单纯按新近度排序可能让某一个平台霸屏。一个可选的多样化器处理这一点——默认关闭。开启后,它只在一页中的不同平台数量少于期望值(默认 3,下限 2)时才动作。它读取平台分面,挑出当前页缺席的若干平台,对每个平台跑一次小规模补充搜索,按职位 id 去重,然后对合集做轮转(round-robin):职位按首次出现顺序分到各平台的队列,逐个平台各取一条直到取尽,最后把这一页裁回原本的大小。效果是一页把各平台交错排列,而不是堆叠。
缓存
搜索缓存是一层很薄的 Redis,默认存活时间很短(数十秒)。它的键是 前缀 : 版本 : sha1(序列化后的输入),因此归一化查询的任何变化都会产生不同的键。版本 段是失效的杠杆:把单个版本计数器加一,就能让之前所有缓存键一次性变得不可达——这让服务别处的一次写入无需枚举键即可让全部缓存搜索失效。
相关
- aggregated-job-ingestion-pipeline ——文档如何进入搜索所读的索引
- job-lifecycle-validity-and-cleanup ——可见性检查背后的过期与可达性规则
- service-architecture-and-request-flow ——这条读取路径所处的分层