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.
This commit is contained in:
Bart van Bragt 2026-04-16 11:52:34 +02:00
parent 0f58abf141
commit 33d4946947
3 changed files with 75 additions and 61 deletions

View File

@ -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),
];

View File

@ -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<string, string> = {
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.analyzer-language',
label: app.translator.trans('blomstra-search.admin.analyzer.label'),
help: app.translator.trans('blomstra-search.admin.analyzer.help'),
type: 'select',
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,
});
});
});

View File

@ -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.