| 341 | * Multi-word queries also check individual term matches against the name. |
| 342 | */ |
| 343 | export function nameMatchBonus(nodeName: string, query: string): number { |
| 344 | const nameLower = nodeName.toLowerCase(); |
| 345 | |
| 346 | // Split query into word-level terms (handles "CacheBuilder build" → ["cache","builder","build"]) |
| 347 | const rawTerms = query |
| 348 | .replace(/([a-z])([A-Z])/g, '$1 $2') |
| 349 | .split(/[\s_.\-]+/) |
| 350 | .map(t => t.toLowerCase()) |
| 351 | .filter(t => t.length >= 2); |
| 352 | |
| 353 | // Also keep original space-separated tokens for exact-term matching |
| 354 | const queryTokens = query.split(/\s+/).map(t => t.toLowerCase()).filter(t => t.length >= 2); |
| 355 | |
| 356 | // Full query as a single token (for compound identifiers like "CacheBuilder") |
| 357 | const queryLower = query.replace(/[\s]+/g, '').toLowerCase(); |
| 358 | |
| 359 | // Exact match: query exactly equals the node name |
| 360 | if (nameLower === queryLower) return 80; |
| 361 | |
| 362 | // Exact match on a query token: "CacheBuilder build" and node name is "build" |
| 363 | if (queryTokens.length > 1 && queryTokens.includes(nameLower)) return 60; |
| 364 | |
| 365 | // Name starts with query — scale by length ratio so "Pod"→"Pod" (exact, handled above) |
| 366 | // scores much higher than "Pod"→"PodGCControllerOptions" (ratio 0.125). |
| 367 | if (nameLower.startsWith(queryLower)) { |
| 368 | const ratio = queryLower.length / nameLower.length; |
| 369 | return Math.round(10 + 30 * ratio); |
| 370 | } |
| 371 | |
| 372 | // All camelCase-split terms appear in the name |
| 373 | if (rawTerms.length > 1) { |
| 374 | const allMatch = rawTerms.every(t => nameLower.includes(t)); |
| 375 | if (allMatch) return 15; |
| 376 | } |
| 377 | |
| 378 | // Name contains the full query as substring |
| 379 | if (nameLower.includes(queryLower)) return 10; |
| 380 | |
| 381 | return 0; |
| 382 | } |
| 383 | |
| 384 | /** |
| 385 | * Kind-based bonus for search ranking |