diff --git a/public/language/en_GB/search.json b/public/language/en_GB/search.json index 4f099ef554..b8a970d6a9 100644 --- a/public/language/en_GB/search.json +++ b/public/language/en_GB/search.json @@ -3,6 +3,8 @@ "no-matches": "No matches found", "in": "In", "by": "By", + "titles": "Titles", + "titles-posts": "Titles and Posts", "posted-by": "Posted by", "in-categories": "In Categories", "search-child-categories": "Search child categories", @@ -19,5 +21,16 @@ "one-month": "One month", "three-months": "Three months", "six-months": "Six months", - "one-year": "One year" + "one-year": "One year", + "sort-by": "Sort by", + "post-time": "Post time", + "last-reply-time": "Last reply time", + "topic-title": "Topic title", + "number-of-replies": "Number of replies", + "number-of-views": "Number of views", + "topic-start-date": "Topic start date", + "username": "Username", + "category": "Category", + "descending": "In descending order", + "ascending": "In ascending order" } diff --git a/public/src/app.js b/public/src/app.js index 35037c8033..1e5b92cf4d 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -436,7 +436,7 @@ app.uid = null; e.preventDefault(); var input = $(this).find('input'); - search.query({term: input.val(), in: 'posts'}, function() { + search.query({term: input.val()}, function() { input.val(''); }); }); diff --git a/public/src/client/search.js b/public/src/client/search.js index 029f7b811a..691ab51281 100644 --- a/public/src/client/search.js +++ b/public/src/client/search.js @@ -15,26 +15,31 @@ define('forum/search', ['search'], function(searchModule) { fillOutFormFromQueryParams(); searchIn.on('change', function() { - $('.post-search-item').toggleClass('hide', searchIn.val() !== 'posts'); + updateFormItemVisiblity(searchIn.val()); }); highlightMatches(searchQuery); $('#advanced-search').off('submit').on('submit', function(e) { e.preventDefault(); - var input = $(this).find('#search-input'); + var $this = $(this) + var input = $this.find('#search-input'); - searchModule.query({ + var searchData = { term: input.val(), - in: $(this).find('#search-in').val(), - by: $(this).find('#posted-by-user').val(), - categories: $(this).find('#posted-in-categories').val(), - searchChildren: $(this).find('#search-children').is(':checked'), - replies: $(this).find('#reply-count').val(), - repliesFilter: $(this).find('#reply-count-filter').val(), - timeFilter: $(this).find('#post-time-filter').val(), - timeRange: $(this).find('#post-time-range').val() - }, function() { + in: $this.find('#search-in').val(), + by: $this.find('#posted-by-user').val(), + categories: $this.find('#posted-in-categories').val(), + searchChildren: $this.find('#search-children').is(':checked'), + replies: $this.find('#reply-count').val(), + repliesFilter: $this.find('#reply-count-filter').val(), + timeFilter: $this.find('#post-time-filter').val(), + timeRange: $this.find('#post-time-range').val(), + sortBy: $this.find('#post-sort-by').val(), + sortDirection: $this.find('#post-sort-direction').val() + }; + + searchModule.query(searchData, function() { input.val(''); }); }); @@ -42,12 +47,17 @@ define('forum/search', ['search'], function(searchModule) { enableAutoComplete(); }; + function updateFormItemVisiblity(searchIn) { + var hide = searchIn.indexOf('posts') === -1 && searchIn.indexOf('titles') === -1; + $('.post-search-item').toggleClass('hide', hide); + } + function fillOutFormFromQueryParams() { var params = utils.params(); if (params) { if (params.in) { $('#search-in').val(params.in); - $('.post-search-item').toggleClass('hide', params.in !== 'posts'); + updateFormItemVisiblity(params.in); } if (params.by) { @@ -71,6 +81,11 @@ define('forum/search', ['search'], function(searchModule) { $('#post-time-range').val(params.timeRange); $('#post-time-filter').val(params.timeFilter); } + + if (params.sortBy) { + $('#post-sort-by').val(params.sortBy); + $('#post-sort-direction').val(params.sortDirection); + } } } diff --git a/public/src/modules/search.js b/public/src/modules/search.js index 2b90ab290a..9b896b0394 100644 --- a/public/src/modules/search.js +++ b/public/src/modules/search.js @@ -9,8 +9,6 @@ define('search', ['navigator'], function(nav) { Search.query = function(data, callback) { var term = data.term; - var searchIn = data.in || 'posts'; - var postedBy = data.by || ''; // Detect if a tid was specified var topicSearch = term.match(/in:topic-([\d]+)/); @@ -23,29 +21,8 @@ define('search', ['navigator'], function(nav) { } catch(e) { return app.alertError('[[error:invalid-search-term]]'); } - var query = {in: searchIn}; - if (postedBy && searchIn === 'posts') { - query.by = postedBy; - } - - if (data.categories && data.categories.length) { - query.categories = data.categories; - if (data.searchChildren) { - query.searchChildren = data.searchChildren; - } - } - - if (parseInt(data.replies, 10) > 0) { - query.replies = data.replies; - query.repliesFilter = data.repliesFilter || 'atleast'; - } - if (data.timeRange) { - query.timeRange = data.timeRange; - query.timeFilter = data.timeFilter || 'newer'; - } - - ajaxify.go('search/' + term + '?' + decodeURIComponent($.param(query))); + ajaxify.go('search/' + term + '?' + createQueryString(data)); callback(); } else { var cleanedTerm = term.replace(topicSearch[0], ''), @@ -55,6 +32,39 @@ define('search', ['navigator'], function(nav) { } }; + function createQueryString(data) { + var searchIn = data.in || 'titlesposts'; + var postedBy = data.by || ''; + var query = {in: searchIn}; + + if (postedBy && (searchIn === 'posts' || searchIn === 'titles' || searchIn === 'titlesposts')) { + query.by = postedBy; + } + + if (data.categories && data.categories.length) { + query.categories = data.categories; + if (data.searchChildren) { + query.searchChildren = data.searchChildren; + } + } + + if (parseInt(data.replies, 10) > 0) { + query.replies = data.replies; + query.repliesFilter = data.repliesFilter || 'atleast'; + } + + if (data.timeRange) { + query.timeRange = data.timeRange; + query.timeFilter = data.timeFilter || 'newer'; + } + + if (data.sortBy) { + query.sortBy = data.sortBy; + query.sortDirection = data.sortDirection; + } + return decodeURIComponent($.param(query)); + } + Search.queryTopic = function(tid, term, callback) { socket.emit('topics.search', { tid: tid, diff --git a/src/controllers/search.js b/src/controllers/search.js index 2c0222a6b3..f5f93f6874 100644 --- a/src/controllers/search.js +++ b/src/controllers/search.js @@ -28,7 +28,6 @@ searchController.search = function(req, res, next) { time: 0, search_query: '', posts: [], - topics: [], users: [], tags: [], categories: categories, @@ -49,6 +48,8 @@ searchController.search = function(req, res, next) { repliesFilter: req.query.repliesFilter, timeRange: req.query.timeRange, timeFilter: req.query.timeFilter, + sortBy: req.query.sortBy, + sortDirection: req.query.sortDirection, page: page, uid: uid }, function(err, results) { diff --git a/src/search.js b/src/search.js index b8f8ffa849..6e70cfa1bc 100644 --- a/src/search.js +++ b/src/search.js @@ -8,7 +8,8 @@ var async = require('async'), categories = require('./categories'), user = require('./user'), plugins = require('./plugins'), - privileges = require('./privileges'); + privileges = require('./privileges'), + utils = require('../public/src/utils'); var search = {}; @@ -21,6 +22,9 @@ search.search = function(data, callback) { } result.search_query = query; + if (searchIn === 'titles' || searchIn === 'titlesposts') { + searchIn = 'posts'; + } result[searchIn] = data.matches; result.matchCount = data.matchCount; result.hidePostedBy = searchIn !== 'posts'; @@ -31,7 +35,7 @@ search.search = function(data, callback) { var start = process.hrtime(); var query = data.query; - var searchIn = data.searchIn || 'posts'; + var searchIn = data.searchIn || 'titlesposts'; var result = { posts: [], @@ -39,8 +43,8 @@ search.search = function(data, callback) { tags: [] }; - if (searchIn === 'posts') { - searchInPosts(query, data, done); + if (searchIn === 'posts' || searchIn === 'titles' || searchIn === 'titlesposts') { + searchInContent(query, data, done); } else if (searchIn === 'users') { searchInUsers(query, data.uid, done); } else if (searchIn === 'tags') { @@ -50,14 +54,22 @@ search.search = function(data, callback) { } }; -function searchInPosts(query, data, callback) { +function searchInContent(query, data, callback) { data.uid = data.uid || 0; async.parallel({ pids: function(next) { - searchQuery('post', query, next); + if (data.searchIn === 'posts' || data.searchIn === 'titlesposts') { + searchQuery('post', query, next); + } else { + next(null, []); + } }, tids: function(next) { - searchQuery('topic', query, next); + if (data.searchIn === 'titles' || data.searchIn === 'titlesposts') { + searchQuery('topic', query, next); + } else { + next(null, []); + } }, searchCategories: function(next) { getSearchCategories(data, next); @@ -104,24 +116,9 @@ function searchInPosts(query, data, callback) { } function filterAndSort(pids, data, searchCategories, callback) { - var postFields = ['pid', 'tid', 'timestamp']; - var topicFields = []; - - if (data.postedBy) { - postFields.push('uid'); - } - - if (searchCategories.length) { - topicFields.push('cid'); - } - - if (data.replies) { - topicFields.push('postcount'); - } - async.parallel({ posts: function(next) { - getMatchedPosts(pids, postFields, topicFields, next); + getMatchedPosts(pids, data, searchCategories, next); }, postedByUid: function(next) { if (data.postedBy) { @@ -154,28 +151,130 @@ function filterAndSort(pids, data, searchCategories, callback) { }); } -function getMatchedPosts(pids, postFields, topicFields, callback) { - var keys = pids.map(function(pid) { - return 'post:' + pid; - }); +function getMatchedPosts(pids, data, searchCategories, callback) { + var postFields = ['pid', 'tid', 'timestamp']; + var topicFields = []; + var categoryFields = []; + + if (data.postedBy) { + postFields.push('uid'); + } + + if (searchCategories.length || (data.sortBy && data.sortBy.startsWith('category.'))) { + topicFields.push('cid'); + } + + if (data.replies) { + topicFields.push('postcount'); + } + + if (data.sortBy) { + if (data.sortBy.startsWith('topic.')) { + topicFields.push(data.sortBy.split('.')[1]); + } else if (data.sortBy.startsWith('user.')) { + postFields.push('uid'); + } else if (data.sortBy.startsWith('category.')) { + categoryFields.push(data.sortBy.split('.')[1]); + } else if (data.sortBy.startsWith('teaser')) { + topicFields.push('teaserPid'); + } + } + var posts; async.waterfall([ function(next) { + var keys = pids.map(function(pid) { + return 'post:' + pid; + }); db.getObjectsFields(keys, postFields, next); }, function(_posts, next) { posts = _posts; - if (!topicFields.length) { - return callback(null, posts); - } - var topicKeys = posts.map(function(post) { - return 'topic:' + post.tid; - }); - db.getObjectsFields(topicKeys, topicFields, next); + + async.parallel({ + users: function(next) { + if (data.sortBy && data.sortBy.startsWith('user')) { + var uids = posts.map(function(post) { + return post.uid; + }); + user.getMultipleUserFields(uids, ['username'], next); + } else { + next(); + } + }, + topics: function(next) { + if (!topicFields.length) { + return next(); + } + + var topics; + async.waterfall([ + function(next) { + var topicKeys = posts.map(function(post) { + return 'topic:' + post.tid; + }); + db.getObjectsFields(topicKeys, topicFields, next); + }, + function(_topics, next) { + topics = _topics; + + async.parallel({ + teasers: function(next) { + if (topicFields.indexOf('teaserPid') !== -1) { + var teaserKeys = topics.map(function(topic) { + return 'post:' + topic.teaserPid; + }); + db.getObjectsFields(teaserKeys, ['timestamp'], next); + } else { + next(); + } + }, + categories: function(next) { + if (!categoryFields.length) { + return next(); + } + var cids = topics.map(function(topic) { + return 'category:' + topic.cid; + }); + db.getObjectsFields(cids, categoryFields, next); + } + }, next); + } + ], function(err, results) { + if (err) { + return next(err); + } + + topics.forEach(function(topic, index) { + if (topic && results.categories && results.categories[index]) { + topic.category = results.categories[index]; + } + if (topic && results.teasers && results.teasers[index]) { + topic.teaser = results.teasers[index]; + } + }); + + next(null, topics); + }); + } + }, next); }, - function(topics, next) { + function(results, next) { + posts.forEach(function(post, index) { - post.topic = topics[index]; + if (results.topics && results.topics[index]) { + post.topic = results.topics[index]; + if (results.topics[index].category) { + post.category = results.topics[index].category; + } + if (results.topics[index].teaser) { + post.teaser = results.topics[index].teaser; + } + } + + if (results.users && results.users[index]) { + post.user = results.users[index]; + } }); next(null, posts); @@ -236,8 +335,75 @@ function filterByTimerange(posts, timeRange, timeFilter) { } function sortPosts(posts, data) { + if (!posts.length) { + return; + } + data.sortBy = data.sortBy || 'timestamp'; + data.sortDirection = data.sortDirection || 'desc'; + if (data.sortBy === 'timestamp') { + if (data.sortDirection === 'desc') { + posts.sort(function(p1, p2) { + return p2.timestamp - p1.timestamp; + }); + } else { + posts.sort(function(p1, p2) { + return p1.timestamp - p2.timestamp; + }); + } + + return; + } + + var firstPost = posts[0]; + var fields = data.sortBy.split('.'); + + if (!fields || fields.length !== 2 || !firstPost[fields[0]] || !firstPost[fields[0]][fields[1]]) { + return; + } + + var value = firstPost[fields[0]][fields[1]]; + var isNumeric = utils.isNumber(value); + + if (isNumeric) { + if (data.sortDirection === 'desc') { + sortDescendingNumeric(posts, fields); + } else { + sortAscendingNumeric(posts, fields); + } + } else { + if (data.sortDirection === 'desc') { + sortDescendingAlpha(posts, fields); + } else { + sortAscendingAlpha(posts, fields); + } + } +} + +function sortAscendingNumeric(posts, fields) { + posts.sort(function(p1, p2) { + return p1[fields[0]][fields[1]] - p2[fields[0]][fields[1]]; + }); +} + +function sortDescendingNumeric(posts, fields) { posts.sort(function(p1, p2) { - return p2.timestamp - p1.timestamp; + return p2[fields[0]][fields[1]] - p1[fields[0]][fields[1]]; + }); +} + +function sortAscendingAlpha(posts, fields) { + posts.sort(function(p1, p2) { + if (p1[fields[0]][fields[1]] > p2[fields[0]][fields[1]]) return -1; + if (p1[fields[0]][fields[1]] < p2[fields[0]][fields[1]]) return 1; + return 0; + }); +} + +function sortDescendingAlpha(posts, fields) { + posts.sort(function(p1, p2) { + if (p1[fields[0]][fields[1]] < p2[fields[0]][fields[1]]) return -1; + if (p1[fields[0]][fields[1]] > p2[fields[0]][fields[1]]) return 1; + return 0; }); } @@ -311,6 +477,10 @@ function searchInTags(query, callback) { } function getMainPids(tids, callback) { + if (!Array.isArray(tids) || !tids.length) { + return callback(null, []); + } + topics.getTopicsFields(tids, ['mainPid'], function(err, topics) { if (err) { return callback(err);