@jasonwch/nodebb-plugin-meilisearch-r 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -28
- package/lib/alerts.js +24 -0
- package/lib/chat-search-global.js +221 -0
- package/lib/client.js +445 -0
- package/lib/constants.js +42 -0
- package/lib/defaults.js +73 -0
- package/lib/embedder.js +297 -0
- package/lib/hooks/messages.js +49 -0
- package/lib/hooks/posts.js +180 -0
- package/lib/hooks/topics.js +210 -0
- package/lib/pending-queue.js +69 -0
- package/lib/reindex.js +311 -0
- package/lib/routes.js +129 -0
- package/lib/search.js +230 -0
- package/lib/settings-helpers.js +15 -0
- package/lib/settings.js +119 -0
- package/lib/state.js +69 -0
- package/library.js +17 -1555
- package/package.json +1 -1
- package/static/languages/en-GB/meilisearch.json +112 -79
- package/static/languages/he/meilisearch.json +113 -0
- package/static/languages/pl/meilisearch.json +34 -1
- package/static/languages/zh-CN/meilisearch.json +34 -1
- package/static/lib/admin.js +317 -2
- package/static/templates/admin/plugins/meilisearch.tpl +289 -222
package/static/lib/admin.js
CHANGED
|
@@ -10,7 +10,8 @@ define('admin/plugins/meilisearch', [
|
|
|
10
10
|
'api',
|
|
11
11
|
'alerts',
|
|
12
12
|
'modals',
|
|
13
|
-
|
|
13
|
+
'translator',
|
|
14
|
+
], (settings, api, alerts, modals, translator) => {
|
|
14
15
|
const ACP = {};
|
|
15
16
|
let reindexCompleteShown = false;
|
|
16
17
|
|
|
@@ -67,23 +68,212 @@ define('admin/plugins/meilisearch', [
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
// Suggestions only - the model field is always free text so any model name/tag works.
|
|
72
|
+
// First entry in each list doubles as the input's placeholder.
|
|
73
|
+
const SEMANTIC_MODEL_SUGGESTIONS = {
|
|
74
|
+
openAi: ['text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002'],
|
|
75
|
+
huggingFace: [
|
|
76
|
+
'BAAI/bge-base-en-v1.5', 'BAAI/bge-small-en-v1.5', 'BAAI/bge-large-en-v1.5',
|
|
77
|
+
'sentence-transformers/all-MiniLM-L6-v2', 'sentence-transformers/all-mpnet-base-v2',
|
|
78
|
+
],
|
|
79
|
+
ollama: ['nomic-embed-text', 'mxbai-embed-large', 'all-minilm', 'bge-m3'],
|
|
80
|
+
rest: [],
|
|
81
|
+
};
|
|
82
|
+
const SEMANTIC_URL_PLACEHOLDERS = {
|
|
83
|
+
// Leave OpenAI's own default endpoint as an empty placeholder - it's optional there,
|
|
84
|
+
// only needed to point at an OpenAI-compatible provider (OpenRouter, Azure, LocalAI...).
|
|
85
|
+
openAi: 'https://api.openai.com/v1/embeddings',
|
|
86
|
+
ollama: 'http://localhost:11434/api/embeddings',
|
|
87
|
+
rest: 'https://api.example.com/embeddings',
|
|
88
|
+
};
|
|
89
|
+
// The literal {{text}}/{{embedding}}/{{..}} tokens below are Meilisearch's own REST
|
|
90
|
+
// embedder template syntax, not benchpress - kept in JS (not the .tpl) so they aren't
|
|
91
|
+
// mistaken for template expressions by the admin page's own renderer. "{{..}}" as the
|
|
92
|
+
// 2nd array element (in BOTH templates) enables batching - Meilisearch sends multiple
|
|
93
|
+
// documents' texts in one HTTP call during indexing; omitting it causes
|
|
94
|
+
// "response has a single embedding, but request has multiple texts to embed".
|
|
95
|
+
const SEMANTIC_REST_REQUEST_PLACEHOLDER = '{"input": ["{{text}}", "{{..}}"], "model": "text-embedding-3-small"}';
|
|
96
|
+
const SEMANTIC_REST_RESPONSE_PLACEHOLDER = '{"data": [{"embedding": "{{embedding}}"}, "{{..}}"]}';
|
|
97
|
+
const SEMANTIC_FIELDS_BY_PROVIDER = {
|
|
98
|
+
openAi: ['apiKey', 'model', 'url'],
|
|
99
|
+
huggingFace: ['model'],
|
|
100
|
+
ollama: ['apiKey', 'model', 'url'],
|
|
101
|
+
rest: ['apiKey', 'url', 'rest'],
|
|
102
|
+
};
|
|
103
|
+
// Mirrors the server's SEMANTIC_SETTING_KEYS (lib/embedder.js) minus "enabled" (handled
|
|
104
|
+
// separately below, since off->on matters regardless of these) and "ratio" (confirmed
|
|
105
|
+
// cost-free - buildEmbedderConfig() never reads it). Any of these changing while semantic
|
|
106
|
+
// search is enabled makes the next Save re-embed every document through the configured
|
|
107
|
+
// (often paid) API - see admin.semanticSearchSaveNotice.
|
|
108
|
+
const SEMANTIC_COST_FIELDS = [
|
|
109
|
+
'semanticSearchProvider', 'semanticSearchApiKey', 'semanticSearchModel',
|
|
110
|
+
'semanticSearchUrl', 'semanticSearchDimensions', 'semanticSearchRestRequest', 'semanticSearchRestResponse',
|
|
111
|
+
];
|
|
112
|
+
// Snapshotted right after the form is populated (on load, and again after every
|
|
113
|
+
// successful save) so saveSettings() can tell "did a cost-triggering field actually
|
|
114
|
+
// change" apart from "the form just happens to hold these values" (e.g. re-saving
|
|
115
|
+
// unrelated settings like ranking rules should never prompt a confirm).
|
|
116
|
+
let semanticSnapshot = null;
|
|
117
|
+
|
|
118
|
+
function captureSemanticSnapshot() {
|
|
119
|
+
semanticSnapshot = { enabled: $('#semanticSearchEnabled').is(':checked'), fields: {} };
|
|
120
|
+
SEMANTIC_COST_FIELDS.forEach((id) => {
|
|
121
|
+
semanticSnapshot.fields[id] = $(`#${id}`).val();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// True only when semantic search is (or is about to be) enabled AND something that
|
|
126
|
+
// feeds buildEmbedderConfig() actually changed since the last load/save - never fires
|
|
127
|
+
// for a plain re-save of the same values, or for disabling semantic search (free).
|
|
128
|
+
function semanticCostChangeDetected() {
|
|
129
|
+
if (!semanticSnapshot) return false;
|
|
130
|
+
const enabledNow = $('#semanticSearchEnabled').is(':checked');
|
|
131
|
+
if (!enabledNow) return false;
|
|
132
|
+
if (!semanticSnapshot.enabled) return true;
|
|
133
|
+
return SEMANTIC_COST_FIELDS.some(id => $(`#${id}`).val() !== semanticSnapshot.fields[id]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Mirrors lib/embedder.js validateEmbedderConfig. Reads form values via jQuery
|
|
137
|
+
// selectors (not nodebb.require — runs in browser) and applies the same per-provider
|
|
138
|
+
// rules: openAi requires apiKey iff url blank; huggingFace/ollama need nothing;
|
|
139
|
+
// rest requires url, warns on non-empty non-JSON request/response templates.
|
|
140
|
+
//
|
|
141
|
+
// Returns null if valid, or an error string (will be displayed via alerts.alert).
|
|
142
|
+
function validateEmbedderForm() {
|
|
143
|
+
const enabled = $('#semanticSearchEnabled').is(':checked');
|
|
144
|
+
if (!enabled) return null;
|
|
145
|
+
const provider = $('#semanticSearchProvider').val();
|
|
146
|
+
// .trim() all text inputs — admin may paste with trailing whitespace/newline, which
|
|
147
|
+
// would pass client-side "non-empty" check but fail at the embedding API (e.g. OpenAI
|
|
148
|
+
// returns 401 for an apiKey with trailing whitespace).
|
|
149
|
+
const apiKey = ($('#semanticSearchApiKey').val() || '').trim();
|
|
150
|
+
const url = ($('#semanticSearchUrl').val() || '').trim();
|
|
151
|
+
const restRequest = ($('#semanticSearchRestRequest').val() || '').trim();
|
|
152
|
+
const restResponse = ($('#semanticSearchRestResponse').val() || '').trim();
|
|
153
|
+
const missing = [];
|
|
154
|
+
const invalid = [];
|
|
155
|
+
switch (provider) {
|
|
156
|
+
case 'openAi':
|
|
157
|
+
// apiKey required only when url is blank (custom OpenAI-compatible endpoint may not need auth)
|
|
158
|
+
if (!apiKey && !url) missing.push('apiKey');
|
|
159
|
+
break;
|
|
160
|
+
case 'huggingFace':
|
|
161
|
+
// model + nothing required (HF runs in Meilisearch, defaults work)
|
|
162
|
+
break;
|
|
163
|
+
case 'ollama':
|
|
164
|
+
// url + model + apiKey all optional (defaults work for local install)
|
|
165
|
+
break;
|
|
166
|
+
case 'rest':
|
|
167
|
+
if (!url) missing.push('url');
|
|
168
|
+
if (restRequest.trim() && !isValidJson(restRequest)) invalid.push('request');
|
|
169
|
+
if (restResponse.trim() && !isValidJson(restResponse)) invalid.push('response');
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
return `Unknown provider: ${provider}`;
|
|
173
|
+
}
|
|
174
|
+
if (!missing.length && !invalid.length) return null;
|
|
175
|
+
const parts = [];
|
|
176
|
+
if (missing.length) parts.push(`Required field(s) missing for "${provider}" provider: ${missing.join(', ')}`);
|
|
177
|
+
if (invalid.length) parts.push(`Invalid JSON in field(s): ${invalid.join(', ')}`);
|
|
178
|
+
return parts.join('; ');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isValidJson(str) {
|
|
182
|
+
try { JSON.parse(str); return true; } catch (e) { return false; }
|
|
183
|
+
}
|
|
184
|
+
|
|
70
185
|
ACP.init = function () {
|
|
71
186
|
app.enterRoom('admin/plugins/meilisearch');
|
|
72
187
|
settings.load('meilisearch', $('.meilisearch-settings'), () => {
|
|
73
188
|
toggleLimitNotice();
|
|
189
|
+
initSemanticSearchFields();
|
|
190
|
+
toggleSemanticSearch();
|
|
191
|
+
toggleForceReindexCostNotice();
|
|
192
|
+
captureSemanticSnapshot();
|
|
74
193
|
});
|
|
75
194
|
$('#save').on('click', saveSettings);
|
|
76
195
|
$('#reindex').on('click', reindex);
|
|
77
196
|
formatLocalTimes();
|
|
78
197
|
$('#globalChatSearchEnabled').on('change', toggleLimitNotice);
|
|
198
|
+
$('#semanticSearchEnabled').on('change', () => { toggleSemanticSearch(); toggleForceReindexCostNotice(); });
|
|
199
|
+
$('#semanticSearchProvider').on('change', toggleSemanticSearch);
|
|
200
|
+
$('#semanticSearchRatio').on('input', updateSemanticRatioLabel);
|
|
201
|
+
$('#semanticSearchScoreThreshold').on('input', updateSemanticScoreThresholdLabel);
|
|
202
|
+
$('#force-reindex').on('change', toggleForceReindexCostNotice);
|
|
79
203
|
socket.removeListener('plugins.meilisearch.reindex', onReindex);
|
|
80
204
|
socket.on('plugins.meilisearch.reindex', onReindex);
|
|
205
|
+
socket.removeListener('plugins.meilisearch.alert', onAlert);
|
|
206
|
+
socket.on('plugins.meilisearch.alert', onAlert);
|
|
81
207
|
};
|
|
82
208
|
|
|
209
|
+
// Real-time server-side failures (embedder push rejected, search degraded because the
|
|
210
|
+
// configured embedder/endpoint is broken, ...) - surfaced here instead of only in the
|
|
211
|
+
// server log, since admins reading the ACP won't necessarily be tailing logs.
|
|
212
|
+
function onAlert(data) {
|
|
213
|
+
if (!data) return;
|
|
214
|
+
alerts.alert({
|
|
215
|
+
type: data.type || 'danger',
|
|
216
|
+
title: data.titleKey || '[[meilisearch:admin.meilisearchError]]',
|
|
217
|
+
message: data.message || '',
|
|
218
|
+
timeout: 15000,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
83
222
|
function toggleLimitNotice() {
|
|
84
223
|
$('#globalChatSearchLimitNotice').toggle($('#globalChatSearchEnabled').is(':checked'));
|
|
85
224
|
}
|
|
86
225
|
|
|
226
|
+
function toggleForceReindexCostNotice() {
|
|
227
|
+
const semanticEnabled = $('#semanticSearchEnabled').is(':checked');
|
|
228
|
+
const forced = $('#force-reindex').is(':checked');
|
|
229
|
+
$('#semanticForceReindexCostNotice').toggle(semanticEnabled && forced);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function initSemanticSearchFields() {
|
|
233
|
+
updateModelSuggestions($('#semanticSearchProvider').val());
|
|
234
|
+
$('#semanticSearchUrl').attr('placeholder', SEMANTIC_URL_PLACEHOLDERS[$('#semanticSearchProvider').val()] || '');
|
|
235
|
+
$('#semanticSearchRestRequest').attr('placeholder', SEMANTIC_REST_REQUEST_PLACEHOLDER);
|
|
236
|
+
$('#semanticSearchRestResponse').attr('placeholder', SEMANTIC_REST_RESPONSE_PLACEHOLDER);
|
|
237
|
+
updateSemanticRatioLabel();
|
|
238
|
+
updateSemanticScoreThresholdLabel();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Model stays a free-text input for every provider - the datalist is only
|
|
242
|
+
// suggestions, so any custom model name/tag can still be typed in directly.
|
|
243
|
+
function updateModelSuggestions(provider) {
|
|
244
|
+
const suggestions = SEMANTIC_MODEL_SUGGESTIONS[provider] || [];
|
|
245
|
+
const $list = $('#semanticSearchModelList');
|
|
246
|
+
$list.empty();
|
|
247
|
+
suggestions.forEach((model) => {
|
|
248
|
+
$list.append($('<option></option>').attr('value', model));
|
|
249
|
+
});
|
|
250
|
+
$('#semanticSearchModel').attr('placeholder', suggestions[0] || '');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function updateSemanticRatioLabel() {
|
|
254
|
+
const val = parseFloat($('#semanticSearchRatio').val());
|
|
255
|
+
$('#semanticSearchRatioValue').text(Number.isFinite(val) ? val.toFixed(2) : '0.50');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function updateSemanticScoreThresholdLabel() {
|
|
259
|
+
const val = parseFloat($('#semanticSearchScoreThreshold').val());
|
|
260
|
+
$('#semanticSearchScoreThresholdValue').text(Number.isFinite(val) ? val.toFixed(2) : '0.20');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function toggleSemanticSearch() {
|
|
264
|
+
const enabled = $('#semanticSearchEnabled').is(':checked');
|
|
265
|
+
$('#semantic-search-options').toggle(enabled);
|
|
266
|
+
if (!enabled) return;
|
|
267
|
+
const provider = $('#semanticSearchProvider').val();
|
|
268
|
+
const visibleFields = SEMANTIC_FIELDS_BY_PROVIDER[provider] || [];
|
|
269
|
+
updateModelSuggestions(provider);
|
|
270
|
+
$('#semanticSearchUrl').attr('placeholder', SEMANTIC_URL_PLACEHOLDERS[provider] || '');
|
|
271
|
+
$('[data-semantic-field]').each(function toggleField() {
|
|
272
|
+
const $field = $(this);
|
|
273
|
+
$field.toggle(visibleFields.includes($field.attr('data-semantic-field')));
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
87
277
|
function setProgress(element, current, total) {
|
|
88
278
|
current = Number(current) || 0;
|
|
89
279
|
total = Number(total) || 0;
|
|
@@ -104,12 +294,133 @@ define('admin/plugins/meilisearch', [
|
|
|
104
294
|
}
|
|
105
295
|
|
|
106
296
|
function saveSettings() {
|
|
297
|
+
// (A) Pre-flight validation: block save on missing required fields (config invalid).
|
|
298
|
+
const validationError = validateEmbedderForm();
|
|
299
|
+
if (validationError) {
|
|
300
|
+
alerts.alert({
|
|
301
|
+
type: 'danger',
|
|
302
|
+
alert_id: 'meilisearch-validation',
|
|
303
|
+
title: '[[meilisearch:admin.semanticConfigInvalid]]',
|
|
304
|
+
message: validationError,
|
|
305
|
+
timeout: 10000,
|
|
306
|
+
});
|
|
307
|
+
return; // block save — doSaveSettings not called
|
|
308
|
+
}
|
|
309
|
+
// (Pure A — NEW) Ollama reachability probe. When semantic is enabled AND provider is
|
|
310
|
+
// ollama, asynchronously test <url>/api/version from NodeBB's container (5s timeout).
|
|
311
|
+
// On unreachable, show modal: "Ollama is not reachable from this NodeBB. If you are
|
|
312
|
+
// sure the Ollama is up and reachable from Meilisearch instance, click OK to save anyway."
|
|
313
|
+
// Admin can override because the test runs from NodeBB's network, not Meilisearch's
|
|
314
|
+
// (different networks possible if Meilisearch is host-networked or different Docker net).
|
|
315
|
+
// Other providers skip the probe and go straight to cost-confirm / save.
|
|
316
|
+
const semanticEnabled = $('#semanticSearchEnabled').is(':checked');
|
|
317
|
+
const provider = $('#semanticSearchProvider').val();
|
|
318
|
+
if (semanticEnabled && provider === 'ollama') {
|
|
319
|
+
proceedWithOllamaProbe();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Non-ollama providers: cost-change confirm (if applicable) then save
|
|
323
|
+
if (semanticCostChangeDetected()) {
|
|
324
|
+
modals.confirm('[[meilisearch:admin.confirmSemanticSave]]', (confirm) => {
|
|
325
|
+
if (confirm) doSaveSettings();
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
doSaveSettings();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Pure A: Async Ollama reachability probe. Shows "Testing Ollama..." info toast, then
|
|
333
|
+
// calls POST /api/v3/meilisearch/test-ollama which makes a 5s HTTP GET to <url>/api/version
|
|
334
|
+
// from NodeBB's container. On success, proceeds with the normal cost-confirm → save chain.
|
|
335
|
+
// On unreachable, shows modals.confirm() with the admin-specified text — admin's "OK"
|
|
336
|
+
// click means "Save anyway" (override the probe failure), "Cancel" aborts the save.
|
|
337
|
+
async function proceedWithOllamaProbe() {
|
|
338
|
+
const url = ($('#semanticSearchUrl').val() || '').trim();
|
|
339
|
+
// Disable save button during the async probe to prevent double-click stacking modals.
|
|
340
|
+
// Re-enabled in every exit path (success → doSaveSettings, failure → modal, error → save).
|
|
341
|
+
const $saveBtn = $('#save');
|
|
342
|
+
$saveBtn.prop('disabled', true);
|
|
343
|
+
const reEnableSave = () => { $saveBtn.prop('disabled', false); };
|
|
344
|
+
alerts.alert({
|
|
345
|
+
type: 'info',
|
|
346
|
+
alert_id: 'meilisearch-ollama-probe',
|
|
347
|
+
message: '[[meilisearch:admin.ollamaTesting]]',
|
|
348
|
+
timeout: 6000,
|
|
349
|
+
});
|
|
350
|
+
try {
|
|
351
|
+
const response = await api.post('/plugins/meilisearch/test-ollama', { url });
|
|
352
|
+
if (response && response.reachable) {
|
|
353
|
+
alerts.alert({
|
|
354
|
+
type: 'success',
|
|
355
|
+
alert_id: 'meilisearch-ollama-reachable',
|
|
356
|
+
message: response.version
|
|
357
|
+
? `[[meilisearch:admin.ollamaReachable, ${response.version}]]`
|
|
358
|
+
: '[[meilisearch:admin.ollamaReachableNoVersion]]',
|
|
359
|
+
timeout: 3000,
|
|
360
|
+
});
|
|
361
|
+
reEnableSave();
|
|
362
|
+
// Probe passed — continue with cost-confirm if applicable, else save directly
|
|
363
|
+
if (semanticCostChangeDetected()) {
|
|
364
|
+
modals.confirm('[[meilisearch:admin.confirmSemanticSave]]', (confirm) => {
|
|
365
|
+
if (confirm) doSaveSettings();
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
doSaveSettings();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
// Unreachable — show confirm modal with override option.
|
|
373
|
+
// modals.confirm(message, callback): callback(true) = OK/Save anyway,
|
|
374
|
+
// callback(false) = Cancel/abort. Use translator.translateKey + args as an ARRAY
|
|
375
|
+
// to substitute %1, %2 — bypasses NodeBB's `[[key, arg1, arg2]]` syntax which
|
|
376
|
+
// mis-splits args when an arg contains a comma (e.g. errorDetail
|
|
377
|
+
// "Network error: foo, bar"). translator.compile is NOT a translator — it only
|
|
378
|
+
// BUILDS [[...]] tokens (and would also work here via its escapeArg comma-escaping,
|
|
379
|
+
// but translateKey is more direct).
|
|
380
|
+
const probeUrl = (response && response.probeUrl) || (url || 'http://localhost:11434/api/embeddings');
|
|
381
|
+
const errorDetail = (response && response.error) || 'Unknown error';
|
|
382
|
+
const message = await translator.translateKey(
|
|
383
|
+
'meilisearch:admin.ollamaUnreachableBody',
|
|
384
|
+
[probeUrl, errorDetail],
|
|
385
|
+
translator.getLanguage(),
|
|
386
|
+
);
|
|
387
|
+
reEnableSave();
|
|
388
|
+
modals.confirm(message, (confirm) => {
|
|
389
|
+
if (!confirm) return; // admin cancelled — save aborted, form preserved
|
|
390
|
+
// Admin chose "Save anyway" — continue with cost-confirm if applicable
|
|
391
|
+
if (semanticCostChangeDetected()) {
|
|
392
|
+
modals.confirm('[[meilisearch:admin.confirmSemanticSave]]', (costConfirm) => {
|
|
393
|
+
if (costConfirm) doSaveSettings();
|
|
394
|
+
});
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
doSaveSettings();
|
|
398
|
+
});
|
|
399
|
+
} catch (err) {
|
|
400
|
+
// NodeBB's own API route unreachable (shouldn't normally happen — server down?)
|
|
401
|
+
// Fall through to save so admin isn't blocked by an unrelated server error
|
|
402
|
+
reEnableSave();
|
|
403
|
+
alerts.error(err.message || 'Ollama probe failed unexpectedly');
|
|
404
|
+
if (semanticCostChangeDetected()) {
|
|
405
|
+
modals.confirm('[[meilisearch:admin.confirmSemanticSave]]', (confirm) => {
|
|
406
|
+
if (confirm) doSaveSettings();
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
doSaveSettings();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function doSaveSettings() {
|
|
107
415
|
settings.save('meilisearch', $('.meilisearch-settings'), () => {
|
|
108
416
|
const saveBtn = $('#save').get(0);
|
|
109
417
|
if (saveBtn) {
|
|
110
418
|
saveBtn.classList.toggle('saved', true);
|
|
111
419
|
setTimeout(() => { saveBtn.classList.toggle('saved', false); }, 1500);
|
|
112
420
|
}
|
|
421
|
+
// Re-baseline: this save's values are now "current", so the next save only
|
|
422
|
+
// prompts again if something changes relative to what was JUST saved.
|
|
423
|
+
captureSemanticSnapshot();
|
|
113
424
|
alerts.alert({
|
|
114
425
|
type: 'success',
|
|
115
426
|
alert_id: 'meilisearch-saved',
|
|
@@ -132,7 +443,11 @@ define('admin/plugins/meilisearch', [
|
|
|
132
443
|
}
|
|
133
444
|
function reindex() {
|
|
134
445
|
const forceReindex = document.getElementById('force-reindex').checked;
|
|
135
|
-
|
|
446
|
+
const semanticEnabled = $('#semanticSearchEnabled').is(':checked');
|
|
447
|
+
const confirmMessage = (forceReindex && semanticEnabled)
|
|
448
|
+
? '[[meilisearch:admin.confirmReindex]]<br><br><strong>[[meilisearch:admin.semanticForceReindexCostNotice]]</strong>'
|
|
449
|
+
: '[[meilisearch:admin.confirmReindex]]';
|
|
450
|
+
modals.confirm(confirmMessage, (confirm) => {
|
|
136
451
|
if (!confirm) {
|
|
137
452
|
return;
|
|
138
453
|
}
|