Matching and notifications (WS3)
How a submission becomes ranked matches and in-app alerts
Matching is the bridge between an industry's language and a government's technical organization. By default the pilot uses understandable deterministic scoring: lowercase tags and keyword overlap produce ranked portfolio candidates. Deployments can opt in to semantic scoring with Amazon Bedrock embeddings by setting MATCHING_SCORER=bedrock; the scorer interface keeps route handlers unchanged either way.
What happens after Submit?
Submission with SUBMITTED status and then calls routeSubmission(submissionId).PortfolioNode with its command.TagOverlapScorer by default, BedrockScorer when MATCHING_SCORER=bedrock) scores each node and filters candidates.SubmissionMatch rows atomically and changes status to ROUTED if any exist.SUBMISSION_MATCH notification when settings allow it.Scoring details
The current constants are:
| Constant | Value | Meaning |
|---|---|---|
MATCH_THRESHOLD | 0.2 | Minimum score normally retained for both scorers. |
MIN_MATCHES_ON_OVERLAP | 3 | TagOverlapScorer-only fallback count when overlap exists but scores miss threshold; BedrockScorer does not retain sub-threshold candidates. |
| Tag weight | 0.75 | TagOverlapScorer-only contribution from tag overlap. |
| Keyword weight | 0.35 | TagOverlapScorer-only contribution from title/summary keyword hits. |
| Score cap | 1 | Normalized score cap used by both scorers. |
Tags are normalized and unioned from organization and submission. Keyword hits compare node tags with submission title and summary. Matches are sorted descending and rounded to three decimals for persistence.
If at least one score reaches threshold, those candidates are persisted. Under tag-overlap scoring, if no score reaches threshold but there is meaningful overlap, the top three candidates are retained so a sparse pilot dataset does not silently discard a potentially useful submission; this fallback does not apply under embedding scoring, where a positive score does not imply literal overlap. If rerouting produces no candidates, a previously ROUTED record returns to SUBMITTED.
POC fan-out
The matcher checks every InterestProfile. A profile matches when its assigned portfolio node is among the matched nodes or its tags/keywords overlap the submission tags. The service deduplicates by user and link URL; rerouting the same submission does not create an identical notification again.
notifyUserForTags then applies NotificationSetting: disabled categories are skipped, and a nonempty preference filter requires at least one overlapping event tag. The notification body identifies organization, submission title/type, and whether the match came from a portfolio or tags.
Notification functions
| Function | Use |
|---|---|
notifyUser | One recipient, no event-tag filtering. |
notifyUserForTags | One recipient, with event-tag filtering. |
notifyGovByTags | Find government interest profiles overlapping event tags. |
notifyAllIndustry | Fan out a published announcement/call to all industry users. |
The bell UI reads notification rows. The unread-count route counts read = false; read routes update only the current user's rows.
Bedrock embedding scorer
When MATCHING_SCORER=bedrock, BedrockScorer embeds the submission text (title, summary, tags) and each portfolio node's name/description/tags with a Bedrock embedding model (BEDROCK_EMBEDDING_MODEL_ID, default Amazon Titan Text Embeddings V2) and scores by cosine similarity. Similarity is rescaled against BEDROCK_SIMILARITY_FLOOR (default 0.35) so scores are comparable to MATCH_THRESHOLD. Node embeddings are cached per server process; the submission is embedded once per routing pass. Credentials come from the default AWS provider chain (IAM task role) and AWS_REGION — no static keys.
If Bedrock is not configured, unreachable, or any call fails, the routing pass falls back to TagOverlapScorer in full: a degraded pass is rescored deterministically so semantic and tag-overlap scores are never mixed in one ranking, and routeSubmission never breaks. Deployments that do not set MATCHING_SCORER keep the deterministic scorer.
This is real model invocation: routing calls Amazon Bedrock (InvokeModel) with the deployment's IAM role. Persistence and notification behavior are identical for both scorers — only the score and rationale computation differs.
Troubleshooting a missing notification
Check that the profile has the expected tags or portfolio node, the category setting is enabled, and the submission's normalized tags overlap. Check whether an identical notification link already exists. A match row and a notification row are separate: a submission can be matched while a user is skipped by preference policy.
Worked Pelagic example
Pelagic Dynamics is seeded with uuv,autonomy,acoustics. A white paper tagged uuv,autonomy produces a union containing those terms. The Unmanned Maritime Systems node contains uuv,usv,unmanned,maritime robotics,autonomy, so it has strong overlap. The Undersea Warfare node contains sonar,acoustics,undersea,asw,torpedoes; it may be retained through acoustics or fallback overlap depending on the submission text. The service persists node matches first, then evaluates POC profiles.
The default tag-overlap scorer is intentionally explainable: a reviewer can inspect normalized tags, node tags, score, and rationale rather than trusting an opaque classifier. Bedrock scoring trades that tag-level inspectability for semantic similarity, although its score and rationale are still persisted for the record. With the default scorer, changing spelling (unmanned surface vehicle versus usv) can change a result; tag vocabulary is a product contract.
Rerouting
POST /api/notifications/reroute is useful after seed data or interest profiles change. It selects SUBMITTED records, invokes the same service as a new submission, and reports how many records were routed and how many matches were created. Existing matching rows are replaced transactionally. Existing identical notification links are not resent.
Rerouting does not make a submission reviewed or archived. It only recalculates routing state and associated alerts.
Diagnosing a missing match
When an expected match is absent, compare the normalized tag tokens first. Tags are lowercased, trimmed, and split on commas; C2 (Afloat) and c2 (afloat) therefore compare consistently, while a semicolon-separated list is one token. Then check whether the submission's organization tags were included in the union passed to the scorer.
Next inspect the portfolio node's techTags, its InterestProfile users, and the configured threshold. MATCH_THRESHOLD is 0.2 for both scorers, with Bedrock similarity rescaled against its configured floor before comparison. Under tag-overlap scoring, a score below the threshold can still retain one of the top three candidates when meaningful overlap exists; Bedrock scoring does not use that overlap fallback, so sub-threshold candidates are discarded. A match row without a notification can be explained by a disabled setting, a tag filter, or a recipient without a matching profile.
Diagnosing a duplicate notification
Notification links are part of de-duplication. Two different links can represent two rows even when titles look the same. Conversely, rerunning routing for the same submission and node should not create an identical alert. Inspect Notification.kind, userId, and linkUrl together rather than searching title text alone.