diff --git a/src/categories/create.js b/src/categories/create.js index 46aae0a097..81097650e1 100644 --- a/src/categories/create.js +++ b/src/categories/create.js @@ -15,7 +15,7 @@ module.exports = function (Categories) { const parentCid = data.parentCid ? data.parentCid : 0; const cid = await db.incrObjectField('global', 'nextCid'); - data.name = data.name || 'Category ' + cid; + data.name = String(data.name || 'Category ' + cid); const slug = cid + '/' + slugify(data.name); const order = data.order || cid; // If no order provided, place it at the end const colours = Categories.assignColours(); @@ -53,7 +53,12 @@ module.exports = function (Categories) { if (!category.descriptionParsed) { await Categories.parseDescription(category.cid, category.description); } - await db.sortedSetsAdd(['categories:cid', 'cid:' + parentCid + ':children'], category.order, category.cid); + + await db.sortedSetAddBulk([ + ['categories:cid', category.order, category.cid], + ['cid:' + parentCid + ':children', category.order, category.cid], + ['categories:name', 0, data.name.substr(0, 200).toLowerCase() + ':' + category.cid], + ]); const defaultPrivileges = [ 'groups:find', diff --git a/src/categories/index.js b/src/categories/index.js index 5fb73e475f..4f74c6b6d1 100644 --- a/src/categories/index.js +++ b/src/categories/index.js @@ -21,6 +21,7 @@ require('./activeusers')(Categories); require('./recentreplies')(Categories); require('./update')(Categories); require('./watch')(Categories); +require('./search')(Categories); Categories.exists = async function (cid) { if (Array.isArray(cid)) { diff --git a/src/categories/search.js b/src/categories/search.js new file mode 100644 index 0000000000..4bab418caa --- /dev/null +++ b/src/categories/search.js @@ -0,0 +1,71 @@ +'use strict'; + +const _ = require('lodash'); + +const privileges = require('../privileges'); +const plugins = require('../plugins'); +const db = require('../database'); + +module.exports = function (Categories) { + Categories.search = async function (data) { + const query = data.query || ''; + const page = data.page || 1; + const uid = data.uid || 0; + const paginate = data.hasOwnProperty('paginate') ? data.paginate : true; + + const startTime = process.hrtime(); + + let cids = await findCids(query, data.hardCap); + + const result = await plugins.hooks.fire('filter:categories.search', { + cids: cids, + uid: uid, + }); + cids = await privileges.categories.filterCids('find', result.cids, uid); + + const searchResult = { + matchCount: cids.length, + }; + + if (paginate) { + const resultsPerPage = data.resultsPerPage || 50; + const start = Math.max(0, page - 1) * resultsPerPage; + const stop = start + resultsPerPage; + searchResult.pageCount = Math.ceil(cids.length / resultsPerPage); + cids = cids.slice(start, stop); + } + + const childrenCids = await getChildrenCids(cids, uid); + const uniqCids = _.uniq(cids.concat(childrenCids)); + const categoryData = await Categories.getCategories(uniqCids, uid); + + Categories.getTree(categoryData, 0); + await Categories.getRecentTopicReplies(categoryData, uid, data.qs); + categoryData.sort(function (c1, c2) { + if (c1.parentCid !== c2.parentCid) { + return c1.parentCid - c2.parentCid; + } + return c1.order - c2.order; + }); + searchResult.timing = (process.elapsedTimeSince(startTime) / 1000).toFixed(2); + searchResult.categories = categoryData.filter(c => cids.includes(c.cid)); + return searchResult; + }; + + async function findCids(query, hardCap) { + if (!query || String(query).length < 2) { + return []; + } + const data = await db.getSortedSetScan({ + key: 'categories:name', + match: '*' + String(query).toLowerCase() + '*', + limit: hardCap || 500, + }); + return data.map(data => parseInt(data.split(':').pop(), 10)); + } + + async function getChildrenCids(cids, uid) { + const childrenCids = await Promise.all(cids.map(cid => Categories.getChildrenCids(cid))); + return await privileges.categories.filterCids('find', childrenCids.flat(), uid); + } +}; diff --git a/src/controllers/search.js b/src/controllers/search.js index c93e33dba3..afc4ff34ab 100644 --- a/src/controllers/search.js +++ b/src/controllers/search.js @@ -30,6 +30,7 @@ searchController.search = async function (req, res, next) { req.query.in = req.query.in || 'posts'; const allowed = (req.query.in === 'users' && userPrivileges['search:users']) || (req.query.in === 'tags' && userPrivileges['search:tags']) || + (req.query.in === 'categories') || (['titles', 'titlesposts', 'posts'].includes(req.query.in) && userPrivileges['search:content']); if (!allowed) { @@ -77,8 +78,11 @@ searchController.search = async function (req, res, next) { return res.json(searchData); } - searchData.categories = categoriesData; - searchData.categoriesCount = Math.max(10, Math.min(20, categoriesData.length)); + if (['titles', 'titlesposts', 'posts'].includes(req.query.in)) { + searchData.categories = categoriesData; + searchData.categoriesCount = Math.max(10, Math.min(20, categoriesData.length)); + } + searchData.breadcrumbs = helpers.buildBreadcrumbs([{ text: '[[global:search]]' }]); searchData.expandSearch = !req.query.term; diff --git a/src/search.js b/src/search.js index 043a69f12d..8dd0d61cc3 100644 --- a/src/search.js +++ b/src/search.js @@ -23,6 +23,8 @@ search.search = async function (data) { result = await searchInContent(data); } else if (data.searchIn === 'users') { result = await user.search(data); + } else if (data.searchIn === 'categories') { + result = await categories.search(data); } else if (data.searchIn === 'tags') { result = await topics.searchAndLoadTags(data); } else { diff --git a/src/upgrades/1.17.0/category_name_zset.js b/src/upgrades/1.17.0/category_name_zset.js new file mode 100644 index 0000000000..8caa44ee36 --- /dev/null +++ b/src/upgrades/1.17.0/category_name_zset.js @@ -0,0 +1,30 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Create category name sorted set', + timestamp: Date.UTC(2021, 0, 27), + method: async function () { + const progress = this.progress; + + await batch.processSortedSet('categories:cid', async function (cids) { + const keys = cids.map(cid => 'category:' + cid); + let categoryData = await db.getObjectsFields(keys, ['cid', 'name']); + categoryData = categoryData.filter(c => c.cid && c.name); + const bulkAdd = categoryData.map(function (cat) { + return [ + 'categories:name', + 0, + String(cat.name).substr(0, 200).toLowerCase() + ':' + cat.cid, + ]; + }); + await db.sortedSetAddBulk(bulkAdd); + progress.incr(cids.length); + }, { + batch: 500, + progress: progress, + }); + }, +};