feat: Improve node search with fuzzy matching and ranking (#5370)

* Improve node search with fuzzy matching and ranking

* PR changes
This commit is contained in:
Håvard Homb
2025-10-30 14:13:26 +01:00
committed by GitHub
parent 601de76aea
commit 97515989a2
+102 -14
View File
@@ -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) => {