diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.ts index 68ca7c4cc1d..a71c8790f3a 100644 --- a/public/app/features/alerting/unified/hooks/useFilteredRules.ts +++ b/public/app/features/alerting/unified/hooks/useFilteredRules.ts @@ -18,6 +18,10 @@ import { getRuleHealth, isAlertingRule, isGrafanaRulerRule, isPromRuleType } fro import { calculateGroupTotals, calculateRuleFilteredTotals, calculateRuleTotals } from './useCombinedRuleNamespaces'; import { useURLSearchParams } from './useURLSearchParams'; +// if the search term is longer than MAX_NEEDLE_SIZE we disable Levenshtein distance +const MAX_NEEDLE_SIZE = 25; +const INFO_THRESHOLD = Infinity; + export function useRulesFilter() { const [queryParams, updateQueryParams] = useURLSearchParams(); const searchQuery = queryParams.get('search') ?? ''; @@ -100,17 +104,6 @@ export const useFilteredRules = (namespaces: CombinedRuleNamespace[], filterStat }, [namespaces, filterState]); }; -// Options details can be found here https://github.com/leeoniya/uFuzzy#options -// The following configuration complies with Damerau-Levenshtein distance -// https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance -const ufuzzy = new uFuzzy({ - intraMode: 1, - intraIns: 1, - intraSub: 1, - intraTrn: 1, - intraDel: 1, -}); - export const filterRules = ( namespaces: CombinedRuleNamespace[], filterState: RulesFilter = { dataSourceNames: [], labels: [], freeFormWords: [] } @@ -129,7 +122,13 @@ export const filterRules = ( if (namespaceFilter) { const namespaceHaystack = filteredNamespaces.map((ns) => ns.name); - const [idxs, info, order] = ufuzzy.search(namespaceHaystack, namespaceFilter); + const ufuzzy = getSearchInstance(namespaceFilter); + const [idxs, info, order] = ufuzzy.search( + namespaceHaystack, + namespaceFilter, + getOutOfOrderLimit(namespaceFilter), + INFO_THRESHOLD + ); if (info && order) { filteredNamespaces = order.map((idx) => filteredNamespaces[info.idx[idx]]); } else if (idxs) { @@ -148,7 +147,14 @@ const reduceNamespaces = (filterState: RulesFilter) => { if (groupNameFilter) { const groupsHaystack = filteredGroups.map((g) => g.name); - const [idxs, info, order] = ufuzzy.search(groupsHaystack, groupNameFilter); + const ufuzzy = getSearchInstance(groupNameFilter); + + const [idxs, info, order] = ufuzzy.search( + groupsHaystack, + groupNameFilter, + getOutOfOrderLimit(groupNameFilter), + INFO_THRESHOLD + ); if (info && order) { filteredGroups = order.map((idx) => filteredGroups[info.idx[idx]]); } else if (idxs) { @@ -178,7 +184,14 @@ const reduceGroups = (filterState: RulesFilter) => { if (ruleNameQuery) { const rulesHaystack = filteredRules.map((r) => r.name); - const [idxs, info, order] = ufuzzy.search(rulesHaystack, ruleNameQuery); + const ufuzzy = getSearchInstance(ruleNameQuery); + + const [idxs, info, order] = ufuzzy.search( + rulesHaystack, + ruleNameQuery, + getOutOfOrderLimit(ruleNameQuery), + INFO_THRESHOLD + ); if (info && order) { filteredRules = order.map((idx) => filteredRules[info.idx[idx]]); } else if (idxs) { @@ -275,6 +288,15 @@ const reduceGroups = (filterState: RulesFilter) => { }; }; +// apply an outOfOrder limit which helps to limit the number of permutations to search for +// and prevents the browser from hanging +function getOutOfOrderLimit(searchTerm: string) { + const ufuzzy = getSearchInstance(searchTerm); + + const termCount = ufuzzy.split(searchTerm).length; + return termCount < 5 ? 4 : 0; +} + function looseParseMatcher(matcherQuery: string): Matcher | undefined { try { return parseMatcher(matcherQuery); @@ -284,6 +306,23 @@ function looseParseMatcher(matcherQuery: string): Matcher | undefined { } } +// determine which search instance to use, very long search terms should match without checking for Levenshtein distance +function getSearchInstance(searchTerm: string): uFuzzy { + const searchTermExeedsMaxNeedleSize = searchTerm.length > MAX_NEEDLE_SIZE; + + // Options details can be found here https://github.com/leeoniya/uFuzzy#options + // The following configuration complies with Damerau-Levenshtein distance + // https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance + return new uFuzzy({ + // we will disable Levenshtein distance for very long search terms – this will help with performance + // as it will avoid creating a very complex regular expression + intraMode: searchTermExeedsMaxNeedleSize ? 0 : 1, + // split search terms only on whitespace, this will significantly reduce the amount of regex permutations to test + // and is important for performance with large amount of rules and large needle + interSplit: '\\s+', + }); +} + const isQueryingDataSource = (rulerRule: RulerGrafanaRuleDTO, filterState: RulesFilter): boolean => { if (!filterState.dataSourceNames?.length) { return true;