settings: rank search results by page level and whole-word matches

This commit is contained in:
bbedward
2026-09-25 18:54:53 -04:00
parent 3b1b63c93f
commit 3247476368
3 changed files with 124 additions and 294 deletions
+95 -57
View File
@@ -341,7 +341,8 @@ Singleton {
runtimeType: t.runtimeType,
runtimeId: t.runtimeId,
page: t.page,
isTab: String(t.section).startsWith("_tab_"),
level: _entryLevel(t.section),
order: i,
labelSearch: labelLower,
categorySearch: categoryLower,
descriptionSearch: descriptionLower,
@@ -433,17 +434,81 @@ Singleton {
return out;
}
function _bestFieldScore(fields, queryLower, exactScore, prefixScore, includesScore) {
readonly property var labelScores: ({
"exact": 10000,
"plural": 9500,
"prefix": 5000,
"wordStart": 3000,
"includes": 1000
})
readonly property var squashScores: ({
"exact": 9000,
"plural": 8500,
"prefix": 4500,
"wordStart": 900,
"includes": 900
})
// V4 regex has no \p{} classes, so word breaks are whitespace and punctuation ranges.
readonly property var wordBreak: /[\s!-\/:-@\[-`{-~\u2000-\u206f\u3000-\u303f]/
function _entryLevel(section) {
const id = String(section);
if (id.startsWith("_hub_"))
return 2;
if (id.startsWith("_tab_"))
return 1;
return 0;
}
function _singular(value) {
if (value.length <= 3 || !value.endsWith("s") || value.endsWith("ss"))
return value;
return value.slice(0, -1);
}
function _startsWord(field, query) {
for (var at = field.indexOf(query, 1); at > 0; at = field.indexOf(query, at + 1)) {
if (wordBreak.test(field[at - 1]))
return true;
}
return false;
}
function _labelScore(labels, query, scores) {
var score = 0;
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
if (field === queryLower) {
score = Math.max(score, exactScore);
} else if (field.startsWith(queryLower)) {
score = Math.max(score, prefixScore);
} else if (field.includes(queryLower)) {
score = Math.max(score, includesScore);
}
for (var i = 0; i < labels.length; i++) {
var label = labels[i];
if (label === query)
return scores.exact;
if (_singular(label) === _singular(query))
score = Math.max(score, scores.plural);
else if (label.startsWith(query))
score = Math.max(score, scores.prefix);
else if (_startsWord(label, query))
score = Math.max(score, scores.wordStart);
else if (label.includes(query))
score = Math.max(score, scores.includes);
}
return score;
}
function _keywordScore(entry, query, querySquash) {
var score = 0;
for (const keyword of entry.keywords) {
if (keyword === query)
return 900;
if (keyword.startsWith(query))
score = Math.max(score, 800);
else if (keyword.includes(query))
score = Math.max(score, 400);
}
if (!querySquash)
return score;
for (const keyword of entry.keywordsSquash) {
if (keyword === querySquash)
return Math.max(score, 850);
if (keyword.startsWith(querySquash))
score = Math.max(score, 750);
}
return score;
}
@@ -465,10 +530,10 @@ Singleton {
}
function _searchEntries(text, maxResults) {
if (!text)
var queryLower = (text || "").toLowerCase().trim();
if (!queryLower)
return [];
var queryLower = text.toLowerCase().trim();
var querySquash = _squash(queryLower);
var queryWords = queryLower.split(/\s+/).filter(w => w.length > 0);
var scored = [];
@@ -480,46 +545,15 @@ Singleton {
if (!checkCondition(entry))
continue;
var labelScore = _bestFieldScore(entry.labelSearch, queryLower, 10000, 5000, 1000);
var labelScore = _labelScore(entry.labelSearch, queryLower, labelScores);
if (querySquash)
labelScore = Math.max(labelScore, _bestFieldScore(entry.labelSquash, querySquash, 9000, 4500, 900));
labelScore = Math.max(labelScore, _labelScore(entry.labelSquash, querySquash, squashScores));
var score = labelScore;
score = Math.max(score, _bestFieldScore(entry.categorySearch, queryLower, 500, 500, 500));
score = Math.max(score, _bestFieldScore(entry.descriptionSearch, queryLower, 250, 250, 250));
if (querySquash) {
score = Math.max(score, _bestFieldScore(entry.categorySquash, querySquash, 500, 500, 500));
score = Math.max(score, _bestFieldScore(entry.descriptionSquash, querySquash, 250, 250, 250));
}
if (score === 0) {
var keywords = entry.keywords;
for (var k = 0; k < keywords.length; k++) {
var keyword = keywords[k];
if (keyword === queryLower) {
score = 900;
break;
}
if (keyword.startsWith(queryLower)) {
score = Math.max(score, 800);
} else if (keyword.includes(queryLower) && score < 400) {
score = 400;
}
}
}
if (score === 0 && querySquash) {
var keywordsSquash = entry.keywordsSquash;
for (var ks = 0; ks < keywordsSquash.length; ks++) {
if (keywordsSquash[ks] === querySquash) {
score = Math.max(score, 850);
break;
}
if (keywordsSquash[ks].startsWith(querySquash)) {
score = Math.max(score, 750);
}
}
}
var score = Math.max(labelScore, _keywordScore(entry, queryLower, querySquash));
if (_fieldsContainWord(entry.categorySearch, queryLower) || (querySquash && _fieldsContainWord(entry.categorySquash, querySquash)))
score = Math.max(score, 500);
if (_fieldsContainWord(entry.descriptionSearch, queryLower) || (querySquash && _fieldsContainWord(entry.descriptionSquash, querySquash)))
score = Math.max(score, 250);
if (score === 0 && queryWords.length > 1) {
var allMatch = true;
@@ -551,23 +585,27 @@ Singleton {
scored.push({
item: entry,
score: score,
labelScore: labelScore
labelTier: labelScore >= labelScores.wordStart ? 2 : labelScore > 0 ? 1 : 0
});
}
}
scored.sort((a, b) => {
if (b.labelTier !== a.labelTier)
return b.labelTier - a.labelTier;
if (a.labelTier > 0 && b.item.level !== a.item.level)
return b.item.level - a.item.level;
if (b.score !== a.score)
return b.score - a.score;
if (b.item.level !== a.item.level)
return b.item.level - a.item.level;
const aRuntime = !!a.item.runtimeType;
const bRuntime = !!b.item.runtimeType;
if (aRuntime !== bRuntime)
return aRuntime ? 1 : -1;
if (b.labelScore !== a.labelScore)
return b.labelScore - a.labelScore;
if (a.item.isTab !== b.item.isTab)
return a.item.isTab ? 1 : -1;
return a.item.label.length - b.item.label.length;
if (a.item.label.length !== b.item.label.length)
return a.item.label.length - b.item.label.length;
return a.item.order - b.item.order;
});
return scored.slice(0, limit).map(s => s.item);
}
@@ -235,24 +235,31 @@ STOPWORDS = {
}
def alias_keywords(text):
aliases = set()
for term in sorted(ABBREVIATIONS, key=len, reverse=True):
pattern = rf"\b{re.escape(term)}(?:e?s)?\b"
if not re.search(pattern, text):
continue
aliases.update(ABBREVIATIONS[term])
if " " in term or "-" in term:
text = re.sub(pattern, " ", text)
return aliases
def enrich_keywords(label, description, category, existing_tags, parent_label=None):
keywords = set(existing_tags)
label_lower = label.lower()
label_words = re.split(r"[\s\-_&/]+", label_lower)
keywords.update(w for w in label_words if len(w) > 2)
for term, aliases in ABBREVIATIONS.items():
if term in label_lower:
keywords.update(aliases)
keywords.update(alias_keywords(label_lower))
if description:
desc_lower = description.lower()
desc_words = re.split(r"[\s\-_&/,.]+", desc_lower)
keywords.update(w for w in desc_words if len(w) > 3 and w.isalpha())
for term, aliases in ABBREVIATIONS.items():
if term in desc_lower:
keywords.update(aliases)
keywords.update(alias_keywords(desc_lower))
for name in (category, parent_label):
if not name:
@@ -260,9 +267,7 @@ def enrich_keywords(label, description, category, existing_tags, parent_label=No
keywords.update(CATEGORY_KEYWORDS.get(name, []))
name_lower = name.lower()
keywords.update(w for w in re.split(r"[\s\-_&/]+", name_lower) if len(w) > 2)
for term, aliases in ABBREVIATIONS.items():
if term in name_lower:
keywords.update(aliases)
keywords.update(alias_keywords(name_lower))
keywords = {k for k in keywords if k not in STOPWORDS and len(k) > 1}
return sorted(keywords)
@@ -597,6 +602,8 @@ def generate_hub_entries(hubs):
"keywords": sorted(k for k in keywords if k not in STOPWORDS),
"icon": hub["icon"],
}
if hub["hint"]:
entry["description"] = hub["hint"]
if hub["conditionKey"]:
entry["conditionKey"] = hub["conditionKey"]
entries.append(entry)
@@ -614,7 +621,7 @@ def generate_tab_entries(leaves, settings_entries):
entries = []
for leaf in leaves:
base_label = leaf["label"]
if not base_label:
if not base_label or leaf["children"]:
continue
label = (
f"{leaf['parentLabel']}: {base_label}"
@@ -300,7 +300,8 @@
"taskbar",
"topbar"
],
"icon": "toolbar"
"icon": "toolbar",
"description": "Layout, position, appearance"
},
{
"section": "barConfigurations",
@@ -931,7 +932,8 @@
"visible",
"widgets"
],
"icon": "dock_to_bottom"
"icon": "dock_to_bottom",
"description": "Visibility, position, pinned apps, trash"
},
{
"section": "dockConfiguration",
@@ -2455,27 +2457,6 @@
],
"icon": "palette"
},
{
"section": "_tab_0",
"label": "Wallpaper & colors",
"tabIndex": 0,
"category": "Wallpaper & colors",
"keywords": [
"background",
"bg",
"colors",
"colour",
"colours",
"desktop",
"hue",
"image",
"palette",
"picture",
"tint",
"wallpaper"
],
"icon": "palette"
},
{
"section": "clockDateFormat",
"label": "Bar format",
@@ -3908,9 +3889,7 @@
"panel",
"screen",
"screens",
"statusbar",
"taskbar",
"topbar"
"taskbar"
],
"icon": "tune",
"parentLabel": "Dock",
@@ -5370,13 +5349,11 @@
"app menu",
"applications",
"background",
"dark mode",
"darken",
"drawer",
"launcher",
"menu",
"modal",
"night",
"overlay",
"start",
"start menu"
@@ -5882,10 +5859,8 @@
"applications",
"bar",
"chips",
"day",
"drawer",
"launcher",
"light mode",
"menu",
"panel",
"shortcut",
@@ -7031,24 +7006,6 @@
"registries",
"widgets"
],
"icon": "extension"
},
{
"section": "_tab_12",
"label": "Plugins",
"tabIndex": 12,
"category": "Plugins",
"keywords": [
"addon",
"addons",
"browse",
"extend",
"extensions",
"install",
"plugins",
"registries",
"widgets"
],
"icon": "extension",
"description": "Browse, install, registries"
},
@@ -7883,26 +7840,6 @@
"rules",
"toast"
],
"icon": "notifications"
},
{
"section": "_tab_17",
"label": "Notifications",
"tabIndex": 17,
"category": "Notifications",
"keywords": [
"alert",
"alerts",
"disturb",
"dnd",
"messages",
"notif",
"notifications",
"notifs",
"popups",
"rules",
"toast"
],
"icon": "notifications",
"description": "Popups, do not disturb, rules"
},
@@ -7915,15 +7852,10 @@
"always",
"displays",
"indicator",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"percentage",
"popup",
"screen",
"screens",
"show"
]
},
@@ -7936,14 +7868,10 @@
"audio",
"displays",
"indicator",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"popup",
"screen",
"screens",
"switch"
]
},
@@ -7959,14 +7887,9 @@
"dim",
"displays",
"indicator",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"popup",
"screen",
"screens"
"screen"
]
},
{
@@ -7979,14 +7902,9 @@
"displays",
"indicator",
"lock",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"popup",
"screen",
"screens"
"screen"
]
},
{
@@ -8001,14 +7919,9 @@
"inactive",
"indicator",
"inhibitor",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"popup",
"screen",
"screens",
"screensaver",
"timeout"
]
@@ -8022,15 +7935,10 @@
"displays",
"indicator",
"media",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"playback",
"popup",
"screen",
"screens"
"screen"
]
},
{
@@ -8044,14 +7952,9 @@
"indicator",
"loudness",
"media",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"popup",
"screen",
"screens",
"sound",
"speaker",
"volume"
@@ -8066,15 +7969,10 @@
"displays",
"indicator",
"microphone",
"monitor",
"monitors",
"mute",
"osd",
"output",
"outputs",
"popup",
"screen",
"screens"
"screen"
]
},
{
@@ -8088,14 +7986,9 @@
"indicator",
"loudness",
"microphone",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"popup",
"screen",
"screens",
"sound",
"speaker",
"volume"
@@ -8117,15 +8010,10 @@
"indicator",
"lock",
"loudness",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"popup",
"position",
"screen",
"screens",
"sound",
"speaker",
"volume"
@@ -8141,15 +8029,10 @@
"keywords": [
"displays",
"indicator",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"popup",
"position",
"screen",
"screens"
"screen"
]
},
{
@@ -8160,15 +8043,10 @@
"keywords": [
"displays",
"indicator",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"popup",
"position",
"screen",
"screens"
"screen"
]
},
{
@@ -8180,18 +8058,13 @@
"displays",
"hibernate",
"indicator",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"popup",
"power",
"profile",
"reboot",
"restart",
"screen",
"screens",
"shutdown",
"sleep",
"suspend"
@@ -8205,14 +8078,9 @@
"keywords": [
"displays",
"indicator",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"popup",
"screen",
"screens",
"show"
]
},
@@ -8226,14 +8094,9 @@
"displays",
"indicator",
"loudness",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"popup",
"screen",
"screens",
"sound",
"speaker",
"volume"
@@ -8248,14 +8111,9 @@
"desktop",
"displays",
"indicator",
"monitor",
"monitors",
"osd",
"output",
"outputs",
"popup",
"screen",
"screens",
"switch",
"virtual",
"workspace"
@@ -9081,26 +8939,6 @@
"widget",
"widgets"
],
"icon": "widgets"
},
{
"section": "_tab_22",
"label": "Bar widgets",
"tabIndex": 22,
"category": "Bar widgets",
"keywords": [
"bar",
"components",
"configure",
"modules",
"panel",
"remove",
"reorder",
"statusbar",
"taskbar",
"topbar",
"widgets"
],
"icon": "widgets",
"description": "Add, remove, reorder and configure"
},
@@ -10490,29 +10328,6 @@
"widget",
"widgets"
],
"icon": "widgets"
},
{
"section": "_tab_27",
"label": "Desktop widgets",
"tabIndex": 27,
"category": "Desktop widgets",
"keywords": [
"addons",
"clocks",
"components",
"conky",
"desktop",
"desktop clock",
"extensions",
"modules",
"monitors",
"plugins",
"system",
"time",
"watch",
"widgets"
],
"icon": "widgets",
"description": "Clocks, system monitors, plugins"
},
@@ -13405,25 +13220,6 @@
"shadows",
"style"
],
"icon": "layers"
},
{
"section": "_tab_48",
"label": "Interface style",
"tabIndex": 48,
"category": "Interface style",
"keywords": [
"blur",
"borders",
"corners",
"interface",
"opacity",
"radius",
"round",
"rounded",
"shadows",
"style"
],
"icon": "layers",
"description": "Opacity, borders, blur, shadows, corners"
},
@@ -15564,17 +15360,6 @@
],
"icon": "account_circle"
},
{
"section": "_tab_60",
"label": "Users & accounts",
"tabIndex": 60,
"category": "Users & accounts",
"keywords": [
"accounts",
"users"
],
"icon": "account_circle"
},
{
"section": "createUserGreeter",
"label": "Allow greeter login access",