Matching architecture¶
API version
Main API: 4.50.36
Matching is organized as a business capability under src/matching. Its three
products own their workflows and scoring rules; only genuinely reusable
matching mechanics live in matching/shared.
src/matching/
├── api/ # cross-product processing adapters
├── job_offer/ # JOM: user ↔ opportunity
│ ├── api/ # JOM health, feedback, pool, scores and statistics
│ ├── feedback/ # JOM feedback generation and transformation
│ ├── insights/ # Talent Match Insight
│ ├── pool/ # candidate pool and eligibility rules
│ ├── reporting/ # JOM exports and statistics workflows
│ └── scoring/ # dislikes, motivation and similarity
├── job_proximity/ # JPM: user ↔ job
│ ├── api/ # JPM score and statistics adapters
│ ├── reporting/ # JPM statistics workflows
│ └── scoring/ # description and skill eligibility
├── training/ # training ranking, rationale and topics
│ └── api/ # Training Matching HTTP adapters and schemas
└── shared/
├── filtering/ # cross-product hard filters
├── postgres/ # PostgreSQL JOM/JPM persistence and trigger jobs
├── results/ # formatting, persistence and publication
└── scoring/skills/ # reusable skill scoring
Product boundaries¶
Job Offer Matching (JOM)¶
matching/job_offer owns both matching directions: opportunities recommended
to a user and users recommended for an opportunity. It also owns pool creation,
JOM-specific scoring and Talent Match Insight. Insight generation is part of
the JOM product even though it exposes its own endpoints.
The separate src/pwc_jom_compatibility boundary preserves five selected
pwc-jom-php HTTP contracts without changing the native JOM APIs. HTTP
adapters and authentication dependencies live in api, public models in
schemas, AI and score workflows in services, and the legacy filters,
formatters, prompts and errors in common. Its score routes read the existing
PwC UsersMatchingScore and
OpportunityMatchingScore collections and source their default weights from
the legacy Company.scoresWeight attributes. Its AI routes retain the
historical prompt preparation while using AI Hub's tenant configuration and
default LLM profile. The boundary is MongoDB-only and prefers the API key's
clientId, falling back to Client-ID, with the fixed smartmobility data
source; it does not introduce a second matching or provider runtime.
Job Proximity Matching (JPM)¶
matching/job_proximity owns user-to-job and job-to-user workflows, description
scoring, final JPM score calculation and the minimum-skill eligibility rule.
PostgreSQL JOM/JPM runtime¶
When DATABASE_URL is configured, matching uses the relational entity
projections and PostgreSQL score tables instead of MongoDB collections.
JOM stores user-to-opportunity scores in aihub_user_opportunity_matching and
opportunity-to-user scores in aihub_opportunity_user_matching. JPM reuses the
existing Smart Mobility user_job_matching table and replaces rows directly;
the JPM RabbitMQ score producer is not used in PostgreSQL mode.
Alembic creates the score tables, indexes for user/opportunity/job score and
pool lookups, and the aihub_matching_jobs queue table. PostgreSQL triggers
enqueue deduplicated jom_user and jpm_user jobs only for user-side changes:
profile fields used by matching, last_login, declared skills, inferred
skills, organization assignments, platform activity and career-plan rows. There
is no matching-job enqueue trigger on opportunities or jobs, and no backfill rule
for users with missing scores. A future last_login update schedules a
freshness check when the matching product is enabled in aihub_config.
Separate source-change triggers record dates used by that check.
The PostgreSQL matching repository translates the current MongoDB hard-filter
logic to SQL for active tenant rows, dislikes, classification, contract type,
location and external-user exclusion. Opportunity accessGroup is a PwC-only
contract and remains ignored in PostgreSQL mode because the relational
opportunity projection represents Smart Mobility source data. Training access
is independent and uses trainingAccessGroup.
Unit processing endpoints (/api/v1/user/process,
/api/v1/opportunity/process and /api/v1/job/process) and bounded matching
batches select the same backend as the application lifecycle. The user endpoint
accepts the jom and jpm query flags, both enabled by default, and runs the
selected products concurrently. Its response contains a jom or jpm key for
each selected product; disabled products are omitted, so disabling both returns
an empty object. If PostgreSQL resources exist, these endpoints load users,
opportunities, jobs, skills and existing scores through SQL repositories. If
PostgreSQL resources are absent, they keep the MongoDB collection paths. Empty
PostgreSQL result pools still delete the previous rows for the processed entity
so a recalculation always replaces the stored result set.
RabbitMQ user processing also runs eligible JOM and JPM recalculations
concurrently after the independent score-freshness checks. The PostgreSQL
matching listener processes every claimed batch concurrently, with concurrency
bounded by PG_MATCHING_BATCH_SIZE; each job retains its own lease heartbeat,
failure recording and retry behavior.
User matching freshness¶
The RabbitMQ user flow and the PostgreSQL matching worker decide independently whether to run a complete user-side JOM or JPM. The check is scoped to the user's tenant and covers the whole target catalogue, including targets outside the previous hard-filter pool and inactive targets. It never calculates only changed pairs and never schedules a new batch after an import.
A product runs when its complete-run reference is missing, its scores are missing, the user source date differs from the date used by that run, or a target was changed at or after the calculation started. Recording the start boundary prevents an update during calculation from being hidden by the later score-save date. The boundary is conservative at BSON millisecond precision. User-source dates are kept separately to detect a user change after the input profile was read. Inverse job-to-user JPM calculations share the score table but do not advance a complete user-side reference.
Before replacing persisted scores, the previous reference is cleared. Only a
successful, nonempty complete user calculation records a new reference.
Empty results always run again on the next user message, whether the
catalogue is empty or all entities fail the hard filters. Their old scores are
still removed. JPM's zero-score neutralized rows for excluded jobs do not
count as eligible results and cannot establish freshness. Failed
persistence/publication and save_scores=false never
record a successful reference. Read-only score retrieval, forced processing,
manual batches, product-enable flags and only_standardize retain their
existing execution contracts.
- MongoDB:
Users.matchingState.jomand.jpmstore the source boundary and matched user date. Application writes toJobOffersandJobsstamp a BSON UTCmatchingUpdatedAt; bulk deactivation, expiry maintenance and attached-job propagation maintain it too. Indexes on this field support latest-target lookups. An import counts as an update, including a repeated target import. Physical target deletion clears that product's references for the tenant, since the deleted document can no longer carry its change date. User imports cannot overwrite the internal complete-run reference. - PostgreSQL:
aihub_matching_source_changestores indexed, tenant-scoped dates for users, jobs and opportunities, including relational associations, translations, classifications, skills and eligibility changes. Deleted target references remain available for invalidation. Opportunity expiry is checked through indexed validity dates even without a source-row update.aihub_user_matching_runstores nonempty complete-run references. Alast_login-only update schedules the existing task but does not change the profile source date.
Deploy Alembic revision 20261021_27 before using the updated PostgreSQL runtime.
For existing MongoDB clients, reconcile the new classic indexes through
POST /admin/api/v1/clients/indexes; imports and matching do not create indexes.
Existing individual scores are preserved, but cannot supply a complete-run
reference: each active user/product is calculated on its next eligible event.
No automatic catch-up campaign is introduced.
Training Matching¶
matching/training owns the Main API adapters for natural-language learning
content search and generated rationales. POST /api/v1/get_training_matching
reuses the existing Learning Finder flow, including tenant access rules and
user context, and returns unique real learning-content IDs. Synthetic
simulation cards are not returned by this endpoint.
The JSON request accepts an optional limit from 1 to 100 and defaults to 6.
It also accepts up to 100 optional trainingIds; these identifiers, their
parents and their children are excluded from the MongoDB or PostgreSQL search.
Every training present in the user's trainingHistory is excluded from the
automatic MongoDB and PostgreSQL search, whether its completionDate is set or
null.
Because these endpoints return identifiers only, they run the Learning Finder in an identifier-only mode that skips every text produced for presentation: the collection title and subtitle, the per-training rationales, the companion simulation cards, and the generated empty-result message. That mode also skips the search-or-clarify classification, since an API caller has nobody to answer a clarification and always wants a search; a request that the chatbot would answer with a clarification question therefore runs a real search here. The streaming reflection steps are skipped as well, because no LangGraph stream consumes them outside the chatbot. Training Matching query resolution and filtering are shared with chatbot searches. The returned identifiers keep the weighted RRF order; identifier-only mode only skips presentation generation.
GET /api/v1/get_training_matching_rationale generates its rationale in the
language identified by the optional BCP 47 locale parameter, which defaults
to English (en). MongoDB resolves its tenant from the API key's clientId,
with Client-ID as fallback. PostgreSQL ignores that routing header and derives
the relevant setting.id from the resolved user when filtering jobs or
trainings.
POST /api/v1/development-objectives/suggestions builds personalized
development objectives from the user's job, experiences, skills, skill gaps and
sanitized training history through the default semantic LLM profile. The LLM receives up to 100 historical trainings,
each with its ID, title and nullable completion date. The route requires the
standard API key. The workflow excludes
existingDevelopmentObjectiveTitles, and accepts an optional BCP 47 locale
defaulting to English (en) and an optional limit of at least 1 that defaults
to 3. Every suggestion carries a title, description, rationale, up to three
skills already present on the profile, actionSuggestions and
trainingSuggestions. trainingSuggestions holds training identifiers only.
The two request shapes produce different suggestions:
- An empty
trainingIdsmakes the model generate the action suggestions. The service then searches up to three accessible trainings per objective, all objectives in parallel, reusing the Learning Finder flow with the sameuserIdand the exact objective title and description as keyword. This caller alone disables the intermediate LLM reformulation and explicit catalog-filter extraction; it preserves the training embedding profile, access, availability, language handling, exclusions and weighted RRF ranking. Training Matching and LangGraph searches keep query reformulation enabled by default. A failed search leaves that objective'strainingSuggestionsempty and preserves every other objective. - A non-empty
trainingIdsmakes the model assign the supplied trainings to the objectives it generates. No training search runs andactionSuggestionsstays empty. Every objective carries at least one training and an objective may carry any number of them, so the service never generates more objectives than the request supplied trainings. Every requested training that resolves to a tenant training is guaranteed to appear in the response; any training the model left out is attached to the last objective. IDs that resolve to no tenant training are ignored.
Skill gaps are optional when other profile evidence or selected trainings exist.
An entirely empty context returns an empty list without calling a model; an
unknown or deactivated user returns 404.
Dependency rules¶
- Entity packages under
src/entity(user,job,opportunity,trainingandskill) own ingestion, cleaning and standardization. Entity-to-matching calls use a productservice.pyfacade. - Matching products can use entity data and infrastructure, but one product must not reach into another product's internal modules.
matching/sharedcontains code reused by at least two matching products. It is not a home for generic entity models or unrelated utilities.commonremains reserved for technical infrastructure and application-wide utilities. Business entities are deliberately not moved there because that would erase ownership rather than clarify it.- FastAPI routers are adapters: product behavior remains in services and workflows. Standardization-by-ID orchestration is route-neutral so score routers do not depend on the global router package.
Architecture tests enforce removal of the former matching packages under
common, prevent the deleted global src/routers package from returning and
restrict entity-to-matching imports to service facades.
Stable HTTP contract¶
Routers moved to the relevant product's api directory, but their public paths,
methods, parameters and response schemas remain unchanged. Connected
applications therefore continue to use the same URLs. Authentication semantics
follow the active database mode.
Routes that historically had no API-key dependency remain public. Both public
and administrative Talent Match Insight endpoints remain JOM endpoints.
Swagger tags are grouped into Matching Operations, Job Offer Matching
(JOM), Job Proximity Matching (JPM) and Training Matching, rather than by
individual router or operation type.
The Training Matching search retains its historical URL while accepting an
authenticated natural-language request body containing userId and keyword.
The optional limit controls the number of results and defaults to 6. Tenant
selection prefers the API key's clientId and falls back to Client-ID in
MongoDB mode; callers no longer supply matching ranking parameters or a client
identifier in the query string.
Cross-product matching is exposed by matching/api/processing_router.py. Import batches live
with their owning entities under entity/user/api, entity/job/api,
entity/opportunity/api and entity/training/api because they are ingestion
concerns.