nodebb-plugin-dbsearch 6.4.0 → 6.5.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 CHANGED
@@ -6,5 +6,10 @@ A Plugin that lets users search posts and topics
6
6
 
7
7
  npm install nodebb-plugin-dbsearch
8
8
 
9
+ ## MongoDB read preference (replica set deployments)
9
10
 
11
+ When the plugin is running on MongoDB, the ACP page exposes a **MongoDB Read Preference** selector that controls where the plugin's read queries (full-text search aggregations and indexed-document counts) are routed. The selector has no effect on the Postgres or Redis backends.
10
12
 
13
+ Allowed values match the standard MongoDB read preference modes: `primary` (default), `primaryPreferred`, `secondary`, `secondaryPreferred`, `nearest`. Writes (indexing, removals) always go to the primary regardless of this setting.
14
+
15
+ The default of `primary` keeps the original behavior. On a replica set, switching to `secondaryPreferred` (or `secondary`) offloads search load from the primary at the cost of slightly stale results due to replication lag — usually unnoticeable for full-text search but worth knowing for workflows that reindex and immediately query. On standalone deployments the driver transparently falls back to the primary, so the setting is a no-op.
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- 'use strict';
2
-
3
- module.exports = require('./lib/dbsearch');
4
-
1
+ 'use strict';
2
+
3
+ module.exports = require('./lib/dbsearch');
4
+
package/lib/dbsearch.js CHANGED
@@ -42,13 +42,21 @@ const languageLookup = {
42
42
 
43
43
  const defaultPostLimit = 500;
44
44
  const defaultTopicLimit = 500;
45
+ const defaultMongoReadPreference = 'primary';
45
46
 
46
47
  let pluginConfig = {
47
48
  postLimit: defaultPostLimit,
48
49
  topicLimit: defaultTopicLimit,
49
50
  excludeCategories: [],
51
+ mongoReadPreference: defaultMongoReadPreference,
50
52
  };
51
53
 
54
+ function applyMongoReadPreference(pref) {
55
+ if (typeof searchModule.setReadPreference === 'function') {
56
+ searchModule.setReadPreference(pref);
57
+ }
58
+ }
59
+
52
60
  const batchSize = 500;
53
61
 
54
62
  const search = module.exports;
@@ -68,10 +76,12 @@ search.init = async function (params) {
68
76
  router.post('/api/admin/plugins/dbsearch/save', params.middleware.applyCSRF, save);
69
77
 
70
78
  pluginConfig = await getPluginData();
79
+ applyMongoReadPreference(pluginConfig.mongoReadPreference);
71
80
  await searchModule.createIndices(convertLanguageName(pluginConfig ? pluginConfig.indexLanguage || 'en' : 'en'));
72
81
 
73
82
  pubsub.on('nodebb-plugin-dbsearch:settings:save', (data) => {
74
83
  Object.assign(pluginConfig, data);
84
+ applyMongoReadPreference(pluginConfig.mongoReadPreference);
75
85
  });
76
86
  };
77
87
 
@@ -153,7 +163,7 @@ search.actionTopicsPurge = async function (data) {
153
163
  }, {
154
164
  batch: batchSize,
155
165
  });
156
- }));
166
+ }));
157
167
  }
