From 97515989a2e535a9be5d19e75fbe33fd4a5648d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Homb?= Date: Thu, 30 Oct 2025 14:13:26 +0100 Subject: [PATCH] feat: Improve node search with fuzzy matching and ranking (#5370) * Improve node search with fuzzy matching and ranking * PR changes --- packages/ui/src/views/canvas/AddNodes.jsx | 116 +++++++++++++++++++--- 1 file changed, 102 insertions(+), 14 deletions(-) diff --git a/packages/ui/src/views/canvas/AddNodes.jsx b/packages/ui/src/views/canvas/AddNodes.jsx index 35ccd81f..903747a0 100644 --- a/packages/ui/src/views/canvas/AddNodes.jsx +++ b/packages/ui/src/views/canvas/AddNodes.jsx @@ -116,17 +116,111 @@ const AddNodes = ({ nodesData, node, isAgentCanvas, isAgentflowv2, onFlowGenerat return nodes } + // Fuzzy search utility function that calculates similarity score + const fuzzyScore = (searchTerm, text) => { + const search = ((searchTerm ?? '') + '').trim().toLowerCase() + if (!search) return 0 + const target = ((text ?? '') + '').toLowerCase() + + let score = 0 + let searchIndex = 0 + let firstMatchIndex = -1 + let lastMatchIndex = -1 + let consecutiveMatches = 0 + + // Check for exact substring match + const exactMatchIndex = target.indexOf(search) + if (exactMatchIndex !== -1) { + score = 1000 + // Bonus for match at start of string + if (exactMatchIndex === 0) { + score += 200 + } + // Bonus for match at start of word + else if (target[exactMatchIndex - 1] === ' ' || target[exactMatchIndex - 1] === '-' || target[exactMatchIndex - 1] === '_') { + score += 100 + } + // Penalty for how far into the string the match is + score -= exactMatchIndex * 2 + // Penalty for length difference (shorter target = better match) + score -= (target.length - search.length) * 3 + return score + } + + // Fuzzy matching with character-by-character scoring + for (let i = 0; i < target.length && searchIndex < search.length; i++) { + if (target[i] === search[searchIndex]) { + // Base score for character match + score += 10 + + // Bonus for consecutive matches + if (lastMatchIndex === i - 1) { + consecutiveMatches++ + score += 5 + consecutiveMatches * 2 // Increasing bonus for longer sequences + } else { + consecutiveMatches = 0 + } + + // Bonus for match at start of string + if (i === 0) { + score += 20 + } + + // Bonus for match after space or special character (word boundary) + if (i > 0 && (target[i - 1] === ' ' || target[i - 1] === '-' || target[i - 1] === '_')) { + score += 15 + } + + if (firstMatchIndex === -1) firstMatchIndex = i + lastMatchIndex = i + searchIndex++ + } + } + + // Return 0 if not all characters were matched + if (searchIndex < search.length) { + return 0 + } + + // Penalty for length difference (favor shorter targets) + score -= Math.max(0, target.length - search.length) * 2 + // Penalty for gaps between first/last matched span + const span = lastMatchIndex - firstMatchIndex + 1 + const gaps = Math.max(0, span - search.length) + score -= gaps * 3 + + return score + } + + // Score and sort nodes by fuzzy search relevance + const scoreAndSortNodes = (nodes, searchValue) => { + // Return all nodes unsorted if search is empty + if (!searchValue || searchValue.trim() === '') { + return nodes + } + + // Calculate fuzzy scores for each node + const nodesWithScores = nodes.map((nd) => { + const nameScore = fuzzyScore(searchValue, nd.name) + const labelScore = fuzzyScore(searchValue, nd.label) + const categoryScore = fuzzyScore(searchValue, nd.category) * 0.5 // Lower weight for category + const maxScore = Math.max(nameScore, labelScore, categoryScore) + + return { node: nd, score: maxScore } + }) + + // Filter nodes with score > 0 and sort by score (highest first) + return nodesWithScores + .filter((item) => item.score > 0) + .sort((a, b) => b.score - a.score) + .map((item) => item.node) + } + const getSearchedNodes = (value) => { if (isAgentCanvas) { const nodes = nodesData.filter((nd) => !blacklistCategoriesForAgentCanvas.includes(nd.category)) nodes.push(...addException()) - const passed = nodes.filter((nd) => { - const passesName = nd.name.toLowerCase().includes(value.toLowerCase()) - const passesLabel = nd.label.toLowerCase().includes(value.toLowerCase()) - const passesCategory = nd.category.toLowerCase().includes(value.toLowerCase()) - return passesName || passesCategory || passesLabel - }) - return passed + return scoreAndSortNodes(nodes, value) } let nodes = nodesData.filter((nd) => nd.category !== 'Multi Agents' && nd.category !== 'Sequential Agents') @@ -135,13 +229,7 @@ const AddNodes = ({ nodesData, node, isAgentCanvas, isAgentflowv2, onFlowGenerat nodes = nodes.filter((nd) => !nodeNames.includes(nd.name)) } - const passed = nodes.filter((nd) => { - const passesName = nd.name.toLowerCase().includes(value.toLowerCase()) - const passesLabel = nd.label.toLowerCase().includes(value.toLowerCase()) - const passesCategory = nd.category.toLowerCase().includes(value.toLowerCase()) - return passesName || passesCategory || passesLabel - }) - return passed + return scoreAndSortNodes(nodes, value) } const filterSearch = (value, newTabValue) => {