Skip to content

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.jom and .jpm store the source boundary and matched user date. Application writes to JobOffers and Jobs stamp a BSON UTC matchingUpdatedAt; 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_change stores 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_run stores nonempty complete-run references. A last_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 trainingIds makes 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 same userId and 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's trainingSuggestions empty and preserves every other objective.
  • A non-empty trainingIds makes the model assign the supplied trainings to the objectives it generates. No training search runs and actionSuggestions stays 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, training and skill) own ingestion, cleaning and standardization. Entity-to-matching calls use a product service.py facade.
  • Matching products can use entity data and infrastructure, but one product must not reach into another product's internal modules.
  • matching/shared contains code reused by at least two matching products. It is not a home for generic entity models or unrelated utilities.
  • common remains 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.