Bilel Abdelmoula

Home › Blog

OpenAI · Embeddings · Vector search

Building Candidate Matching With Persisted Embeddings and Hard Filters

By Bilel Abdelmoula ·

Recruiters on a production ATS had a ~35k-candidate pool. I designed and built Potential Match, the system that ranks those candidates against a job. Here's the architecture, and the mistake in the first version that taught me the most.

Persist embeddings, don't regenerate them

  • Candidate profile data is converted into clean semantic text and embedded with OpenAI text-embedding-3-small.
  • Vectors are stored in a dedicated table, not regenerated on every search.
  • Candidate changes trigger an embedding update, so the index stays fresh.
  • Jobs are represented in a comparable structure.

Hard filters first, similarity second

The ranking flow:

~35k candidates → language filter → geographic/distance filter (configurable radius) → vector similarity → weighted similarity signals → client-defined 0–100 score → ranked list.

Languages and distance are hard rules recruiters can't compromise on, so they run before any similarity scoring.

The first version was too noisy

Version one embedded a long natural-language summary of each candidate. The extra text diluted the signal and lowered similarity quality. We redesigned it with cleaner, separated representations: role/position, skills and technologies, and experience.

Lesson: what you embed matters more than which vector database you use. Clean, focused text beats a long paragraph.

What it doesn't do

The production system returns a ranked list with a 0–100 score. It does not generate written AI explanations for each score. More on the platform in the Recruitment ATS/CRM case study and on my AI integration page.

Add as a preferred sourceon Google

Got a slow or messy app?

I'm a full-stack developer (React/TypeScript, APIs, PostgreSQL, AI), open to remote roles and contract work. A short call is enough to see if I can help.

More articles