# Recruit AI: Intelligent Recruitment Matching

Jobs find candidates. Candidates find jobs. One score, both ways.

An open-source hiring marketplace where the ranking is the product: jobs surface the right candidates, candidates surface the right jobs, and both sides are scored by the same rules. Built as a reference for search that gets measured instead of guessed at.

## Tags

Next.js, Effect.ts, Qdrant, Vector Search, Retrieve & Rerank, Vercel AI SDK, Drizzle, PostgreSQL, Turborepo

![Recruit AI matching platform, showing ranked candidate results](https://lxbpjvrr41.ufs.sh/f/6KZjuRTQYJxHcq4HLI3dVa4H9KX8mgF3ln7DUxzuvRhteWip)

## At a glance

- **Type:** Open source, public repo
- **Architecture:** Hexagonal ports and adapters on Effect.ts
- **Stack:** TypeScript end to end, Qdrant, PostgreSQL
- **Status:** Built and public, eval framework waiting on labeled data

## Retrieve and rerank

The pipeline is the pattern search engines and recommenders use: retrieve a wide set fast, then spend the expensive scoring on that set only. Qdrant returns the top 50 by approximate nearest neighbor, and a multi-factor model re-ranks those to the top 10.

Retrieval runs under the hard constraints first. Work mode, location and relocation are payload filters Qdrant applies before the nearest-neighbor step. Vectors are 3072-dimensional Gemini embeddings, indexed with HNSW and compared by cosine distance, so search stays sub-linear as the candidate pool grows. HNSW exposes `m` and `ef_construct` at index time, which is where the precision and recall tradeoff actually gets set.

Re-ranking combines four signals: semantic similarity carried over from retrieval, keyword recall between extracted skills and stated requirements, experience fit on years and seniority, and soft constraints such as salary range and start date. One function scores job→talent and talent→job. Both sides of the marketplace are ranked by the same rules, so the two directions cannot drift apart.

## Measuring the ranking

The weights are a deliberate starting point, not a tuned result. NDCG@10 is the primary ranking-quality metric, and the eval framework is built to tune against it as soon as recruiter-labeled ground truth exists. The eval groups cover each layer:

### Component evals

- **LLM extraction:** field-level accuracy and keyword F1 for resume and job-description parsing, compared across providers
- **Embedding models:** Recall@10, MRR, and the separation gap between good and poor matches
- **Retrieval quality:** filter correctness, eligible-candidate recall, and latency across vector database options

### Scoring and end-to-end evals

- **Weight optimization:** grid and Bayesian search over weight combinations, scored by rank correlation with recruiter preferences
- **Factor ablation:** what NDCG@10 does when a single scoring factor is removed
- **Agent-driven discovery:** an agentic loop that rewrites the scoring function toward a target metric
- **Full pipeline:** system rankings against recruiter-preferred orderings

The framework follows Anthropic's methodology of tasks, trials and graders, with a strict split: dev sets (60%) for iteration, held-out test sets (40%) for the final comparison. Deterministic code graders cover objective accuracy, LLM rubric graders cover the subjective part.

## From uploaded file to searchable record

An LLM turns a resume into skills, experience, preferences and constraints, and a job description into requirements, responsibilities and qualifications. Where something is missing it asks a follow-up question instead of guessing. Every processed record is embedded and indexed automatically.

## Hexagonal architecture

### Core domain

- **Pure scoring functions:** the matching logic is plain testable functions with no Effect dependency
- **Domain models:** typed definitions for talents, jobs and matches
- **Ports:** service boundaries declared as Effect Context Tags

### Adapters

- **PostgreSQL:** Drizzle ORM, type-safe migrations
- **Qdrant:** vector search with payload pre-filtering on the hard constraints
- **AI:** Gemini 2.0 through the Vercel AI SDK, for both extraction and embeddings

Effect.ts carries this beyond convention. Services are `Context.Tag`, so the type system rejects a program until every dependency is provided: missing wiring is a compile error, not a 3am runtime one. Every failure mode is a discriminated union rather than a thrown exception, which means the compiler lists the cases you have not handled. `@effect/rpc` carries the same schemas across the frontend and backend boundary, validated on both sides.

## Monorepo

Turborepo and Bun, split so each package has one job:

- **apps/web:** Next.js frontend with shadcn/ui
- **packages/core:** domain models, ports, scoring
- **packages/db:** Drizzle schema, migrations, PostgreSQL adapters
- **packages/vector:** Qdrant integration
- **packages/ai:** model and embedding providers via the Vercel AI SDK
- **packages/api:** Effect HTTP API layer
- **packages/ui:** shared components
- **packages/env:** environment validation

## Stack

Gemini 2.0 Flash does the structured extraction and the clarifying questions. Gemini embeddings produce the 3072-dimensional vectors. TypeScript runs in strict mode across every package, with Ultracite over Biome for linting.

## Links

- Project page: https://andrey-markin.com/projects/recruit-ai
- All projects: https://andrey-markin.com/projects.md
- GitHub: https://github.com/Mark-Life/recruit-ai
- Contact: https://andrey-markin.com/#contact
