Skip to content

Commit ab67776

Browse files
authored
Merge pull request #292 from KxlSys/bolt-refactor-matching-single-pass-2550758009988849554
⚡ Bolt: Refactor matching logic for O(n) array passing
2 parents fc8cf22 + 1dd54d1 commit ab67776

1 file changed

Lines changed: 113 additions & 102 deletions

File tree

src/lib/matching.ts

Lines changed: 113 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -46,55 +46,62 @@ export function calculateMatches(
4646
): MatchResult[] {
4747
// ⚡ Bolt: Pre-calculate the current user's tech stack as a Set to avoid O(N*M) lookups
4848
const currentUserTechsSet = new Set(currentUser.tech_stack);
49-
50-
return candidates
51-
.filter((c) => c.id !== currentUser.id && c.open_to_collaboration)
52-
.map((candidate) => {
53-
let score = 0;
54-
const reasons: string[] = [];
55-
56-
const complementary = COMPLEMENTARY_ROLES[currentUser.role_type] || [];
57-
if (complementary.includes(candidate.role_type)) {
58-
score += 30;
59-
reasons.push("Competences complementaires");
60-
}
61-
62-
// ⚡ Bolt: Fast Set lookup instead of array.includes() for nested tech stack matching
63-
const commonTechs = candidate.tech_stack.filter((t) =>
64-
currentUserTechsSet.has(t)
65-
);
66-
if (commonTechs.length > 0) {
67-
score += Math.min(commonTechs.length * 10, 25);
68-
reasons.push(`${commonTechs.length} technologie(s) en commun`);
69-
}
70-
71-
if (
72-
currentUser.city &&
73-
candidate.city &&
74-
currentUser.city.toLowerCase() === candidate.city.toLowerCase()
75-
) {
76-
score += 20;
77-
reasons.push("Meme ville");
78-
}
79-
80-
const levelMap = { junior: 1, mid: 2, senior: 3 };
81-
const diff = Math.abs(
82-
levelMap[currentUser.experience_level] -
83-
levelMap[candidate.experience_level]
84-
);
85-
if (diff <= 1) {
86-
score += 15;
87-
reasons.push("Niveau d'experience compatible");
88-
}
89-
90-
if (candidate.open_to_collaboration) {
91-
score += 10;
92-
}
93-
94-
return { profile: candidate, score: Math.min(score, 100), reasons };
95-
})
96-
.filter((m) => m.score > 0)
97-
.sort((a, b) => b.score - a.score);
49+
const complementary = COMPLEMENTARY_ROLES[currentUser.role_type] || [];
50+
const levelMap = { junior: 1, mid: 2, senior: 3 };
51+
52+
// ⚡ Bolt: Consolidate map and filter into a single reduce pass
53+
const results = candidates.reduce<MatchResult[]>((acc, candidate) => {
54+
if (candidate.id === currentUser.id || !candidate.open_to_collaboration) {
55+
return acc;
56+
}
57+
58+
let score = 0;
59+
const reasons: string[] = [];
60+
61+
if (complementary.includes(candidate.role_type)) {
62+
score += 30;
63+
reasons.push("Competences complementaires");
64+
}
65+
66+
// ⚡ Bolt: Fast Set lookup instead of array.includes() for nested tech stack matching
67+
const commonTechs = candidate.tech_stack.filter((t) =>
68+
currentUserTechsSet.has(t)
69+
);
70+
if (commonTechs.length > 0) {
71+
score += Math.min(commonTechs.length * 10, 25);
72+
reasons.push(`${commonTechs.length} technologie(s) en commun`);
73+
}
74+
75+
if (
76+
currentUser.city &&
77+
candidate.city &&
78+
currentUser.city.toLowerCase() === candidate.city.toLowerCase()
79+
) {
80+
score += 20;
81+
reasons.push("Meme ville");
82+
}
83+
84+
const diff = Math.abs(
85+
levelMap[currentUser.experience_level] -
86+
levelMap[candidate.experience_level]
87+
);
88+
if (diff <= 1) {
89+
score += 15;
90+
reasons.push("Niveau d'experience compatible");
91+
}
92+
93+
if (candidate.open_to_collaboration) {
94+
score += 10;
95+
}
96+
97+
if (score > 0) {
98+
acc.push({ profile: candidate, score: Math.min(score, 100), reasons });
99+
}
100+
101+
return acc;
102+
}, []);
103+
104+
return results.sort((a, b) => b.score - a.score);
98105
}
99106

