← All work

WordPress · AI matching

Keoni Consulting

Corporate website and AI CV matching (Keoni AI)

A corporate website, plus a matching engine that ranks the CVs of a WordPress job board for each job offer, with an explanation for every candidate.

Role
WordPress developer & matching engineer
Period
2025 – 2026
Client
Kéoni Consulting · Bénin Digital
Status
Live
  • 32k+CVs indexed
  • 66active job offers
  • 180automated tests passing
WordPressPHPFastAPIPostgreSQLpgvectorsentence-transformersTesseractn8nDocker ComposeGitHub Actions

Context

Kéoni Consulting needed a clear online presence and a faster way to shortlist candidates in a pool of more than 32,000 CVs. The goals: cut pre-selection time, improve the relevance of the profiles proposed, and keep control of the data by self-hosting the stack.

What I built

  • The institutional website in WordPress and Elementor, with clear service pages and contact flows.
  • A matching engine in FastAPI: it reads offers and CVs from WordPress, indexes CV text and vectors in PostgreSQL with pgvector, scores every CV, and writes the results back into WordPress itself.
  • A score that shares its formula, weights and skills taxonomy with rhconsole, so one CV gets comparable scores on both platforms. The cross-encoder re-reads only the 100 best CVs, and the 300 best results are written back.
  • keoni-bridge, a custom WordPress plugin in PHP: REST endpoints, result tables, an admin screen, a keoni_matching shortcode, and the recruiter screens with a "Lancer IA" button, live progress that survives a page reload, and a detailed score view.
  • Text extraction with a per-page OCR fallback for scanned or badly encoded PDFs, and an incremental indexing that skips unchanged CVs.
  • A self-hosted stack on Docker Compose (three containers on a shared VPS) with images in GHCR. GitHub Actions runs the tests, builds the image and deploys over SSH in 7 to 10 minutes. Backup, rollback and monitoring scripts complete it.

How it fits together

WordPress (js-jobs)"Lancer IA" buttonMatching enginePOST /score/triggerPostgreSQL+ pgvector indexCross-encodertop 100 re-readtop 300 results + end signal written to WordPress (keoni-bridge)DEPLOYMENTGitHub ActionstestGHCRimage buildSSH deployDocker Compose3 containersSmoke tests(health + E2E)

Simplified view of the matching flow and the deployment pipeline.

How it evolved

The first version (February to March 2026) chained a WordPress hook, an n8n workflow, an in-memory FAISS index and a write back to WordPress. In September 2026 I rewrote the engine around pgvector and text windows, with direct writes into WordPress. Since October the "Lancer IA" button calls the engine directly, and n8n is only used for extraction previews.

Decisions & trade-offs

Rewrite instead of patching

The first chain truncated text silently and kept results in memory. The new engine indexes CVs in overlapping text windows (no silent truncation), ranks a candidate on the best window rather than an average, and writes results itself.

Score everyone, re-read only the best

Running the cross-encoder on all 33,000 CVs meant an offer never finished. It now re-reads the top 100 only, and every other CV keeps a real score from cosine similarity. Sending one result per CV in a single request was also replaced by writing the 300 best.

Measure before fixing the OCR

OCR was timing out on every page. Measurements showed one OCR took 1.1 s, but eight in parallel pushed the server load to 32. Tesseract is multi-threaded, so a wrapper limits it to one thread: the same eight now finish in 2.4 s, and PyTorch keeps its threads.

No evidence is not a good score

An audit showed empty or unreadable CVs ranking near the top, because semantic similarity saturates around 73%. A CV with no detected skill is now capped at 30%, generic skills weigh less, and one person gets one line even with several applications.