nodebb-plugin-dbsearch 6.0.1 → 6.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/lib/dbsearch.js +111 -11
- package/lib/mongo.js +81 -12
- package/lib/postgres.js +55 -0
- package/lib/redis.js +34 -3
- package/package.json +3 -3
- package/plugin.json +7 -1
- package/public/admin.js +2 -0
- package/templates/admin/plugins/dbsearch.tpl +66 -55
package/lib/dbsearch.js
CHANGED
|
@@ -8,6 +8,7 @@ const nconf = require.main.require('nconf');
|
|
|
8
8
|
const db = require.main.require('./src/database');
|
|
9
9
|
const topics = require.main.require('./src/topics');
|
|
10
10
|
const posts = require.main.require('./src/posts');
|
|
11
|
+
const messaging = require.main.require('./src/messaging');
|
|
11
12
|
const utils = require.main.require('./src/utils');
|
|
12
13
|
const socketAdmin = require.main.require('./src/socket.io/admin');
|
|
13
14
|
const batch = require.main.require('./src/batch');
|
|
@@ -60,10 +61,11 @@ function convertLanguageName(name) {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
search.init = async function (params) {
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
const { router } = params;
|
|
65
|
+
const routeHelpers = require.main.require('./src/routes/helpers');
|
|
66
|
+
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/dbsearch', renderAdmin);
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
router.post('/api/admin/plugins/dbsearch/save', params.middleware.applyCSRF, save);
|
|
67
69
|
|
|
68
70
|
pluginConfig = await getPluginData();
|
|
69
71
|
await searchModule.createIndices(convertLanguageName(pluginConfig ? pluginConfig.indexLanguage || 'en' : 'en'));
|
|
@@ -155,6 +157,22 @@ search.actionTopicChangeOwner = function (hookData) {
|
|
|
155
157
|
topicsSave(hookData.topics);
|
|
156
158
|
};
|
|
157
159
|
|
|
160
|
+
search.actionMessagingSave = async function (hookData) {
|
|
161
|
+
await messagesSave([hookData.message]);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
search.actionMessagingDelete = async function (hookData) {
|
|
165
|
+
await searchRemove('chat', [hookData.message.mid]);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
search.actionMessagingRestore = async function (hookData) {
|
|
169
|
+
await messagesSave([hookData.message]);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
search.actionMessagingEdit = async function (hookData) {
|
|
173
|
+
await messagesSave([hookData.message]);
|
|
174
|
+
};
|
|
175
|
+
|
|
158
176
|
search.filterSearchQuery = async function (data) {
|
|
159
177
|
if (!data || !data.index) {
|
|
160
178
|
return data;
|
|
@@ -198,15 +216,43 @@ search.filterSearchTopic = async function (hookData) {
|
|
|
198
216
|
return hookData;
|
|
199
217
|
};
|
|
200
218
|
|
|
219
|
+
search.filterMessagingSearchMessages = async function (data) {
|
|
220
|
+
if (!data || !data.content) {
|
|
221
|
+
return data;
|
|
222
|
+
}
|
|
223
|
+
const limit = 100;
|
|
224
|
+
const query = {};
|
|
225
|
+
if (data.hasOwnProperty('roomId') && data.roomId) {
|
|
226
|
+
query.roomId = data.roomId;
|
|
227
|
+
}
|
|
228
|
+
if (data.hasOwnProperty('uid') && data.uid) {
|
|
229
|
+
query.uid = data.uid;
|
|
230
|
+
}
|
|
231
|
+
if (data.hasOwnProperty('content') && data.content) {
|
|
232
|
+
query.content = data.content;
|
|
233
|
+
}
|
|
234
|
+
if (!Object.keys(query).length) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
if (data.hasOwnProperty('matchWords')) {
|
|
238
|
+
query.matchWords = data.matchWords;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
data.ids = data.ids.concat(await searchModule.chat.search(query, limit));
|
|
242
|
+
return data;
|
|
243
|
+
};
|
|
244
|
+
|
|
201
245
|
search.reindex = async function () {
|
|
202
246
|
await db.setObject('nodebb-plugin-dbsearch', {
|
|
203
247
|
topicsIndexed: 0,
|
|
204
248
|
postsIndexed: 0,
|
|
249
|
+
messagesIndexed: 0,
|
|
205
250
|
working: 1,
|
|
206
251
|
});
|
|
207
252
|
await Promise.all([
|
|
208
253
|
reIndexTopics(),
|
|
209
254
|
reIndexPosts(),
|
|
255
|
+
reIndexMessages(),
|
|
210
256
|
]);
|
|
211
257
|
await db.setObject('nodebb-plugin-dbsearch', {
|
|
212
258
|
working: 0,
|
|
@@ -307,12 +353,55 @@ async function postsSave(posts) {
|
|
|
307
353
|
await db.incrObjectFieldBy('nodebb-plugin-dbsearch', 'postsIndexed', result.pids.length);
|
|
308
354
|
}
|
|
309
355
|
|
|
356
|
+
async function reIndexMessages() {
|
|
357
|
+
await batch.processSortedSet(`messages:mid`, async (mids) => {
|
|
358
|
+
let messageData = await messaging.getMessagesFields(mids, ['mid', 'content', 'roomId', 'fromuid', 'deleted', 'system']);
|
|
359
|
+
messageData = messageData.filter(p => p && p.deleted !== 1 && p.system !== 1);
|
|
360
|
+
await messagesSave(messageData);
|
|
361
|
+
}, {
|
|
362
|
+
batch: batchSize,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function messagesSave(msgs) {
|
|
367
|
+
msgs = msgs.filter(m => m && m.mid && parseInt(m.deleted, 10) !== 1 && parseInt(m.system, 10) !== 1);
|
|
368
|
+
|
|
369
|
+
let data = msgs.map((msgData) => {
|
|
370
|
+
const indexData = {};
|
|
371
|
+
if (msgData.content) {
|
|
372
|
+
indexData.content = msgData.content;
|
|
373
|
+
}
|
|
374
|
+
if (msgData.roomId) {
|
|
375
|
+
indexData.roomId = msgData.roomId;
|
|
376
|
+
}
|
|
377
|
+
if (msgData.fromuid) {
|
|
378
|
+
indexData.uid = msgData.fromuid;
|
|
379
|
+
}
|
|
380
|
+
if (!Object.keys(indexData).length) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
return indexData;
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const mids = msgs.filter((msg, index) => !!data[index]).map(msg => msg.mid);
|
|
387
|
+
data = data.filter(Boolean);
|
|
388
|
+
if (!data.length) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const result = await plugins.hooks.fire('filter:search.indexMessages', { data: data, mids: mids, messages: msgs });
|
|
393
|
+
await searchModule.chat.index(result.data, result.mids);
|
|
394
|
+
await db.incrObjectFieldBy('nodebb-plugin-dbsearch', 'messagesIndexed', result.mids.length);
|
|
395
|
+
}
|
|
396
|
+
|
|
310
397
|
async function searchRemove(key, ids) {
|
|
311
398
|
await db.searchRemove(key, ids);
|
|
312
399
|
if (key === 'topic') {
|
|
313
400
|
await db.incrObjectFieldBy('nodebb-plugin-dbsearch', 'topicsIndexed', -ids.length);
|
|
314
401
|
} else if (key === 'post') {
|
|
315
402
|
await db.incrObjectFieldBy('nodebb-plugin-dbsearch', 'postsIndexed', -ids.length);
|
|
403
|
+
} else if (key === 'chat') {
|
|
404
|
+
await db.incrObjectFieldBy('nodebb-plugin-dbsearch', 'messagesIndexed', -ids.length);
|
|
316
405
|
}
|
|
317
406
|
}
|
|
318
407
|
|
|
@@ -366,6 +455,7 @@ async function reIndexPids(pids, topic) {
|
|
|
366
455
|
async function renderAdmin(req, res) {
|
|
367
456
|
const results = await getGlobalAndPluginData();
|
|
368
457
|
results.plugin.progressData = await getProgress();
|
|
458
|
+
results.plugin.title = 'DB Search';
|
|
369
459
|
res.render('admin/plugins/dbsearch', results.plugin);
|
|
370
460
|
}
|
|
371
461
|
|
|
@@ -396,6 +486,7 @@ async function getPluginData() {
|
|
|
396
486
|
const data = await db.getObject('nodebb-plugin-dbsearch') || {};
|
|
397
487
|
data.topicsIndexed = parseInt(data.topicsIndexed, 10) || 0;
|
|
398
488
|
data.postsIndexed = parseInt(data.postsIndexed, 10) || 0;
|
|
489
|
+
data.messagesIndexed = parseInt(data.messagesIndexed, 10) || 0;
|
|
399
490
|
data.excludeCategories = data.excludeCategories || '[]';
|
|
400
491
|
data.postLimit = data.postLimit || defaultPostLimit;
|
|
401
492
|
data.topicLimit = data.topicLimit || defaultTopicLimit;
|
|
@@ -413,7 +504,7 @@ async function getPluginData() {
|
|
|
413
504
|
|
|
414
505
|
async function getGlobalAndPluginData() {
|
|
415
506
|
const [global, plugin, allCategories] = await Promise.all([
|
|
416
|
-
db.getObjectFields('global', ['topicCount', 'postCount']),
|
|
507
|
+
db.getObjectFields('global', ['topicCount', 'postCount', 'messageCount']),
|
|
417
508
|
getPluginData(),
|
|
418
509
|
categories.buildForSelectAll(['value', 'text']),
|
|
419
510
|
]);
|
|
@@ -429,10 +520,12 @@ async function getGlobalAndPluginData() {
|
|
|
429
520
|
plugin.allCategories = allCategories;
|
|
430
521
|
plugin.topicCount = parseInt(global.topicCount, 10);
|
|
431
522
|
plugin.postCount = parseInt(global.postCount, 10);
|
|
523
|
+
plugin.messageCount = parseInt(global.messageCount, 10);
|
|
432
524
|
plugin.topicLimit = plugin.topicLimit || defaultTopicLimit;
|
|
433
525
|
plugin.postLimit = plugin.postLimit || defaultPostLimit;
|
|
434
526
|
plugin.topicsIndexed = plugin.topicsIndexed > plugin.topicCount ? plugin.topicCount : plugin.topicsIndexed;
|
|
435
527
|
plugin.postsIndexed = plugin.postsIndexed > plugin.postCount ? plugin.postCount : plugin.postsIndexed;
|
|
528
|
+
plugin.messagesIndexed = plugin.messagesIndexed > plugin.messageCount ? plugin.messageCount : plugin.messagesIndexed;
|
|
436
529
|
plugin.languageSupported = languageSupported;
|
|
437
530
|
plugin.languages = languages;
|
|
438
531
|
plugin.indexLanguage = plugin.indexLanguage || 'en';
|
|
@@ -449,35 +542,40 @@ async function getGlobalAndPluginData() {
|
|
|
449
542
|
|
|
450
543
|
async function getProgress() {
|
|
451
544
|
const [global, pluginData] = await Promise.all([
|
|
452
|
-
db.getObjectFields('global', ['topicCount', 'postCount']),
|
|
545
|
+
db.getObjectFields('global', ['topicCount', 'postCount', 'messageCount']),
|
|
453
546
|
getPluginData(),
|
|
454
547
|
]);
|
|
455
548
|
const topicCount = parseInt(global.topicCount, 10);
|
|
456
549
|
const postCount = parseInt(global.postCount, 10);
|
|
550
|
+
const messageCount = parseInt(global.messageCount, 10);
|
|
457
551
|
const topicsPercent = topicCount ? (pluginData.topicsIndexed / topicCount) * 100 : 0;
|
|
458
552
|
const postsPercent = postCount ? (pluginData.postsIndexed / postCount) * 100 : 0;
|
|
553
|
+
const messagesPercent = messageCount ? (pluginData.messagesIndexed / messageCount) * 100 : 0;
|
|
459
554
|
return {
|
|
460
555
|
topicsPercent: Math.max(0, Math.min(100, topicsPercent.toFixed(2))),
|
|
461
556
|
postsPercent: Math.max(0, Math.min(100, postsPercent.toFixed(2))),
|
|
557
|
+
messagesPercent: Math.max(0, Math.min(100, messagesPercent.toFixed(2))),
|
|
462
558
|
topicsIndexed: topicsPercent >= 100 ? topicCount : Math.max(0, pluginData.topicsIndexed),
|
|
463
559
|
postsIndexed: postsPercent >= 100 ? postCount : Math.max(0, pluginData.postsIndexed),
|
|
560
|
+
messagesIndexed: messagesPercent >= 100 ? messageCount : Math.max(0, pluginData.messagesIndexed),
|
|
464
561
|
working: pluginData.working,
|
|
465
562
|
};
|
|
466
563
|
}
|
|
467
564
|
|
|
468
565
|
socketAdmin.plugins.dbsearch.reindex = function (socket, data, callback) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
566
|
+
setTimeout(async () => {
|
|
567
|
+
try {
|
|
568
|
+
await search.reindex();
|
|
569
|
+
} catch (err) {
|
|
570
|
+
winston.error(err);
|
|
571
|
+
}
|
|
572
|
+
}, 0);
|
|
474
573
|
callback();
|
|
475
574
|
};
|
|
476
575
|
|
|
477
576
|
socketAdmin.plugins.dbsearch.clearIndex = async function () {
|
|
478
577
|
setTimeout(async () => {
|
|
479
578
|
try {
|
|
480
|
-
console.log('clear called');
|
|
481
579
|
await clearIndex();
|
|
482
580
|
} catch (err) {
|
|
483
581
|
winston.error(err.stack);
|
|
@@ -493,11 +591,13 @@ async function clearIndex() {
|
|
|
493
591
|
await Promise.all([
|
|
494
592
|
clearSet('topics:tid', 'topic'),
|
|
495
593
|
clearSet('posts:pid', 'post'),
|
|
594
|
+
clearSet('messages:mid', 'chat'),
|
|
496
595
|
]);
|
|
497
596
|
|
|
498
597
|
await db.setObject('nodebb-plugin-dbsearch', {
|
|
499
598
|
postsIndexed: 0,
|
|
500
599
|
topicsIndexed: 0,
|
|
600
|
+
messagesIndexed: 0,
|
|
501
601
|
working: 0,
|
|
502
602
|
});
|
|
503
603
|
}
|
package/lib/mongo.js
CHANGED
|
@@ -13,6 +13,7 @@ exports.createIndices = async function (language) {
|
|
|
13
13
|
if (nconf.get('isPrimary') && !nconf.get('jobsDisabled')) {
|
|
14
14
|
await db.client.collection('searchtopic').createIndex({ content: 'text', uid: 1, cid: 1 }, options);
|
|
15
15
|
await db.client.collection('searchpost').createIndex({ content: 'text', uid: 1, cid: 1 }, options);
|
|
16
|
+
await db.client.collection('searchchat').createIndex({ content: 'text', roomId: 1, uid: 1 }, options);
|
|
16
17
|
}
|
|
17
18
|
};
|
|
18
19
|
|
|
@@ -26,6 +27,8 @@ exports.changeIndexLanguage = async function (language) {
|
|
|
26
27
|
await db.client.collection('searchtopic').createIndex(indexSpec, options);
|
|
27
28
|
await db.client.collection('searchpost').dropIndex('content_text_uid_1_cid_1');
|
|
28
29
|
await db.client.collection('searchpost').createIndex(indexSpec, options);
|
|
30
|
+
await db.client.collection('searchchat').dropIndex('content_text_roomId_1_uid_1');
|
|
31
|
+
await db.client.collection('searchchat').createIndex({ content: 'text', roomId: 1, uid: 1 }, options);
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
exports.searchIndex = async function (key, data, ids) {
|
|
@@ -53,18 +56,7 @@ exports.searchIndex = async function (key, data, ids) {
|
|
|
53
56
|
exports.search = async function (key, data, limit) {
|
|
54
57
|
const searchQuery = {};
|
|
55
58
|
if (data.content) {
|
|
56
|
-
|
|
57
|
-
const allQuoted = data.content.startsWith('"') && data.content.endsWith('"');
|
|
58
|
-
if (data.matchWords === 'all' && !allQuoted) {
|
|
59
|
-
words = words.map((word) => {
|
|
60
|
-
if (!word.startsWith('"') && !word.endsWith('"')) {
|
|
61
|
-
return `"${word}"`;
|
|
62
|
-
}
|
|
63
|
-
return word;
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
searchQuery.$text = { $search: words.join(' ') };
|
|
59
|
+
searchQuery.$text = buildTextQuery(data.content, data.matchWords);
|
|
68
60
|
}
|
|
69
61
|
|
|
70
62
|
if (Array.isArray(data.cid) && data.cid.length) {
|
|
@@ -112,3 +104,80 @@ exports.searchRemove = async function (key, ids) {
|
|
|
112
104
|
|
|
113
105
|
await db.client.collection(`search${key}`).deleteMany({ _id: { $in: ids } });
|
|
114
106
|
};
|
|
107
|
+
|
|
108
|
+
exports.chat = {};
|
|
109
|
+
exports.chat.index = async (data, ids) => {
|
|
110
|
+
if (!ids.length) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ids = ids.map(id => parseInt(id, 10));
|
|
115
|
+
|
|
116
|
+
const bulk = db.client.collection(`searchchat`).initializeUnorderedBulkOp();
|
|
117
|
+
ids.forEach((id, index) => {
|
|
118
|
+
const d = data[index];
|
|
119
|
+
if (d && d.content && d.uid && d.roomId) {
|
|
120
|
+
bulk.find({ _id: id }).upsert().updateOne({
|
|
121
|
+
$set: {
|
|
122
|
+
content: String(d.content),
|
|
123
|
+
roomId: String(d.roomId),
|
|
124
|
+
uid: String(d.uid),
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await bulk.execute();
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
exports.chat.search = async (data, limit) => {
|
|
134
|
+
const searchQuery = {};
|
|
135
|
+
if (!data.content) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
searchQuery.$text = buildTextQuery(data.content, data.matchWords);
|
|
139
|
+
|
|
140
|
+
if (Array.isArray(data.roomId) && data.roomId.filter(Boolean).length) {
|
|
141
|
+
if (data.roomId.length > 1) {
|
|
142
|
+
searchQuery.roomId = { $in: data.roomId.filter(Boolean).map(String) };
|
|
143
|
+
} else {
|
|
144
|
+
searchQuery.roomId = String(data.roomId[0]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (Array.isArray(data.uid) && data.uid.filter(Boolean).length) {
|
|
149
|
+
if (data.uid.length > 1) {
|
|
150
|
+
searchQuery.uid = { $in: data.uid.filter(Boolean).map(String) };
|
|
151
|
+
} else {
|
|
152
|
+
searchQuery.uid = String(data.uid[0]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const collection = db.client.collection(`searchchat`);
|
|
157
|
+
const results = await collection.aggregate([
|
|
158
|
+
{ $match: searchQuery },
|
|
159
|
+
{ $sort: { score: { $meta: 'textScore' } } },
|
|
160
|
+
{ $limit: parseInt(limit, 10) },
|
|
161
|
+
{ $project: { _id: 1 } },
|
|
162
|
+
]).toArray();
|
|
163
|
+
if (!results || !results.length) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
return results.map(item => item._id);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
function buildTextQuery(content, matchWords) {
|
|
170
|
+
let words = content.split(' ');
|
|
171
|
+
const allQuoted = content.startsWith('"') && content.endsWith('"');
|
|
172
|
+
if (matchWords === 'all' && !allQuoted) {
|
|
173
|
+
words = words.map((word) => {
|
|
174
|
+
if (!word.startsWith('"') && !word.endsWith('"')) {
|
|
175
|
+
return `"${word}"`;
|
|
176
|
+
}
|
|
177
|
+
return word;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { $search: words.join(' ') };
|
|
182
|
+
}
|
|
183
|
+
|
package/lib/postgres.js
CHANGED
|
@@ -17,10 +17,16 @@ async function initDB() {
|
|
|
17
17
|
await db.pool.query(`CREATE INDEX IF NOT EXISTS "idx__searchtopic__content" ON "searchtopic" USING GIN (to_tsvector('${searchLanguage}', "content"))`);
|
|
18
18
|
await db.pool.query('CREATE INDEX IF NOT EXISTS "idx__searchtopic__uid" ON "searchtopic"("uid")');
|
|
19
19
|
await db.pool.query('CREATE INDEX IF NOT EXISTS "idx__searchtopic__cid" ON "searchtopic"("cid")');
|
|
20
|
+
|
|
20
21
|
await db.pool.query('CREATE TABLE IF NOT EXISTS "searchpost" ( "id" BIGINT NOT NULL PRIMARY KEY, "content" TEXT, "uid" BIGINT, "cid" BIGINT )');
|
|
21
22
|
await db.pool.query(`CREATE INDEX IF NOT EXISTS "idx__searchpost__content" ON "searchpost" USING GIN (to_tsvector('${searchLanguage}', "content"))`);
|
|
22
23
|
await db.pool.query('CREATE INDEX IF NOT EXISTS "idx__searchpost__uid" ON "searchpost"("uid")');
|
|
23
24
|
await db.pool.query('CREATE INDEX IF NOT EXISTS "idx__searchpost__cid" ON "searchpost"("cid")');
|
|
25
|
+
|
|
26
|
+
await db.pool.query('CREATE TABLE IF NOT EXISTS "searchchat" ( "id" BIGINT NOT NULL PRIMARY KEY, "content" TEXT, "rid" BIGINT, "uid" BIGINT, )');
|
|
27
|
+
await db.pool.query(`CREATE INDEX IF NOT EXISTS "idx__searchchat__content" ON "searchchat" USING GIN (to_tsvector('${searchLanguage}', "content"))`);
|
|
28
|
+
await db.pool.query('CREATE INDEX IF NOT EXISTS "idx__searchchat__rid" ON "searchchat"("rid")');
|
|
29
|
+
await db.pool.query('CREATE INDEX IF NOT EXISTS "idx__searchchat__uid" ON "searchchat"("uid")');
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
async function handleError(err) {
|
|
@@ -45,8 +51,12 @@ exports.changeIndexLanguage = async function (language) {
|
|
|
45
51
|
pubsub.publish('dbsearch-language-changed', language);
|
|
46
52
|
await db.pool.query('DROP INDEX "idx__searchtopic__content"');
|
|
47
53
|
await db.pool.query(`CREATE INDEX "idx__searchtopic__content" ON "searchtopic" USING GIN (to_tsvector('${language}', "content"))`);
|
|
54
|
+
|
|
48
55
|
await db.pool.query('DROP INDEX "idx__searchpost__content"');
|
|
49
56
|
await db.pool.query(`CREATE INDEX "idx__searchpost__content" ON "searchpost" USING GIN (to_tsvector('${language}', "content"))`);
|
|
57
|
+
|
|
58
|
+
await db.pool.query('DROP INDEX "idx__searchchat__content"');
|
|
59
|
+
await db.pool.query(`CREATE INDEX "idx__searchchat__content" ON "searchchat" USING GIN (to_tsvector('${language}', "content"))`);
|
|
50
60
|
};
|
|
51
61
|
|
|
52
62
|
exports.searchIndex = async function (key, data, ids) {
|
|
@@ -109,3 +119,48 @@ exports.searchRemove = async function (key, ids) {
|
|
|
109
119
|
await handleError(err);
|
|
110
120
|
}
|
|
111
121
|
};
|
|
122
|
+
|
|
123
|
+
exports.chat = {};
|
|
124
|
+
exports.chat.index = async (data, ids) => {
|
|
125
|
+
if (!ids.length) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
ids = ids.map(id => parseInt(id, 10));
|
|
130
|
+
try {
|
|
131
|
+
await db.pool.query({
|
|
132
|
+
name: `dbsearch-searchIndex-chat`,
|
|
133
|
+
text: `INSERT INTO "searchchat" SELECT d."id", d."data"->>'content' "content", (d."data"->>'uid')::bigint "uid", (d."data"->>'roomId')::bigint "roomId" FROM UNNEST($1::bigint[], $2::jsonb[]) d("id", "data") ON CONFLICT ("id") DO UPDATE SET "content" = COALESCE(EXCLUDED."content", "searchchat"."content"), "uid" = COALESCE(EXCLUDED."uid", "searchchat"."uid"), "roomId" = COALESCE(EXCLUDED."roomId", "searchchat"."roomId")`,
|
|
134
|
+
values: [ids, data],
|
|
135
|
+
});
|
|
136
|
+
} catch (err) {
|
|
137
|
+
winston.error(`Error indexing ${err.stack}`);
|
|
138
|
+
await handleError(err);
|
|
139
|
+
await exports.chat.index(data, ids);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
exports.chat.search = async (data, limit) => {
|
|
144
|
+
if (Array.isArray(data.uid) && data.uid.filter(Boolean).length) {
|
|
145
|
+
data.uid = data.uid.filter(Boolean);
|
|
146
|
+
} else {
|
|
147
|
+
data.uid = null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (Array.isArray(data.roomId) && data.roomId.filter(Boolean).length) {
|
|
151
|
+
data.roomId = data.roomId.filter(Boolean);
|
|
152
|
+
} else {
|
|
153
|
+
data.roomId = null;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const res = await db.pool.query({
|
|
157
|
+
name: `dbsearch-search-chat`,
|
|
158
|
+
text: `SELECT ARRAY(SELECT s."id" FROM "searchchat" s WHERE ($1::text IS NULL OR to_tsvector($5::regconfig, "content") @@ plainto_tsquery($5::regconfig, $1::text)) AND ($2::bigint[] IS NULL OR "uid" = ANY($2::bigint[])) AND ($3::bigint[] IS NULL OR "roomId" = ANY($3::bigint[])) ORDER BY ts_rank_cd(to_tsvector($5::regconfig, "content"), plainto_tsquery($5::regconfig, $1::text)) DESC, s."id" ASC LIMIT $4::integer) r`,
|
|
159
|
+
values: [data.content, data.uid, data.roomId, parseInt(limit, 10), searchLanguage],
|
|
160
|
+
});
|
|
161
|
+
return res.rows[0].r;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
await handleError(err);
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
};
|
package/lib/redis.js
CHANGED
|
@@ -9,6 +9,7 @@ const db = require.main.require('./src/database');
|
|
|
9
9
|
exports.createIndices = async function () {
|
|
10
10
|
db.postSearch = redisSearch.createSearch('nodebbpostsearch', db.client);
|
|
11
11
|
db.topicSearch = redisSearch.createSearch('nodebbtopicsearch', db.client);
|
|
12
|
+
db.chatSearch = redisSearch.createSearch('nodebbchatsearch', db.client);
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
exports.changeIndexLanguage = async function () {
|
|
@@ -44,9 +45,39 @@ exports.searchRemove = async function (key, ids) {
|
|
|
44
45
|
if (!key || !ids.length) {
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
47
|
-
const method = key === 'post' ? db.postSearch : db.topicSearch;
|
|
48
|
-
|
|
49
48
|
await async.eachLimit(ids, 500, async (ids) => {
|
|
50
|
-
await
|
|
49
|
+
await db[`${key}Search`].remove(ids);
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
exports.chat = {};
|
|
54
|
+
exports.chat.index = async (data, ids) => {
|
|
55
|
+
if (!ids.length) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const indexData = ids.map((id, index) => ({
|
|
59
|
+
id: id,
|
|
60
|
+
data: {
|
|
61
|
+
content: String(data[index].content),
|
|
62
|
+
roomId: String(data[index].roomId),
|
|
63
|
+
uid: String(data[index].uid),
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
await async.eachLimit(indexData, 500, async (indexData) => {
|
|
68
|
+
await db.chatSearch.index(indexData.data, indexData.id);
|
|
51
69
|
});
|
|
52
70
|
};
|
|
71
|
+
|
|
72
|
+
exports.chat.search = async (data, limit) => {
|
|
73
|
+
const query = {
|
|
74
|
+
matchWords: data.matchWords,
|
|
75
|
+
query: {
|
|
76
|
+
roomId: data.roomId,
|
|
77
|
+
uid: data.uid,
|
|
78
|
+
content: data.content,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return await db.chatSearch.query(query, 0, limit - 1);
|
|
83
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-dbsearch",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "A Plugin that lets users search posts and topics",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"license": "BSD-2-Clause",
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"lodash": "4.17.21",
|
|
22
|
-
"redisearch": "^1.0.
|
|
22
|
+
"redisearch": "^1.0.3"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"eslint": "7.32.0",
|
|
@@ -27,6 +27,6 @@
|
|
|
27
27
|
"eslint-plugin-import": "2.25.2"
|
|
28
28
|
},
|
|
29
29
|
"nbbpm": {
|
|
30
|
-
"compatibility": "^3.
|
|
30
|
+
"compatibility": "^3.3.0"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/plugin.json
CHANGED
|
@@ -27,8 +27,14 @@
|
|
|
27
27
|
{ "hook": "action:topic.move", "method": "actionTopicMove" },
|
|
28
28
|
{ "hook": "action:topic.changeOwner", "method": "actionTopicChangeOwner" },
|
|
29
29
|
|
|
30
|
+
{ "hook": "action:messaging.save", "method": "actionMessagingSave" },
|
|
31
|
+
{ "hook": "action:messaging.delete", "method": "actionMessagingDelete" },
|
|
32
|
+
{ "hook": "action:messaging.restore", "method": "actionMessagingRestore" },
|
|
33
|
+
{ "hook": "action:messaging.edit", "method": "actionMessagingEdit" },
|
|
34
|
+
|
|
30
35
|
{ "hook": "filter:search.query", "method": "filterSearchQuery" },
|
|
31
|
-
{ "hook": "filter:topic.search", "method": "filterSearchTopic" }
|
|
36
|
+
{ "hook": "filter:topic.search", "method": "filterSearchTopic" },
|
|
37
|
+
{ "hook": "filter:messaging.searchMessages", "method": "filterMessagingSearchMessages" }
|
|
32
38
|
],
|
|
33
39
|
"modules": {
|
|
34
40
|
"../admin/plugins/dbsearch.js": "public/admin.js"
|
package/public/admin.js
CHANGED
|
@@ -113,8 +113,10 @@ define('admin/plugins/dbsearch', [
|
|
|
113
113
|
|
|
114
114
|
$('#topics-indexed').text(progress.topicsIndexed);
|
|
115
115
|
$('#posts-indexed').text(progress.postsIndexed);
|
|
116
|
+
$('#messages-indexed').text(progress.messagesIndexed);
|
|
116
117
|
$('.topic-progress').css('width', progress.topicsPercent + '%').text(progress.topicsPercent + '%');
|
|
117
118
|
$('.post-progress').css('width', progress.postsPercent + '%').text(progress.postsPercent + '%');
|
|
119
|
+
$('.message-progress').css('width', progress.messagesPercent + '%').text(progress.messagesPercent + '%');
|
|
118
120
|
});
|
|
119
121
|
}
|
|
120
122
|
|
|
@@ -1,71 +1,82 @@
|
|
|
1
|
-
<div class="
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
<div class="acp-page-container">
|
|
2
|
+
<!-- IMPORT admin/partials/settings/header.tpl -->
|
|
3
|
+
|
|
4
|
+
<div class="row m-0">
|
|
5
|
+
<div id="spy-container" class="col-12 px-0 mb-4" tabindex="0">
|
|
6
|
+
<div class="card">
|
|
7
|
+
<div class="card-header">DB Search</div>
|
|
8
|
+
<div class="card-body row">
|
|
9
|
+
<div class="col-6">
|
|
10
|
+
<div class="mb-3">
|
|
11
|
+
<div class="alert alert-info">
|
|
12
|
+
Topics Indexed: <strong id="topics-indexed">{topicsIndexed}</strong> / <strong>{topicCount}</strong>
|
|
13
|
+
</div>
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
<div class="progress" style="height:24px;">
|
|
16
|
+
<div class="topic-progress progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width:{progressData.topicsPercent}%;min-width: 2em;">{progressData.topicsPercent}%</div>
|
|
17
|
+
</div>
|
|
14
18
|
</div>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
<div class="mb-3">
|
|
20
|
+
<div class="alert alert-info">
|
|
21
|
+
Posts Indexed: <strong id="posts-indexed">{postsIndexed}</strong> / <strong>{postCount}</strong>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="progress" style="height:24px;">
|
|
24
|
+
<div class="post-progress progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width:{progressData.postsPercent}%;min-width: 2em;">{progressData.postsPercent}%</div>
|
|
25
|
+
</div>
|
|
19
26
|
</div>
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
|
|
28
|
+
<div class="mb-3">
|
|
29
|
+
<div class="alert alert-info">
|
|
30
|
+
Messages Indexed: <strong id="messages-indexed">{messagesIndexed}</strong> / <strong>{messageCount}</strong>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="progress" style="height:24px;">
|
|
33
|
+
<div class="message-progress progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width:{progressData.messagesPercent}%;min-width: 2em;">{progressData.messagesPercent}%</div>
|
|
34
|
+
</div>
|
|
22
35
|
</div>
|
|
23
|
-
</div>
|
|
24
36
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
<button class="btn btn-warning" id="reindex" <!-- IF working -->disabled<!-- ENDIF working -->>Re Index</button>
|
|
38
|
+
<button class="btn btn-danger" id="clear-index">Clear Index</button>
|
|
39
|
+
<span id="work-in-progress" class="<!-- IF !working -->hidden<!-- ENDIF !working -->">
|
|
40
|
+
<i class="fa fa-gear fa-spin"></i> Working...
|
|
41
|
+
</span>
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
<hr/>
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
<!-- IF languageSupported -->
|
|
46
|
+
<div class="mb-3">
|
|
47
|
+
<label class="form-label">Index Language</label>
|
|
48
|
+
<select class="form-select" id="indexLanguage">
|
|
49
|
+
<!-- BEGIN languages -->
|
|
50
|
+
<option value="{languages.value}" <!-- IF languages.selected -->selected<!-- ENDIF languages.selected -->>{languages.name}</option>
|
|
51
|
+
<!-- END languages -->
|
|
52
|
+
</select>
|
|
53
|
+
</div>
|
|
54
|
+
<button class="btn btn-primary" id="changeLanguage">Change Language</button>
|
|
55
|
+
<hr/>
|
|
56
|
+
<!-- ENDIF languageSupported -->
|
|
45
57
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
<div class="mb-3">
|
|
59
|
+
<label class="form-label">Topic Limit</label>
|
|
60
|
+
<input id="topicLimit" type="text" class="form-control" placeholder="Number of topics to return" value="{topicLimit}">
|
|
61
|
+
</div>
|
|
62
|
+
<div class="mb-3">
|
|
63
|
+
<label class="form-label">Post Limit</label>
|
|
64
|
+
<input id="postLimit" type="text" class="form-control" placeholder="Number of posts to return" value="{postLimit}">
|
|
65
|
+
</div>
|
|
53
66
|
</div>
|
|
54
|
-
</div>
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
<div class="col-6">
|
|
69
|
+
<div class="post-search-item">
|
|
70
|
+
<label class="form-label">Select categories to exclude from indexing</label>
|
|
71
|
+
<select multiple class="form-select" id="exclude-categories" size="30">
|
|
72
|
+
<!-- BEGIN allCategories -->
|
|
73
|
+
<option value="{allCategories.value}" <!-- IF allCategories.selected -->selected<!-- ENDIF allCategories.selected -->>{allCategories.text}</option>
|
|
74
|
+
<!-- END allCategories -->
|
|
75
|
+
</select>
|
|
76
|
+
</div>
|
|
64
77
|
</div>
|
|
65
78
|
</div>
|
|
66
79
|
</div>
|
|
67
80
|
</div>
|
|
68
81
|
</div>
|
|
69
82
|
</div>
|
|
70
|
-
|
|
71
|
-
<!-- IMPORT admin/partials/save_button.tpl -->
|