OpenAI · Embeddings · Vector search
Building Candidate Matching With Persisted Embeddings and Hard Filters
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.