meilisearch-search-ranking-and-mixing

Meilisearch 搜索、排序与结果混排

这是 YouTeacher 职位服务的读取路径:一次搜索查询如何变成一页已排序、已去重、平台多样的职位。它位于职位搜索接口之后,索引交给 Meilisearch,短时缓存交给 Redis,排序与混排则由少量应用层逻辑完成。

请求流程

搜索请求先进入职位搜索控制器。控制器解析可选的用户身份,用 zod schema 校验查询(自由文本 qlocation 限 240 字符、pageSize 上限 100、薪资上下界为数字、languages 按逗号拆分、日期区间,以及平台/招聘者/雇主过滤项),并按调用方施加限流。随后它把一个扁平的过滤对象交给搜索处理器,响应之后再触发一次尽力而为的清理。

处理器先按默认值与上限归一化 pagepageSize,然后先查缓存。命中则原样返回缓存页;未命中则执行 Meilisearch 查询,打上新的 lastUpdatedAt,可选地做平台多样化,最后把补全后的结果写回缓存再返回。

查询 Meilisearch

Meilisearch 适配器根据请求构建过滤列表:职位类型、招聘者、雇主、科目、每种语言一条子句、来源平台、薪资下限与上限、投递日期区间(与存储的毫秒时间戳比较),以及一条按城市省份国家匹配的地点子句。末尾始终追加 status = "active",因此只返回在线职位。自由文本搜索串由查询与地点拼接而成。

薪资下限过滤在有汇率服务时按币种感知:适配器刷新汇率并据此构建最低薪资子句,而不是跨币种直接比较原始数字。

排序

排序表达为一组按序生效的 Meilisearch sort:

  1. locationCountryRank 升序——按国家的排名,把优先市场顶到前面;
  2. postedAt 降序——新近度;
  3. id 降序——一个稳定、确定的并列打破规则,让同排名、同新近度的职位在翻页时保持固定顺序。

Meilisearch 返回命中后,适配器再做一次索引无法表达的运行时可见性检查:对直发(direct)职位剔除已过期者;聚合职位跳过此检查(其生命周期在别处管控)。总数取自响应的估计命中总数。

分面与索引契约

每次搜索还会向 Meilisearch 请求国家、科目、职位类型、语言、招聘者名称、来源平台这几个维度的分面分布,适配器把它们转成按计数排序的桶。可搜索、可过滤、可排序的属性集合只声明一次,由一个幂等的设置步骤推送到索引:只有当索引现有设置与目标不同才重写,若索引不存在则以 id 为主键创建。

平台多样化的结果混排

聚合职位来自多个来源平台,单纯按新近度排序可能让某一个平台霸屏。一个可选的多样化器处理这一点——默认关闭。开启后,它只在一页中的不同平台数量少于期望值(默认 3,下限 2)时才动作。它读取平台分面,挑出当前页缺席的若干平台,对每个平台跑一次小规模补充搜索,按职位 id 去重,然后对合集做轮转(round-robin):职位按首次出现顺序分到各平台的队列,逐个平台各取一条直到取尽,最后把这一页裁回原本的大小。效果是一页把各平台交错排列,而不是堆叠。

缓存

搜索缓存是一层很薄的 Redis,默认存活时间很短(数十秒)。它的键是 前缀 : 版本 : sha1(序列化后的输入),因此归一化查询的任何变化都会产生不同的键。版本 段是失效的杠杆:把单个版本计数器加一,就能让之前所有缓存键一次性变得不可达——这让服务别处的一次写入无需枚举键即可让全部缓存搜索失效。

相关

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 →