From 0e8d1e6c22bd861d97bab35633ee686a7942067c Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 27 Aug 2026 18:52:25 -0400 Subject: [PATCH] refactor: pf-4402 search results grouping --- src/patternFly.search.ts | 58 +++++++++++++++++++++++++++++++++++- src/tool.searchPatternFly.ts | 23 ++++++++++++-- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/patternFly.search.ts b/src/patternFly.search.ts index 1ccf5089..d4e70f20 100644 --- a/src/patternFly.search.ts +++ b/src/patternFly.search.ts @@ -170,6 +170,55 @@ type FilterPatternFlyMemoArgs = [ settings?: FilterPatternFlySettings | undefined ]; +/** + * Rate how closely a search result matches a search query. + * + * @param {SearchPatternFlyResult} result - Result object containing name and display name props. + * @param normalizedQuery - Query string for comparison. + * @returns `result` relevance to a `normalizedQuery`: + * - `0`: Exact match with name or entity slug. + * - `1`: Exact match with display name. + * - `2`: Prefix match with entity name. + * - `3`: Match display name at a word boundary. + * - `4`: Name substring match. + * - `5`: Match solely by descriptive content or keywords. + */ +const calculateRelevance = ( + result: SearchPatternFlyResult, + normalizedQuery: string +): number => { + const resultName = (result.name || '').toLowerCase(); + const displayName = 'displayName' in result ? (result.displayName as string).toLowerCase() : undefined; + + // Exact name / entity match + if (resultName === normalizedQuery) { + return 0; + } + + // Exact display name match + if (displayName && displayName === normalizedQuery) { + return 1; + } + + // Prefix match + if (resultName.startsWith(`${normalizedQuery}-`) || resultName.startsWith(normalizedQuery)) { + return 2; + } + + // Word boundary match in display name + if (displayName && new RegExp(`\\b${normalizedQuery}\\b`, 'i').test(displayName)) { + return 3; + } + + // Substring match in name + if (resultName.includes(normalizedQuery)) { + return 4; + } + + // Tier 5: Matched solely via prose keyword index / description + return 5; +}; + /** * Apply sequenced priority filters for predictable filtering, filter PatternFly data. * @@ -616,7 +665,14 @@ const searchPatternFly = async (searchQuery: unknown, filters?: FilterPatternFly return a.distance - b.distance; } - return a.name.localeCompare(b.name); + const relevantA = calculateRelevance(a, coercedSearchQuery.toLowerCase()); + const relevantB = calculateRelevance(b, coercedSearchQuery.toLowerCase()); + + if (relevantA !== relevantB) { + return relevantA - relevantB; + } + + return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }); }; const sortedExactMatches = exactMatches.sort(sortByDistanceByName); diff --git a/src/tool.searchPatternFly.ts b/src/tool.searchPatternFly.ts index aa6982d9..fdedb55d 100644 --- a/src/tool.searchPatternFly.ts +++ b/src/tool.searchPatternFly.ts @@ -89,10 +89,15 @@ const searchPatternFlyTool = (options = getOptions()): McpTool => { const results = new Map>(); const groupSortNames = new Map(); + const groupOrder = new Map(); let numberCollections = 0; let numberRecords = 0; - parseResults.forEach(result => { + parseResults.forEach((result, index) => { + if (!groupOrder.has(result.groupId)) { + groupOrder.set(result.groupId, index); + } + if (results.has(result.groupId)) { return; } @@ -164,8 +169,22 @@ const searchPatternFlyTool = (options = getOptions()): McpTool => { const gidA = a.groupId as string; const gidB = b.groupId as string; - // 1. Sort by Group (using the Collection's name) + // 1. Sort by Group (Relevance order from search results, or alphabetical for wildcard all) if (gidA !== gidB) { + if (isSearchWildCardAll) { + const nameA = groupSortNames.get(gidA) || ''; + const nameB = groupSortNames.get(gidB) || ''; + + return nameA.localeCompare(nameB); + } + + const orderA = groupOrder.get(gidA) ?? Number.MAX_SAFE_INTEGER; + const orderB = groupOrder.get(gidB) ?? Number.MAX_SAFE_INTEGER; + + if (orderA !== orderB) { + return orderA - orderB; + } + const nameA = groupSortNames.get(gidA) || ''; const nameB = groupSortNames.get(gidB) || '';