From 79cbd22dc7f953c7838470c41dd73654b731a69f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:34:35 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20matching=20algor?= =?UTF-8?q?ithms=20by=20consolidating=20array=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored `calculateMatches` and `calculateProjectMatches` to replace chained `.filter().map().filter()` array operations with single `for` loops. This prevents redundant O(n) passes over the data and avoids allocating temporary intermediate arrays for better performance during data aggregation. Co-authored-by: KxlSys <116387953+KxlSys@users.noreply.github.com> --- .jules/bolt.md | 3 + src/lib/matching.ts | 217 +++++++++++++++++++++++--------------------- 2 files changed, 117 insertions(+), 103 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 22725eb..57d9129 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -21,3 +21,6 @@ ## 2024-07-10 - O(n) Single Pass Data Aggregation **Learning:** Using chained array methods (e.g. `.map().filter()`, `.flatMap()`, and repeated `.filter()` over the same large list like `profiles`) leads to redundant O(n) passes over data and allocates unnecessary intermediate arrays. This blocks the main thread longer than necessary when calculating basic statistics like `uniqueCities`, `uniqueTechs`, or total counts. **Action:** Consolidate statistical aggregations over large object arrays into a single O(n) `for` loop that updates multiple accumulators (like `Set` objects and primitive counters) in one pass, thereby improving performance and reducing memory overhead. +## 2024-07-20 - Consolidate array operations in matching algorithms +**Learning:** Chaining multiple array methods (`.map().filter()`, `.flatMap()`) creates unnecessary intermediate arrays and O(N) passes over the data. +**Action:** Use a single `reduce` or `for` loop with multiple accumulators to filter and transform the data simultaneously before sorting. This improves performance for data aggregation, particularly on large arrays like projects or candidates. diff --git a/src/lib/matching.ts b/src/lib/matching.ts index 1c8b694..6bb9732 100644 --- a/src/lib/matching.ts +++ b/src/lib/matching.ts @@ -46,55 +46,62 @@ export function calculateMatches( ): MatchResult[] { // ⚡ Bolt: Pre-calculate the current user's tech stack as a Set to avoid O(N*M) lookups const currentUserTechsSet = new Set(currentUser.tech_stack); - - return candidates - .filter((c) => c.id !== currentUser.id && c.open_to_collaboration) - .map((candidate) => { - let score = 0; - const reasons: string[] = []; - - const complementary = COMPLEMENTARY_ROLES[currentUser.role_type] || []; - if (complementary.includes(candidate.role_type)) { - score += 30; - reasons.push("Competences complementaires"); - } - - // ⚡ Bolt: Fast Set lookup instead of array.includes() for nested tech stack matching - const commonTechs = candidate.tech_stack.filter((t) => - currentUserTechsSet.has(t) - ); - if (commonTechs.length > 0) { - score += Math.min(commonTechs.length * 10, 25); - reasons.push(`${commonTechs.length} technologie(s) en commun`); - } - - if ( - currentUser.city && - candidate.city && - currentUser.city.toLowerCase() === candidate.city.toLowerCase() - ) { - score += 20; - reasons.push("Meme ville"); - } - - const levelMap = { junior: 1, mid: 2, senior: 3 }; - const diff = Math.abs( - levelMap[currentUser.experience_level] - - levelMap[candidate.experience_level] - ); - if (diff <= 1) { - score += 15; - reasons.push("Niveau d'experience compatible"); - } - - if (candidate.open_to_collaboration) { - score += 10; - } - - return { profile: candidate, score: Math.min(score, 100), reasons }; - }) - .filter((m) => m.score > 0) - .sort((a, b) => b.score - a.score); + const results: MatchResult[] = []; + + // ⚡ Bolt: Consolidate .filter().map().filter() into a single O(n) loop to reduce allocations + for (let i = 0; i < candidates.length; i++) { + const candidate = candidates[i]; + if (candidate.id === currentUser.id || !candidate.open_to_collaboration) { + continue; + } + + let score = 0; + const reasons: string[] = []; + + const complementary = COMPLEMENTARY_ROLES[currentUser.role_type] || []; + if (complementary.includes(candidate.role_type)) { + score += 30; + reasons.push("Competences complementaires"); + } + + // ⚡ Bolt: Fast Set lookup instead of array.includes() for nested tech stack matching + const commonTechs = candidate.tech_stack.filter((t) => + currentUserTechsSet.has(t) + ); + if (commonTechs.length > 0) { + score += Math.min(commonTechs.length * 10, 25); + reasons.push(`${commonTechs.length} technologie(s) en commun`); + } + + if ( + currentUser.city && + candidate.city && + currentUser.city.toLowerCase() === candidate.city.toLowerCase() + ) { + score += 20; + reasons.push("Meme ville"); + } + + const levelMap = { junior: 1, mid: 2, senior: 3 }; + const diff = Math.abs( + levelMap[currentUser.experience_level] - + levelMap[candidate.experience_level] + ); + if (diff <= 1) { + score += 15; + reasons.push("Niveau d'experience compatible"); + } + + if (candidate.open_to_collaboration) { + score += 10; + } + + if (score > 0) { + results.push({ profile: candidate, score: Math.min(score, 100), reasons }); + } + } + + return results.sort((a, b) => b.score - a.score); } export function calculateProjectMatches( @@ -104,59 +111,63 @@ export function calculateProjectMatches( // ⚡ Bolt: Pre-calculate the lowercase tech stack of the profile to avoid re-lowercasing // it for every single tech in every single project. const profileTechsLower = profile.tech_stack.map((t) => t.toLowerCase()); - - return projects - .map((project) => { - let score = 0; - const reasons: string[] = []; - - // ⚡ Bolt: Instead of iterating and lowercasing the project's entire tech stack - // for *every* item in the profile's tech stack (O(M*K)), we pre-calculate a Set - // of the lowercased project techs (O(M)) and do O(1) lookups. - const projectTechsSet = new Set(project.tech_stack.map((pt) => pt.toLowerCase())); - - const commonTechs = profileTechsLower.filter((tLower) => - projectTechsSet.has(tLower) + const results: ProjectMatch[] = []; + + // ⚡ Bolt: Consolidate .map().filter() into a single O(n) loop to reduce intermediate allocations + for (let i = 0; i < projects.length; i++) { + const project = projects[i]; + let score = 0; + const reasons: string[] = []; + + // ⚡ Bolt: Instead of iterating and lowercasing the project's entire tech stack + // for *every* item in the profile's tech stack (O(M*K)), we pre-calculate a Set + // of the lowercased project techs (O(M)) and do O(1) lookups. + const projectTechsSet = new Set(project.tech_stack.map((pt) => pt.toLowerCase())); + + const commonTechs = profileTechsLower.filter((tLower) => + projectTechsSet.has(tLower) + ); + + if (commonTechs.length > 0) { + score += Math.min(commonTechs.length * 15, 40); + reasons.push(`${commonTechs.length} techno(s) en commun`); + } + + const roleInStack = project.tech_stack.some((t) => { + const tl = t.toLowerCase(); + const r = profile.role_type; + return ( + (r === "frontend" && (tl.includes("react") || tl.includes("vue") || tl.includes("angular") || tl.includes("next") || tl.includes("svelte"))) || + (r === "mobile" && (tl.includes("flutter") || tl.includes("react native") || tl.includes("dart") || tl.includes("mobile"))) || + (r === "backend" && (tl.includes("node") || tl.includes("python") || tl.includes("django") || tl.includes("php") || tl.includes("java") || tl.includes("go") || tl.includes("rust"))) || + (r === "fullstack" && (tl.includes("react") || tl.includes("node") || tl.includes("next") || tl.includes("supabase") || tl.includes("typescript"))) || + (r === "data" && (tl.includes("python") || tl.includes("sql") || tl.includes("spark") || tl.includes("data"))) || + (r === "devops" && (tl.includes("docker") || tl.includes("kubernetes") || tl.includes("aws") || tl.includes("linux"))) || + (r === "cybersecurite" && (tl.includes("cyber") || tl.includes("linux") || tl.includes("securite") || tl.includes("reseaux"))) || + (r === "design" && (tl.includes("figma") || tl.includes("design") || tl.includes("ui"))) ); - - if (commonTechs.length > 0) { - score += Math.min(commonTechs.length * 15, 40); - reasons.push(`${commonTechs.length} techno(s) en commun`); - } - - const roleInStack = project.tech_stack.some((t) => { - const tl = t.toLowerCase(); - const r = profile.role_type; - return ( - (r === "frontend" && (tl.includes("react") || tl.includes("vue") || tl.includes("angular") || tl.includes("next") || tl.includes("svelte"))) || - (r === "mobile" && (tl.includes("flutter") || tl.includes("react native") || tl.includes("dart") || tl.includes("mobile"))) || - (r === "backend" && (tl.includes("node") || tl.includes("python") || tl.includes("django") || tl.includes("php") || tl.includes("java") || tl.includes("go") || tl.includes("rust"))) || - (r === "fullstack" && (tl.includes("react") || tl.includes("node") || tl.includes("next") || tl.includes("supabase") || tl.includes("typescript"))) || - (r === "data" && (tl.includes("python") || tl.includes("sql") || tl.includes("spark") || tl.includes("data"))) || - (r === "devops" && (tl.includes("docker") || tl.includes("kubernetes") || tl.includes("aws") || tl.includes("linux"))) || - (r === "cybersecurite" && (tl.includes("cyber") || tl.includes("linux") || tl.includes("securite") || tl.includes("reseaux"))) || - (r === "design" && (tl.includes("figma") || tl.includes("design") || tl.includes("ui"))) - ); - }); - if (roleInStack) { - score += 25; - reasons.push("Votre rôle correspond au projet"); - } - - if (project.open_to_collaboration && profile.open_to_collaboration) { - score += 20; - reasons.push("Les deux parties disponibles"); - } - - if (profile.experience_level === "senior") { - score += 10; - reasons.push("Profil senior valorisé"); - } else if (profile.experience_level === "mid") { - score += 5; - } - - return { project, score: Math.min(score, 100), reasons }; - }) - .filter((m) => m.score > 0) - .sort((a, b) => b.score - a.score); + }); + if (roleInStack) { + score += 25; + reasons.push("Votre rôle correspond au projet"); + } + + if (project.open_to_collaboration && profile.open_to_collaboration) { + score += 20; + reasons.push("Les deux parties disponibles"); + } + + if (profile.experience_level === "senior") { + score += 10; + reasons.push("Profil senior valorisé"); + } else if (profile.experience_level === "mid") { + score += 5; + } + + if (score > 0) { + results.push({ project, score: Math.min(score, 100), reasons }); + } + } + + return results.sort((a, b) => b.score - a.score); }