158
168
  await Promise.all([
159
169
  searchRemove('topic', data.topics.map(t => t.tid)),
@@ -496,10 +506,16 @@ async function renderAdmin(req, res) {
496
506
 
497
507
  async function save(req, res) {
498
508
  if (utils.isNumber(req.body.postLimit) && utils.isNumber(req.body.topicLimit)) {
509
+ const allowed = searchModule.VALID_READ_PREFERENCES || [];
510
+ const mongoReadPreference = allowed.includes(req.body.mongoReadPreference) ?
511
+ req.body.mongoReadPreference :
512
+ defaultMongoReadPreference;
513
+
499
514
  const data = {
500
515
  postLimit: req.body.postLimit,
501
516
  topicLimit: req.body.topicLimit,
502
517
  excludeCategories: JSON.stringify(req.body.excludeCategories || []),
518
+ mongoReadPreference: mongoReadPreference,
503
519
  };
504
520
 
505
521
  await db.setObject('nodebb-plugin-dbsearch', data);
@@ -507,6 +523,7 @@ async function save(req, res) {
507
523
  pluginConfig.postLimit = data.postLimit;
508
524
  pluginConfig.topicLimit = data.topicLimit;
509
525
  pluginConfig.excludeCategories = req.body.excludeCategories || [];
526
+ pluginConfig.mongoReadPreference = mongoReadPreference;
510
527
  pubsub.publish('nodebb-plugin-dbsearch:settings:save', pluginConfig);
511
528
  res.json('Settings saved!');
512
529
  }
@@ -524,6 +541,7 @@ async function getPluginData() {
524
541
  data.postLimit = data.postLimit || defaultPostLimit;
525
542
  data.topicLimit = data.topicLimit || defaultTopicLimit;
526
543
  data.indexLanguage = data.indexLanguage || 'en';
544
+ data.mongoReadPreference = data.mongoReadPreference || defaultMongoReadPreference;
527
545
  data.working = parseInt(data.working, 10) || 0;
528
546
 
529
547
  try {
@@ -550,6 +568,12 @@ async function getGlobalAndPluginData() {
550
568
  plugin.languageSupported = languageSupported;
551
569
  plugin.languages = languages;
552
570
 
571
+ plugin.mongoBackend = nconf.get('database') === 'mongo';
572
+ plugin.mongoReadPreferences = (searchModule.VALID_READ_PREFERENCES || []).map(value => ({
573
+ value,
574
+ selected: value === plugin.mongoReadPreference,
575
+ }));
576
+
553
577
  plugin.allCategories = allCategories;
554
578
  plugin.topicCount = parseInt(global.topicCount, 10);
555
579
  plugin.postCount = parseInt(global.postCount, 10);
package/lib/mongo.js CHANGED
@@ -4,6 +4,30 @@ const nconf = require.main.require('nconf');
4
4
 
5
5
  const db = require.main.require('./src/database');
6
6
 
7
+ const VALID_READ_PREFERENCES = [
8
+ 'primary',
9
+ 'primaryPreferred',
10
+ 'secondary',
11
+ 'secondaryPreferred',
12
+ 'nearest',
13
+ ];
14
+
15
+ let readPreference = 'primary';
16
+
17
+ exports.VALID_READ_PREFERENCES = VALID_READ_PREFERENCES;
18
+
19
+ exports.setReadPreference = function (pref) {
20
+ if (VALID_READ_PREFERENCES.includes(pref)) {
21
+ readPreference = pref;
22
+ } else {
23
+ readPreference = 'primary';
24
+ }
25
+ };
26
+
27
+ function readOptions() {
28
+ return readPreference === 'primary' ? {} : { readPreference };
29
+ }
30
+
7
31
  function generateIndexName(indexSpec) {
8
32
  return Object.entries(indexSpec)
9
33
  .map(([key, value]) => `${key}_${value}`)
@@ -137,7 +161,7 @@ exports.search = async function (key, data, limit) {
137
161
  aggregate.push({ $limit: parseInt(limit, 10) });
138
162
  aggregate.push({ $project: { _id: 1 } });
139
163
 
140
- const results = await db.client.collection(`search${key}`).aggregate(aggregate).toArray();
164
+ const results = await db.client.collection(`search${key}`).aggregate(aggregate, readOptions()).toArray();
141
165
  if (!results || !results.length) {
142
166
  return [];
143
167
  }
@@ -204,7 +228,7 @@ exports.chat.search = async (data, limit) => {
204
228
  { $sort: { score: { $meta: 'textScore' } } },
205
229
  { $limit: parseInt(limit, 10) },
206
230
  { $project: { _id: 1 } },
207
- ]).toArray();
231
+ ], readOptions()).toArray();
208
232
  if (!results || !results.length) {
209
233
  return [];
210
234
  }
@@ -227,13 +251,13 @@ function buildTextQuery(content, matchWords) {
227
251
  }
228
252
 
229
253
  exports.getIndexedTopicCount = async () => {
230
- return await db.client.collection('searchtopic').estimatedDocumentCount();
254
+ return await db.client.collection('searchtopic').estimatedDocumentCount(readOptions());
231
255
  };
232
256
 
233
257
  exports.getIndexedPostCount = async () => {
234
- return await db.client.collection('searchpost').estimatedDocumentCount();
258
+ return await db.client.collection('searchpost').estimatedDocumentCount(readOptions());
235
259
  };
236
260
 
237
261
  exports.getIndexedChatMessageCount = async () => {
238
- return await db.client.collection('searchchat').estimatedDocumentCount();
262
+ return await db.client.collection('searchchat').estimatedDocumentCount(readOptions());
239
263
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-dbsearch",
3
- "version": "6.4.0",
3
+ "version": "6.5.0",
4
4
  "description": "A Plugin that lets users search posts and topics",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -19,13 +19,12 @@
19
19
  "author": "Baris Usakli <baris@designcreateplay.com>",
20
20
  "license": "BSD-2-Clause",
21
21
  "dependencies": {
22
- "lodash": "4.17.21",
22
+ "lodash": "4.17.23",
23
23
  "redisearch": "^2.0.1"
24
24
  },
25
25
  "devDependencies": {
26
- "eslint": "^9.25.1",
27
- "eslint-config-nodebb": "^1.1.4",
28
- "eslint-plugin-import": "^2.31.0"
26
+ "eslint": "^10.0.3",
27
+ "eslint-config-nodebb": "^2.0.1"
29
28
  },
30
29
  "nbbpm": {
31
30
  "compatibility": "^4.9.0"
package/public/admin.js CHANGED
@@ -3,8 +3,8 @@
3
3
  define('admin/plugins/dbsearch', [
4
4
  'alerts', 'admin/settings',
5
5
  ], function (alerts, settings) {
6
- var dbsearch = {};
7
- var intervalId = 0;
6
+ const dbsearch = {};
7
+ let intervalId = 0;
8
8
 
9
9
  $(window).on('action:ajaxify.end', function (ev, data) {
10
10
  if (data.url === 'admin/plugins/dbsearch' && ajaxify.data.working) {
@@ -21,6 +21,7 @@ define('admin/plugins/dbsearch', [
21
21
  topicLimit: $('#topicLimit').val(),
22
22
  postLimit: $('#postLimit').val(),
23
23
  excludeCategories: $('#exclude-categories').val(),
24
+ mongoReadPreference: $('#mongoReadPreference').val(),
24
25
  }, function (data) {
25
26
  if (typeof data === 'string') {
26
27
  settings.toggleSaveSuccess($('#save'));
@@ -66,7 +67,7 @@ define('admin/plugins/dbsearch', [
66
67
  });
67
68
 
68
69
  $('#changeLanguage').on('click', function () {
69
- var lang = $('#indexLanguage').val();
70
+ const lang = $('#indexLanguage').val();
70
71
  alerts.success('Changing index language to "' + lang + '".');
71
72
  socket.emit('admin.plugins.dbsearch.changeLanguage', lang, function (err) {
72
73
  if (err) {
@@ -106,7 +107,7 @@ define('admin/plugins/dbsearch', [
106
107
  $('.post-progress').css('width', progress.postsPercent + '%').text(progress.postsPercent + '%');
107
108
  $('.message-progress').css('width', progress.messagesPercent + '%').text(progress.messagesPercent + '%');
108
109
 
109
- var working = parseInt(progress.working, 10);
110
+ const working = parseInt(progress.working, 10);
110
111
  if (!working) {
111
112
  clearInterval(intervalId);
112
113
  $('#reindex').removeAttr('disabled');
@@ -63,6 +63,17 @@
63
63
  <label class="form-label">Post Limit</label>
64
64
  <input id="postLimit" type="text" class="form-control" placeholder="Number of posts to return" value="{postLimit}">
65
65
  </div>
66
+
67
+ <!-- IF mongoBackend -->
68
+ <div class="mb-3">
69
+ <label class="form-label">MongoDB Read Preference <i class="fa fa-circle-question" title="Routes search read queries (text search and indexed-document counts) to the chosen replica set member. secondaryPreferred can offload search load from the primary on replica set deployments; results may be slightly stale due to replication lag. Has no effect on standalone deployments." data-bs-toggle="tooltip"></i></label>
70
+ <select class="form-select" id="mongoReadPreference">
71
+ <!-- BEGIN mongoReadPreferences -->
72
+ <option value="{mongoReadPreferences.value}" <!-- IF mongoReadPreferences.selected -->selected<!-- ENDIF mongoReadPreferences.selected -->>{mongoReadPreferences.value}</option>
73
+ <!-- END mongoReadPreferences -->
74
+ </select>
75
+ </div>
76
+ <!-- ENDIF mongoBackend -->
66
77
  </div>
67
78
 
68
79
  <div class="col-6">