From 33d4946947c25c45c7f9d270ab91329729fb9b9c Mon Sep 17 00:00:00 2001 From: Bart van Bragt Date: Thu, 16 Apr 2026 11:52:34 +0200 Subject: [PATCH] feat: stem exclusion admin setting and analyzer language selector improvements Add a stem-exclusion textarea to the admin settings page. Words entered here (one per line) are passed as stem_exclusion to the language analyzer, preventing them from being stemmed. Requires a full re-index to take effect. The two language-dependent fields (stem exclusion, min search length) are disabled when CJK is selected, since neither applies to bigram analysis. Settings are reordered: search-scope switches now appear above the language block so the language-dependent fields sit directly below the language selector. Replace the lowercase ES identifier labels in the language dropdown with proper display names (e.g. "Brazilian Portuguese", "CJK (Chinese, Japanese, Korean)", "Sorani (Kurdish)"). Drift detection now tracks stem exclusion instead of the removed min_gram index setting. --- extend.php | 4 +- js/src/admin/index.ts | 127 +++++++++++++++++++++------------------- resources/locale/en.yml | 5 +- 3 files changed, 75 insertions(+), 61 deletions(-) diff --git a/extend.php b/extend.php index 3de3ced..a26947b 100644 --- a/extend.php +++ b/extend.php @@ -35,5 +35,7 @@ return [ ->default('blomstra-search.search-discussion-subjects', true) ->default('blomstra-search.search-post-bodies', true) ->default('blomstra-search.min-search-length', Commands\BuildCommand::DEFAULT_MIN_SEARCH_LENGTH) - ->serializeToForum('blomstraSearchMinLength', 'blomstra-search.min-search-length', 'intval'), + ->default('blomstra-search.stem-exclusion', '') + ->serializeToForum('blomstraSearchMinLength', 'blomstra-search.min-search-length', 'intval') + ->serializeToForum('blomstraSearchPostBodies', 'blomstra-search.search-post-bodies', fn ($v) => (bool) $v, true), ]; diff --git a/js/src/admin/index.ts b/js/src/admin/index.ts index ff0f0f9..58b3256 100644 --- a/js/src/admin/index.ts +++ b/js/src/admin/index.ts @@ -4,7 +4,7 @@ import DashboardPage from 'flarum/admin/components/DashboardPage'; import DashboardWidget from 'flarum/admin/components/DashboardWidget'; import Alert from 'flarum/common/components/Alert'; -const REQUIRED_INDEX_COMPAT = 'v2'; +const REQUIRED_INDEX_COMPAT = 'v3'; class ReindexWarningWidget extends DashboardWidget { className() { @@ -35,46 +35,43 @@ app.initializers.add('blomstra-search', () => { }); } - const languages = new Map(); - [ - 'arabic', - 'armenian', - 'basque', - 'bengali', - 'brazilian', - 'bulgarian', - 'catalan', - 'cjk', - 'czech', - 'danish', - 'dutch', - 'english', - 'estonian', - 'finnish', - 'french', - 'galician', - 'german', - 'greek', - 'hindi', - 'hungarian', - 'indonesian', - 'irish', - 'italian', - 'latvian', - 'lithuanian', - 'norwegian', - 'persian', - 'portuguese', - 'romanian', - 'russian', - 'sorani', - 'spanish', - 'swedish', - 'turkish', - 'thai', - ].forEach((language) => { - languages.set(language, language); - }); + const languages: Record = { + arabic: 'Arabic', + armenian: 'Armenian', + basque: 'Basque', + bengali: 'Bengali', + brazilian: 'Brazilian Portuguese', + bulgarian: 'Bulgarian', + catalan: 'Catalan', + cjk: 'CJK (Chinese, Japanese, Korean)', + czech: 'Czech', + danish: 'Danish', + dutch: 'Dutch', + english: 'English', + estonian: 'Estonian', + finnish: 'Finnish', + french: 'French', + galician: 'Galician', + german: 'German', + greek: 'Greek', + hindi: 'Hindi', + hungarian: 'Hungarian', + indonesian: 'Indonesian', + irish: 'Irish', + italian: 'Italian', + latvian: 'Latvian', + lithuanian: 'Lithuanian', + norwegian: 'Norwegian', + persian: 'Persian', + portuguese: 'Portuguese', + romanian: 'Romanian', + russian: 'Russian', + sorani: 'Sorani (Kurdish)', + spanish: 'Spanish', + swedish: 'Swedish', + turkish: 'Turkish', + thai: 'Thai', + }; app.extensionData .for('blomstra-search') @@ -84,12 +81,10 @@ app.initializers.add('blomstra-search', () => { if (!activeIndex || !indexedAnalyzer) return null; const currentAnalyzer = this.setting('blomstra-search.analyzer-language')() || 'english'; - const currentMinLength = this.setting('blomstra-search.min-search-length')(); - const indexedMinLength = String( - app.data.settings['blomstra-search.indexed-min-search-length'] || app.data.settings['blomstra-search.min-search-length'] - ); + const currentStemExclusion = this.setting('blomstra-search.stem-exclusion')() || ''; + const indexedStemExclusion = app.data.settings['blomstra-search.indexed-stem-exclusion'] || ''; - if (currentAnalyzer === indexedAnalyzer && currentMinLength === indexedMinLength) return null; + if (currentAnalyzer === indexedAnalyzer && currentStemExclusion === indexedStemExclusion) return null; return m( Alert, @@ -118,14 +113,6 @@ app.initializers.add('blomstra-search', () => { default: 'flarum', type: 'input', }) - .registerSetting({ - setting: 'blomstra-search.analyzer-language', - label: app.translator.trans('blomstra-search.admin.analyzer.label'), - help: app.translator.trans('blomstra-search.admin.analyzer.help'), - type: 'select', - options: Object.fromEntries(languages.entries()), - default: 'english', - }) .registerSetting({ setting: 'blomstra-search.search-discussion-subjects', label: app.translator.trans('blomstra-search.admin.search-discussion-subjects'), @@ -137,11 +124,33 @@ app.initializers.add('blomstra-search', () => { type: 'switch', }) .registerSetting({ - setting: 'blomstra-search.min-search-length', - label: app.translator.trans('blomstra-search.admin.min-search-length.label'), - help: app.translator.trans('blomstra-search.admin.min-search-length.help'), + setting: 'blomstra-search.analyzer-language', + label: app.translator.trans('blomstra-search.admin.analyzer.label'), + help: app.translator.trans('blomstra-search.admin.analyzer.help'), type: 'select', - options: { '1': '1', '2': '2', '3': '3', '4': '4' }, - default: app.data.settings['blomstra-search.min-search-length'], + options: languages, + default: 'english', + }) + .registerSetting(function (this: any) { + const isCjk = (this.setting('blomstra-search.analyzer-language')() || 'english') === 'cjk'; + return this.buildSettingComponent({ + setting: 'blomstra-search.stem-exclusion', + type: 'textarea', + label: app.translator.trans('blomstra-search.admin.settings.stem-exclusion.label'), + help: app.translator.trans('blomstra-search.admin.settings.stem-exclusion.help'), + disabled: isCjk, + }); + }) + .registerSetting(function (this: any) { + const isCjk = (this.setting('blomstra-search.analyzer-language')() || 'english') === 'cjk'; + return this.buildSettingComponent({ + setting: 'blomstra-search.min-search-length', + label: app.translator.trans('blomstra-search.admin.min-search-length.label'), + help: app.translator.trans('blomstra-search.admin.min-search-length.help'), + type: 'select', + options: { '1': '1', '2': '2', '3': '3', '4': '4' }, + default: app.data.settings['blomstra-search.min-search-length'], + disabled: isCjk, + }); }); }); diff --git a/resources/locale/en.yml b/resources/locale/en.yml index ec08d82..5ce7f8e 100644 --- a/resources/locale/en.yml +++ b/resources/locale/en.yml @@ -17,9 +17,12 @@ blomstra-search: search-post-bodies: Search inside comments match-sentences: Match search term against full sentence match-words: Match search term against full words + settings: + stem-exclusion: + label: "Protected words (stem exclusion)" + help: "One word per line. These words will not be stemmed during search (e.g. brand names, proper nouns). Not applicable for CJK or Thai languages. Requires a full re-index to take effect." min-search-length: label: Minimum search query length help: | Minimum number of characters required before a search is triggered. Lower this to 1 or 2 for CJK (Chinese, Japanese, Korean) communities where single characters carry full meaning. - Changing this requires a full index rebuild to take effect.