2026-09-23·by Sijie Wang#node#project#youteacher#talent

talent-sync-pipeline

The profile service owns a teacher's canonical data; the talent service owns the search index the school side queries. The talent-search-sync-queue node describes the producer end — how a profile edit gets enqueued. This node is the consumer end: how the talent service picks a job up off that queue and turns it into a row in the search index. The two ends meet at one Redis-backed BullMQ queue named talent-sync.

The worker: consuming talent-sync

TalentSyncWorker wraps a BullMQ Worker bound to the talent-sync queue over a Redis connection. It runs with a concurrency of 5, so several talent updates can be indexed in parallel. Each job carries a TalentSyncJobData payload — the search-relevant projection of a profile: identity (id, username, headline, bio, avatar), the facets the school side filters on (skills and their ids/names, languages, city and country, preferred locations, availability, experience level and years, a salary-expectation range, credentials), and the visibility fields (a public/limited/hidden level plus per-audience booleans).

The worker itself does almost nothing beyond translation: it logs the job in, maps the payload to an UpsertTalentCommand, awaits the handler, and logs the job out. It attaches completed and failed listeners for observability, and exposes a close() for graceful shutdown. Because it awaits the handler, a throw propagates back to BullMQ as a job failure — which is what lets the queue's retry policy (defined on the producer side) actually do its job.

From job to command: the mapping

The mapping step is deliberate, not a passthrough. username on the wire becomes displayName on the command. Nullable wire fields — avatarUrl, yearsExperience, salaryExpectationMin/Max — are normalized from null to undefined (?? undefined), so the downstream layer sees "absent" rather than an explicit null. Skills arrive in three parallel shapes (skills, skillIds, skillNames) and are carried across as-is. This is the seam where the profile service's vocabulary and the search service's vocabulary are reconciled in one place.

The handler: visibility decides upsert vs. delete

UpsertTalentHandler.execute is the single write path into the index. It first guards its input — a command with no id throws immediately. Then it branches on visibility, and this is the load-bearing decision:

  • If visibility === 'hidden', the talent is deleted from the search index rather than written. Hiding a profile is expressed as an index removal, so a teacher who goes hidden simply stops being findable.
  • Otherwise, a TalentDocument is upserted. Upsert (not insert) is what keeps the index eventually consistent — the same code path handles a brand-new teacher and the hundredth edit of an existing one.

The handler comment names the destination in as many words: the document it builds is the one for Meilisearch, which is the search engine behind the talent surface.

Derived fields at index time

The handler does not just copy the command into a document — it derives several fields so the index carries answers the raw profile does not:

  • displayName falls back through displayName → fullName → 'Unknown', so the index never stores a nameless row.
  • hasVerifiedCredentials is computed as "does any credential have status verified" — a single boolean the search side can filter on without walking the credential array.
  • publicToEmployers is true when visibility is public or limited; publicToRecruiters is true only when public. So the one visibility level fans out into per-audience access flags right at write time, and the index enforces who may see whom.
  • experienceYears is written as an alias of yearsExperience for filter compatibility, and updatedAt is stamped with the current time on every write.

The shape of the design: the profile is the source of truth, the index is a derived view, and the derivation lives in one handler so every talent row is built the same way.

Around the pipeline

Two more pieces of talent-service infrastructure sit beside the sync path. ProfileServiceClient is the reverse direction — an authenticated service-to-service HTTP client the talent service uses to read back from the profile service: an employer's verification status, a recruiter's approval status, and the caller's own talent profile. It passes the caller's session through and, on any error or non-success response, logs and returns null rather than throwing — a read failure degrades to "unknown," never a crash. An interface is extracted so tests can supply a mock.

TalentCleanupService is a small opportunistic janitor: at most once per hour, it fires a non-blocking background pass (its own failures are caught and logged, never surfaced to the caller) that deletes expired invitations older than 90 days. It is the genuinely fire-and-forget part of the service, kept apart from the awaited sync write.

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 →