DEV: Improve site setting search ranking (#32461)

This change does a couple of things.

Change in behaviour:

- Matches on the site setting name and keywords now get a higher ranking in search results.
- When counting gaps for ranking fuzzy matches, we no consider the smallest number of gaps.

Code organization:

- Extract a separate SiteSettingMatcher object for the matching logic.
- Flatten some conditionals to make control flow clearer.
- Higher weight now means higher in search results.
This commit is contained in:
Ted Johansson
2025-04-28 16:58:27 +08:00
committed by GitHub
parent 77bc84a3c7
commit b056f9538b
2 changed files with 123 additions and 54 deletions
@@ -1,5 +1,6 @@
import { bind } from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
import SiteSettingMatcher from "admin/lib/site-setting-matcher";
export default class SiteSettingFilter {
constructor(siteSettings) {
@@ -53,62 +54,55 @@ export default class SiteSettingFilter {
matchesGroupedByCategory.push(all);
}
const strippedQuery = filter.replace(/[^a-z0-9]/gi, "");
let fuzzyRegex;
let fuzzyRegexGaps;
if (strippedQuery.length > 2) {
fuzzyRegex = new RegExp(strippedQuery.split("").join(".*"), "i");
fuzzyRegexGaps = new RegExp(strippedQuery.split("").join("(.*)"), "i");
}
this.siteSettings.forEach((settingsCategory) => {
let fuzzyMatches = [];
const siteSettings = settingsCategory.siteSettings.filter((item) => {
if (opts.onlyOverridden && !item.get("overridden")) {
return false;
}
if (pluginFilter && item.plugin !== pluginFilter) {
return false;
}
if (filter) {
const setting = item.get("setting").toLowerCase();
let filterResult =
setting.includes(filter) ||
setting.replace(/_/g, " ").includes(filter) ||
item.get("description").toLowerCase().includes(filter) ||
(item.get("keywords") || []).any((keyword) =>
keyword
.replace(/_/g, " ")
.toLowerCase()
.includes(filter.replace(/_/g, " "))
) ||
(item.get("value") || "").toString().toLowerCase().includes(filter);
if (!filterResult && fuzzyRegex && fuzzyRegex.test(setting)) {
// Tightens up fuzzy search results a bit.
const fuzzySearchLimiter = 25;
const strippedSetting = setting.replace(/[^a-z0-9]/gi, "");
if (
strippedSetting.length <=
strippedQuery.length + fuzzySearchLimiter
) {
const gapResult = strippedSetting.match(fuzzyRegexGaps);
if (gapResult) {
item.weight = gapResult.filter((gap) => gap !== "").length;
}
fuzzyMatches.push(item);
}
}
return filterResult;
} else {
return true;
}
});
const siteSettings = settingsCategory.siteSettings.filter(
(siteSetting) => {
siteSetting.weight = 0;
if (fuzzyMatches.length > 0) {
siteSettings.pushObjects(fuzzyMatches);
}
if (opts.onlyOverridden && !siteSetting.get("overridden")) {
return false;
}
if (pluginFilter && siteSetting.plugin !== pluginFilter) {
return false;
}
if (!filter) {
return true;
}
const matcher = new SiteSettingMatcher(filter, siteSetting);
if (matcher.isNameMatch) {
siteSetting.weight = 10;
return true;
}
if (matcher.isKeywordMatch) {
siteSetting.weight = 5;
return true;
}
if (matcher.isDescriptionMatch) {
return true;
}
if (matcher.isValueMatch) {
return true;
}
if (matcher.isFuzzyNameMatch) {
siteSetting.weight += matcher.matchStrength;
fuzzyMatches.push(siteSetting);
return true;
}
return false;
}
);
if (siteSettings.length > 0) {
matches.pushObjects(siteSettings);
@@ -138,9 +132,9 @@ export default class SiteSettingFilter {
@bind
sortSettings(settings) {
// Sort the site settings so that fuzzy results are at the bottom
// and ordered by their gap count asc.
// and ordered by their match strength.
return settings.sort((a, b) => {
return (a.weight || 0) - (b.weight || 0);
return (b.weight || 0) - (a.weight || 0);
});
}
}
@@ -0,0 +1,75 @@
export default class SiteSettingMatcher {
constructor(filter, siteSetting) {
this.filter = filter;
this.siteSetting = siteSetting;
this.strippedQuery = filter.replace(/[^a-z0-9]/gi, "");
this.fuzzyRegex = new RegExp(this.strippedQuery.split("").join(".*"), "i");
this.fuzzyRegexGaps = new RegExp(
".*" + this.strippedQuery.split("").join("(.*)"),
"i"
);
this.matchStrength = 0;
}
get isNameMatch() {
const name = this.siteSetting.setting.toLowerCase();
return (
name.includes(this.filter) ||
name.replace(/_/g, " ").includes(this.filter)
);
}
get isKeywordMatch() {
return (this.siteSetting.keywords || []).any((keyword) =>
keyword
.replace(/_/g, " ")
.toLowerCase()
.includes(this.filter.replace(/_/g, " "))
);
}
get isDescriptionMatch() {
return this.siteSetting.description.toLowerCase().includes(this.filter);
}
get isValueMatch() {
return (this.siteSetting.value || "")
.toString()
.toLowerCase()
.includes(this.filter);
}
get isFuzzyNameMatch() {
const name = this.siteSetting.setting.toLowerCase();
if (this.strippedQuery.length < 3) {
return false;
}
if (!this.fuzzyRegex.test(name)) {
return false;
}
const fuzzySearchLimiter = 25;
const strippedSetting = name.replace(/[^a-z0-9]/gi, "");
if (
strippedSetting.length >
this.strippedQuery.length + fuzzySearchLimiter
) {
return false;
}
const gapResult = strippedSetting.match(this.fuzzyRegexGaps);
if (gapResult) {
// Discard empty gaps and disregard the full string match.
const numberOfGaps = gapResult.filter((gap) => gap !== "").length - 1;
this.matchStrength -= numberOfGaps;
}
return true;
}
}