Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion src/patternFly.search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
Expand Down
23 changes: 21 additions & 2 deletions src/tool.searchPatternFly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,15 @@ const searchPatternFlyTool = (options = getOptions()): McpTool => {

const results = new Map<string, Record<string, unknown>>();
const groupSortNames = new Map<string, string>();
const groupOrder = new Map<string, number>();
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;
}
Expand Down Expand Up @@ -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) || '';

Expand Down
Loading