nodebb-plugin-dbsearch 6.4.1 → 6.5.1
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 +15 -10
- package/lib/dbsearch.js +38 -14
- package/lib/mongo.js +31 -7
- package/lib/postgres.js +4 -4
- package/lib/redis.js +2 -2
- package/package.json +4 -4
- package/public/admin.js +1 -0
- package/templates/admin/plugins/dbsearch.tpl +11 -0
- package/upgrades/dbsearch_change_integer_index.js +2 -2
- package/upgrades/dbsearch_change_mongodb_schema.js +2 -2
- package/upgrades/dbsearch_mongodb_ts_index.js +2 -2
- package/upgrades/dbsearch_psql_cid_text_index.js +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
# NodeBB Plugin DB Search
|
|
2
|
-
|
|
3
|
-
A Plugin that lets users search posts and topics
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
npm install nodebb-plugin-dbsearch
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
# NodeBB Plugin DB Search
|
|
2
|
+
|
|
3
|
+
A Plugin that lets users search posts and topics
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
npm install nodebb-plugin-dbsearch
|
|
8
|
+
|
|
9
|
+
## MongoDB read preference (replica set deployments)
|
|
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.
|
|
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/lib/dbsearch.js
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
4
|
|
|
5
|
-
const winston =
|
|
6
|
-
const nconf =
|
|
7
|
-
|
|
8
|
-
const db =
|
|
9
|
-
const topics =
|
|
10
|
-
const posts =
|
|
11
|
-
const messaging =
|
|
12
|
-
const utils =
|
|
13
|
-
const socketAdmin =
|
|
14
|
-
const batch =
|
|
15
|
-
const plugins =
|
|
16
|
-
const categories =
|
|
17
|
-
const pubsub =
|
|
5
|
+
const winston = nodebb.require('winston');
|
|
6
|
+
const nconf = nodebb.require('nconf');
|
|
7
|
+
|
|
8
|
+
const db = nodebb.require('./src/database');
|
|
9
|
+
const topics = nodebb.require('./src/topics');
|
|
10
|
+
const posts = nodebb.require('./src/posts');
|
|
11
|
+
const messaging = nodebb.require('./src/messaging');
|
|
12
|
+
const utils = nodebb.require('./src/utils');
|
|
13
|
+
const socketAdmin = nodebb.require('./src/socket.io/admin');
|
|
14
|
+
const batch = nodebb.require('./src/batch');
|
|
15
|
+
const plugins = nodebb.require('./src/plugins');
|
|
16
|
+
const categories = nodebb.require('./src/categories');
|
|
17
|
+
const pubsub = nodebb.require('./src/pubsub');
|
|
18
18
|
|
|
19
19
|
const searchModule = require(`./${nconf.get('database')}`);
|
|
20
20
|
|
|
@@ -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;
|
|
@@ -62,16 +70,18 @@ function convertLanguageName(name) {
|
|
|
62
70
|
|
|
63
71
|
search.init = async function (params) {
|
|
64
72
|
const { router } = params;
|
|
65
|
-
const routeHelpers =
|
|
73
|
+
const routeHelpers = nodebb.require('./src/routes/helpers');
|
|
66
74
|
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/dbsearch', renderAdmin);
|
|
67
75
|
|
|
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
|
|
|
@@ -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
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const nconf =
|
|
3
|
+
const nconf = nodebb.require('nconf');
|
|
4
4
|
|
|
5
|
-
const db =
|
|
5
|
+
const db = nodebb.require('./src/database');
|
|
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
|
+
}
|
|
6
30
|
|
|
7
31
|
function generateIndexName(indexSpec) {
|
|
8
32
|
return Object.entries(indexSpec)
|
|
@@ -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/lib/postgres.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const winston =
|
|
4
|
-
const nconf =
|
|
3
|
+
const winston = nodebb.require('winston');
|
|
4
|
+
const nconf = nodebb.require('nconf');
|
|
5
5
|
|
|
6
|
-
const db =
|
|
7
|
-
const pubsub =
|
|
6
|
+
const db = nodebb.require('./src/database');
|
|
7
|
+
const pubsub = nodebb.require('./src/pubsub');
|
|
8
8
|
|
|
9
9
|
let searchLanguage = 'english';
|
|
10
10
|
|
package/lib/redis.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const redisSearch = require('redisearch');
|
|
4
4
|
|
|
5
|
-
const async =
|
|
5
|
+
const async = nodebb.require('async');
|
|
6
6
|
|
|
7
|
-
const db =
|
|
7
|
+
const db = nodebb.require('./src/database');
|
|
8
8
|
|
|
9
9
|
exports.createIndices = async function () {
|
|
10
10
|
db.postSearch = redisSearch.createSearch('nodebbpostsearch', db.client);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-dbsearch",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.5.1",
|
|
4
4
|
"description": "A Plugin that lets users search posts and topics",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"redisearch": "^2.0.1"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"eslint": "^10.
|
|
27
|
-
"eslint-config-nodebb": "^2.0.
|
|
26
|
+
"eslint": "^10.4.1",
|
|
27
|
+
"eslint-config-nodebb": "^2.0.2"
|
|
28
28
|
},
|
|
29
29
|
"nbbpm": {
|
|
30
|
-
"compatibility": "^4.
|
|
30
|
+
"compatibility": "^4.12.0"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/public/admin.js
CHANGED
|
@@ -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'));
|
|
@@ -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">
|
|
@@ -8,13 +8,13 @@ module.exports = {
|
|
|
8
8
|
timestamp: Date.UTC(2025, 0, 27),
|
|
9
9
|
method: async function () {
|
|
10
10
|
const { progress } = this;
|
|
11
|
-
const nconf =
|
|
11
|
+
const nconf = nodebb.require('nconf');
|
|
12
12
|
const mainDB = nconf.get('database');
|
|
13
13
|
if (mainDB === 'redis') {
|
|
14
14
|
// redis is not affected, since everything is string already
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
|
-
const batch =
|
|
17
|
+
const batch = nodebb.require('./src/batch');
|
|
18
18
|
async function convertIdToString(collection, docs) {
|
|
19
19
|
await batch.processArray(docs, async (docs) => {
|
|
20
20
|
await db.client.collection(collection).deleteMany({
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
const winston =
|
|
4
|
+
const winston = nodebb.require('winston');
|
|
5
5
|
const db = module.parent.require('./database');
|
|
6
6
|
|
|
7
7
|
module.exports = {
|
|
8
8
|
name: 'Changing dbsearch mongodb search schema to use _id',
|
|
9
9
|
timestamp: Date.UTC(2018, 10, 26),
|
|
10
10
|
method: async function () {
|
|
11
|
-
const nconf =
|
|
11
|
+
const nconf = nodebb.require('nconf');
|
|
12
12
|
const isMongo = nconf.get('database') === 'mongo';
|
|
13
13
|
if (!isMongo) {
|
|
14
14
|
return;
|
|
@@ -8,8 +8,8 @@ module.exports = {
|
|
|
8
8
|
timestamp: Date.UTC(2025, 1, 18),
|
|
9
9
|
method: async function () {
|
|
10
10
|
const { progress } = this;
|
|
11
|
-
const nconf =
|
|
12
|
-
const batch =
|
|
11
|
+
const nconf = nodebb.require('nconf');
|
|
12
|
+
const batch = nodebb.require('./src/batch');
|
|
13
13
|
const isMongo = nconf.get('database') === 'mongo';
|
|
14
14
|
if (!isMongo) {
|
|
15
15
|
return;
|
|
@@ -7,7 +7,7 @@ module.exports = {
|
|
|
7
7
|
name: 'Changing integer cid index to text for psql',
|
|
8
8
|
timestamp: Date.UTC(2025, 6, 8),
|
|
9
9
|
method: async function () {
|
|
10
|
-
const nconf =
|
|
10
|
+
const nconf = nodebb.require('nconf');
|
|
11
11
|
const mainDB = nconf.get('database');
|
|
12
12
|
if (mainDB !== 'postgres') {
|
|
13
13
|
return;
|