100107
export function calculateProjectMatches(
@@ -105,58 +112,62 @@ export function calculateProjectMatches(
105112
// it for every single tech in every single project.
106113
const profileTechsLower = profile.tech_stack.map((t) => t.toLowerCase());
107114

108-
return projects
109-
.map((project) => {
110-
let score = 0;
111-
const reasons: string[] = [];
112-
113-
// ⚡ Bolt: Instead of iterating and lowercasing the project's entire tech stack
114-
// for *every* item in the profile's tech stack (O(M*K)), we pre-calculate a Set
115-
// of the lowercased project techs (O(M)) and do O(1) lookups.
116-
const projectTechsSet = new Set(project.tech_stack.map((pt) => pt.toLowerCase()));
117-
118-
const commonTechs = profileTechsLower.filter((tLower) =>
119-
projectTechsSet.has(tLower)
115+
// ⚡ Bolt: Consolidate map and filter into a single reduce pass
116+
const results = projects.reduce<ProjectMatch[]>((acc, project) => {
117+
let score = 0;
118+
const reasons: string[] = [];
119+
120+
// ⚡ Bolt: Instead of iterating and lowercasing the project's entire tech stack
121+
// for *every* item in the profile's tech stack (O(M*K)), we pre-calculate a Set
122+
// of the lowercased project techs (O(M)) and do O(1) lookups.
123+
const projectTechsSet = new Set(project.tech_stack.map((pt) => pt.toLowerCase()));
124+
125+
const commonTechs = profileTechsLower.filter((tLower) =>
126+
projectTechsSet.has(tLower)
127+
);
128+
129+
if (commonTechs.length > 0) {
130+
score += Math.min(commonTechs.length * 15, 40);
131+
reasons.push(`${commonTechs.length} techno(s) en commun`);
132+
}
133+
134+
const roleInStack = project.tech_stack.some((t) => {
135+
const tl = t.toLowerCase();
136+
const r = profile.role_type;
137+
return (
138+
(r === "frontend" && (tl.includes("react") || tl.includes("vue") || tl.includes("angular") || tl.includes("next") || tl.includes("svelte"))) ||
139+
(r === "mobile" && (tl.includes("flutter") || tl.includes("react native") || tl.includes("dart") || tl.includes("mobile"))) ||
140+
(r === "backend" && (tl.includes("node") || tl.includes("python") || tl.includes("django") || tl.includes("php") || tl.includes("java") || tl.includes("go") || tl.includes("rust"))) ||
141+
(r === "fullstack" && (tl.includes("react") || tl.includes("node") || tl.includes("next") || tl.includes("supabase") || tl.includes("typescript"))) ||
142+
(r === "data" && (tl.includes("python") || tl.includes("sql") || tl.includes("spark") || tl.includes("data"))) ||
143+
(r === "devops" && (tl.includes("docker") || tl.includes("kubernetes") || tl.includes("aws") || tl.includes("linux"))) ||
144+
(r === "cybersecurite" && (tl.includes("cyber") || tl.includes("linux") || tl.includes("securite") || tl.includes("reseaux"))) ||
145+
(r === "design" && (tl.includes("figma") || tl.includes("design") || tl.includes("ui")))
120146
);
121-
122-
if (commonTechs.length > 0) {
123-
score += Math.min(commonTechs.length * 15, 40);
124-
reasons.push(`${commonTechs.length} techno(s) en commun`);
125-
}
126-
127-
const roleInStack = project.tech_stack.some((t) => {
128-
const tl = t.toLowerCase();
129-
const r = profile.role_type;
130-
return (
131-
(r === "frontend" && (tl.includes("react") || tl.includes("vue") || tl.includes("angular") || tl.includes("next") || tl.includes("svelte"))) ||
132-
(r === "mobile" && (tl.includes("flutter") || tl.includes("react native") || tl.includes("dart") || tl.includes("mobile"))) ||
133-
(r === "backend" && (tl.includes("node") || tl.includes("python") || tl.includes("django") || tl.includes("php") || tl.includes("java") || tl.includes("go") || tl.includes("rust"))) ||
134-
(r === "fullstack" && (tl.includes("react") || tl.includes("node") || tl.includes("next") || tl.includes("supabase") || tl.includes("typescript"))) ||
135-
(r === "data" && (tl.includes("python") || tl.includes("sql") || tl.includes("spark") || tl.includes("data"))) ||
136-
(r === "devops" && (tl.includes("docker") || tl.includes("kubernetes") || tl.includes("aws") || tl.includes("linux"))) ||
137-
(r === "cybersecurite" && (tl.includes("cyber") || tl.includes("linux") || tl.includes("securite") || tl.includes("reseaux"))) ||
138-
(r === "design" && (tl.includes("figma") || tl.includes("design") || tl.includes("ui")))
139-
);
140-
});
141-
if (roleInStack) {
142-
score += 25;
143-
reasons.push("Votre rôle correspond au projet");
144-
}
145-
146-
if (project.open_to_collaboration && profile.open_to_collaboration) {
147-
score += 20;
148-
reasons.push("Les deux parties disponibles");
149-
}
150-
151-
if (profile.experience_level === "senior") {
152-
score += 10;
153-
reasons.push("Profil senior valorisé");
154-
} else if (profile.experience_level === "mid") {
155-
score += 5;
156-
}
157-
158-
return { project, score: Math.min(score, 100), reasons };
159-
})
160-
.filter((m) => m.score > 0)
161-
.sort((a, b) => b.score - a.score);
147+
});
148+
if (roleInStack) {
149+
score += 25;
150+
reasons.push("Votre rôle correspond au projet");
151+
}
152+
153+
if (project.open_to_collaboration && profile.open_to_collaboration) {
154+
score += 20;
155+
reasons.push("Les deux parties disponibles");
156+
}
157+
158+
if (profile.experience_level === "senior") {
159+
score += 10;
160+
reasons.push("Profil senior valorisé");
161+
} else if (profile.experience_level === "mid") {
162+
score += 5;
163+
}
164+
165+
if (score > 0) {
166+
acc.push({ project, score: Math.min(score, 100), reasons });
167+
}
168+
169+
return acc;
170+
}, []);
171+
172+
return results.sort((a, b) => b.score - a.score);
162173
}

0 commit comments

Comments
 (0)