Skip to content

Matching algo: Scoring based on history, match preferences, and industry. - #135

Open
Allimonae wants to merge 1 commit into
match-randomfrom
matching-algo-score
Open

Allimonae wants to merge 1 commit into
match-randomfrom
matching-algo-score

Conversation

@Allimonae

@Allimonae Allimonae commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Overview

This adds the scoring-based matching algorithm, which replaces the purely random pairing used by the matching CLI.

Greedy pairing

  • GreedyPairer scores every possible pair of members, then takes the highest-scoring pairs first and skips any pair that includes someone already taken.
  • Members are shuffled with a seeded Random before scoring, so ties break randomly but reproducibly. With no scorers, the result is identical to RandomPairer for the same seed.
  • If the member count is odd, one member is left unmatched.

Scorers, each added as a layer on top of the previous ones:

  • HistoryScorer applies a -100 penalty to any pair matched in a previous cycle, regardless of order. Repeats only happen when there is no alternative.
  • MatchPrefScorer gives +20 for compatible preferences (Mentor with Mentee, Peer with Peer, or either member having no preference) and -20 for conflicting ones. The history penalty outweighs the best preference bonus, so preferences never cause a repeat.
  • IndustryScorer gives +10 when both members share a specific industry. "Other", "No Preference" and differing industries are neutral, so industry only breaks ties between pairs the earlier layers score equally.

Preference parsing

  • Industry parses Member.industryPref, ignoring case and whitespace. Missing or unrecognized values count as "No Preference".
  • MatchPreference parses Member.matchPref. The stored value is the full dropdown text (e.g. "Mentor - I am looking for guidance ..."), so it is recognized by its leading label. Missing or unrecognized values count as "No Preference".

Matching CLI

  • MatchingCli now builds the scorer list for the selected layer and every layer before it, and runs GreedyPairer instead of RandomPairer.
  • The pairs table has new columns for each member's match preference, industry and pair score.
  • The totals section reports how many pairs have compatible, neutral and conflicting preferences, and how many share an industry.

Copy link
Copy Markdown
Member Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@Allimonae Allimonae changed the title Matching Algo: Scoring based on history, match preferences, and industry. Matching algo: Scoring based on history, match preferences, and industry. Oct 9, 2026
@Allimonae
Allimonae force-pushed the matching-algo-score branch from ca067f9 to abe319f Compare October 13, 2026 22:00
* The industry a member is in or wants to get into, from the sign-up dropdown stored in {@code Member.industryPref}.
* Comment - The current match preference dropdown includes description, the mock data does not
*/
public enum Industry {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also for the future - but i feel like it might be a good idea to eventually have a negative industry match - like someone wanting to talk to someone out of their industry too. we'd have to adjust the signup form as well, but just a thought.

return NO_PREFERENCE;
}
String trimmed = industryPref.trim();
for (Industry industry : values()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can just do Industry.valueOf(industryPref) here

@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bd76056b-9fe5-4c7d-9194-a8bec394d435

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

@Allimonae
Allimonae marked this pull request as ready for review October 26, 2026 19:06
Comment on lines +20 to +27
return isCompatible(prefA, prefB) ? COMPATIBLE_BONUS : CONFLICT_PENALTY;
}

private static boolean isCompatible(MatchPreference prefA, MatchPreference prefB) {
return (prefA == MatchPreference.MENTOR && prefB == MatchPreference.MENTEE)
|| (prefA == MatchPreference.MENTEE && prefB == MatchPreference.MENTOR)
|| (prefA == MatchPreference.PEER && prefB == MatchPreference.PEER)
|| (prefA == MatchPreference.NO_PREFERENCE || prefB == MatchPreference.NO_PREFERENCE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Javadoc says a member with no preference is neutral, but isCompatible returns true whenever either side is NO_PREFERENCE, so those pairs get COMPATIBLE_BONUS (+20) instead of 0. Missing or unrecognized matchPref values parse to NO_PREFERENCE, so they also get +20.

The greedy pairer then ranks "no preference" pairs level with true Mentor/Mentee and Peer/Peer pairs, and above pairs that merely lack a conflict. The CLI's "no preference" count is also always 0, because such pairs are counted as compatible.

Fix: return 0 when either side is NO_PREFERENCE, and keep +20 or -20 only for specific preferences.

if (prefA == NO_PREFERENCE || prefB == NO_PREFERENCE) return 0;
return isCompatible(prefA, prefB) ? COMPATIBLE_BONUS : CONFLICT_PENALTY;
Suggested change
return isCompatible(prefA, prefB) ? COMPATIBLE_BONUS : CONFLICT_PENALTY;
}
private static boolean isCompatible(MatchPreference prefA, MatchPreference prefB) {
return (prefA == MatchPreference.MENTOR && prefB == MatchPreference.MENTEE)
|| (prefA == MatchPreference.MENTEE && prefB == MatchPreference.MENTOR)
|| (prefA == MatchPreference.PEER && prefB == MatchPreference.PEER)
|| (prefA == MatchPreference.NO_PREFERENCE || prefB == MatchPreference.NO_PREFERENCE);
if (prefA == MatchPreference.NO_PREFERENCE || prefB == MatchPreference.NO_PREFERENCE) {
return 0;
}
return isCompatible(prefA, prefB) ? COMPATIBLE_BONUS : CONFLICT_PENALTY;
}
private static boolean isCompatible(MatchPreference prefA, MatchPreference prefB) {
return (prefA == MatchPreference.MENTOR && prefB == MatchPreference.MENTEE)
|| (prefA == MatchPreference.MENTEE && prefB == MatchPreference.MENTOR)
|| (prefA == MatchPreference.PEER && prefB == MatchPreference.PEER);

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Allimonae this is a valid comment - maybe we should assign 0 points to the score if its no preference instead?

}
String trimmed = matchPref.trim();
for (MatchPreference preference : values()) {
if (trimmed.regionMatches(true, 0, preference.label, 0, preference.label.length())) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above for valueOf

Comment on lines +20 to +27
return isCompatible(prefA, prefB) ? COMPATIBLE_BONUS : CONFLICT_PENALTY;
}

private static boolean isCompatible(MatchPreference prefA, MatchPreference prefB) {
return (prefA == MatchPreference.MENTOR && prefB == MatchPreference.MENTEE)
|| (prefA == MatchPreference.MENTEE && prefB == MatchPreference.MENTOR)
|| (prefA == MatchPreference.PEER && prefB == MatchPreference.PEER)
|| (prefA == MatchPreference.NO_PREFERENCE || prefB == MatchPreference.NO_PREFERENCE);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Allimonae this is a valid comment - maybe we should assign 0 points to the score if its no preference instead?

public int score(Member memberA, Member memberB) {
Industry industryA = Industry.fromIndustryPref(memberA.getIndustryPref());
Industry industryB = Industry.fromIndustryPref(memberB.getIndustryPref());
return industryA.isSpecific() && industryA == industryB ? SAME_INDUSTRY_BONUS : 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if were being biased towards same industry for those who want it, shouldnt we apply a small negative penalty if both industries are specific and different?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants