talent-search-meilisearch

Visibility-Aware Talent Search over Meilisearch

Teacher profiles in youteacher live in a Meilisearch index (default name talents, primary key id). Every search runs through one service that gates results by who is looking before it ever gets to skills, location, or salary. The rule is simple: a viewer sees a profile only if the teacher has made themselves public to that kind of viewer.

Visibility gating comes first

The service maps the viewer type to a filter clause that Meilisearch applies server-side:

  • An employer viewer sees profiles where publicToSchools = true OR publicToEmployers = true.
  • A recruiter viewer sees profiles where publicToRecruiters = true.
  • With no viewer type set, no visibility clause is added.

publicToSchools is the canonical field — in this domain employers are schools — and publicToEmployers is kept as an alias so documents carrying either field still resolve. Because this is a filter on the index (not a post-fetch trim), a hidden profile never enters the result set or the total count. The single-profile detail path (GetTalentDetail) re-runs the same check: it loads the document, and if the viewer's visibility flag is present and false it refuses with a 403 access_denied (reason profile_not_public_to_employers / _recruiters); a missing profile is a 404 not_found. When the flag is absent it defaults to visible.

Faceted filters

On top of visibility, the caller can narrow by facet. Each facet is built as its own clause and all clauses are joined with AND; within a multi-value facet the values are joined with OR:

  • skillsskills = "…" per value.
  • availabilityavailability = "…" per value.
  • location — each value matched against both locationCity and locationCountry.
  • experienceexperienceYears >= min and/or <= max.
  • salarysalaryExpectation <= a ceiling.

These fields are declared filterableAttributes in the index settings, alongside the visibility flags. The free-text query runs against searchableAttributes (display name, headline, bio, skills, languages, city, country) under Meilisearch's default ranking rules.

Sorting, paging, mapping

Sort is either recent (updatedAt:desc) or relevance, where relevance is Meilisearch's default and adds no explicit sort. updatedAt and experienceYears are the only sortableAttributes. Paging is offset-based: offset = (page - 1) * limit, defaulting to page 1 and a page size of 20. The response reports total from Meilisearch's estimatedTotalHits and derives totalPages as ceil(total / limit).

Only a fixed list of attributes is retrieved per hit, and each hit is mapped to a stable result shape with null/empty-array defaults so downstream code never has to guard for absent fields. A search-handler layer wraps the service with an optional cache keyed on a SHA-256 of the filters, page, and limit. Index writes (upsert, delete) are fire-and-forget — they don't wait for Meilisearch to finish indexing.

Inside this node

  • Query orchestration: application/search/queries/SearchTalents.ts, GetTalentDetail.ts
  • Port + shapes: application/search/TalentSearchService.ts
  • Meilisearch adapter: infrastructure/search/MeilisearchTalentSearchService.ts, ensureIndexSettings.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 →