Skip to content

Epic 6 — Discover Feed (AI Matching)


BE-DISCOVER-001 — AI Matching service (9-factor scoring)

  • [x] Implemented

Files:

  • Create: src/services/ai/matching.ts — full implementation replacing stub
  • Edit: src/graphql/resolvers/job.tsdiscoverFeed calls real matching service

9 scoring factors (per spec doc):

  1. Salary alignment — compare job.paymentAmount range vs talent's expected rate
  2. Work authorization — match job.requirements.workAuthorization vs talent.workAuthorization
  3. Physical attributes — match age range, gender, height, body type if job specifies
  4. Notable works relevance — semantic similarity (vector search) between talent's works and job genre/type
  5. Production experience — match production type + subtype experience
  6. Awards & nominations — presence and relevance
  7. Dream gig alignment — semantic match of talent.dreamGigs vs job description
  8. Language/accent proficiency — match required languages/accents
  9. Availability — talent.availableFrom vs job.startDate

Dev mode: Falls back to weighted DB filter. Production: calls Qdrant (self-hosted, same Hetzner machine) for factors 4, 7.
Returns score (0–1) and matchReasons: string[] (human-readable explanation per matched factor).


BE-DISCOVER-002 — Vector embedding pipeline for talents

  • [x] Implemented

Files:

  • Create: src/services/ai/embeddings.ts — OpenAI text-embedding-3-small calls
  • Create: src/services/ai/vectorStore.ts — Qdrant upsert/query abstraction
  • Edit: src/services/talent/index.ts — call embeddings.indexTalent(profile) after profile updates

Description: When a talent profile is created or updated, generate a text embedding from: displayName + primaryCategory + skills + experienceEntries + notableWorks + dreamGigs. Upsert into Qdrant with talentId as the key. The discover feed uses these embeddings for factors 4 and 7.

Infrastructure: Qdrant runs as a Docker container on the same Hetzner machine as the backend. Requires ~200 MB RAM idle — fits comfortably on the existing CX22 (4 GB RAM) alongside the backend and face-comparison sidecar. No separate node needed.

Environment variables:

env
QDRANT_URL=http://localhost:6333   # internal — not exposed to the internet
OPENAI_API_KEY=<key>               # for text-embedding-3-small

Coolify setup: Add a Qdrant service to the existing Docker Compose stack:

yaml
qdrant:
  image: qdrant/qdrant:latest
  ports:
    - "6333:6333"
  volumes:
    - qdrant_data:/qdrant/storage
  restart: unless-stopped

BE-DISCOVER-003 — Extend AI matching to use new structured fields

  • [x] Implemented

Files:

  • Edit: src/services/ai/matching.ts — update factors 1, 2, 8 to use new field data
  • Edit: src/__tests__/services/matching.test.ts

Factor updates:

FactorBeforeAfter
1 — Salary alignmentjob.paymentAmount vs rough talent expectationjob.paymentAmount vs talent.salaryExpectation.{ min, max, currency, period } — normalise to same period before comparing
2 — Work authorizationjob.requirements.workAuthorization[] vs talent.workAuthorization[]+ talent.passportVisa.{ passports[], visas[], workPermits[] } — broader match surface
8 — Language/accentlanguage match only (accent field didn't exist)+ talent.accentDialect[] matched against job.requirements.languages[].accent

No schema or API changes — purely service logic update.


BE-SEARCH-001 — Extend talent search filters (notable works, awards, dream job)

  • [x] Implemented

Files:

  • Edit: src/graphql/schema/index.ts — extend TalentSearchFilters input type
  • Edit: src/services/talent/search.ts (or wherever searchTalents is resolved) — add filter logic
  • Edit: src/__tests__/resolvers/search.test.ts

New filter fields:

graphql
input TalentSearchFilters {
  # ...existing fields...
  hasNotableWork:  String    # partial match on notableWorks[].title
  hasAward:        Boolean   # talent has at least one award entry
  dreamJobKeyword: String    # semantic match against dreamJobs[].title
}

Implementation:

  • hasNotableWork: PostgreSQL JSON_ARRAY_ELEMENTS + ILIKE on notableWorks JSON array.
  • hasAward: WHERE awards IS NOT NULL AND awards != '[]'.
  • dreamJobKeyword: vector similarity search via Qdrant — embed the keyword and compare to talent dream-job embeddings. Falls back to JSON ILIKE in dev mode.

FE-DISCOVER-001 — Discover feed UI with swipe stack (producer)

  • [x] Implemented

Files:

  • Edit: apps/app/src/pages/discover/DiscoverPage.tsx — replace placeholder
  • Create: apps/app/src/lib/queries/discover.tsDISCOVER_FEED_QUERY
  • Create: apps/app/src/hooks/useDiscoverFeed.ts
  • Create: apps/app/src/hooks/useSwipeTalent.ts

Description: Replace "Home feed coming soon" with the real discover feed. Shows a stack of TalentCard components (from DS). Swipe right → shortlist, swipe left → skip, tap × → block. After each swipe, call swipeTalent mutation and advance to next card. When stack runs low (< 3 cards), pre-fetch more. Shows EmptyState when no more talents to show. Matches "Talento Fluxo" / "Produtor Fluxo" frames in Figma.

Note: This view is producer-only. Talent users see a different "home" (job listings or notifications feed). Route guard: redirect talents to /jobs.


FE-DISCOVER-002 — Talent home feed (job recommendations)

  • [x] Implemented

Files:

  • Create: apps/app/src/pages/discover/TalentHomePage.tsx
  • Edit: apps/app/src/pages/discover/DiscoverPage.tsx — switch view by role

Description: Talent users' home view: personalized job recommendations shown as JobCard list, ranked by relevance to their profile. Uses jobs query with talent-profile-based pre-filters. Matches the talent's "Jornada" home screen in Figma.