From eaf0d5e637e0fd5be62b7ccf940ba1bcebeba786 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 21 Dec 2017 04:01:44 +0900 Subject: [PATCH] #1017 #155 --- src/api/endpoints/posts/search.ts | 96 ++++++++++++++++--- .../app/common/scripts/parse-search-query.ts | 41 ++++++++ src/web/app/desktop/router.ts | 4 +- src/web/app/desktop/tags/search-posts.tag | 6 +- src/web/app/desktop/tags/ui.tag | 2 +- src/web/app/mobile/router.ts | 4 +- src/web/app/mobile/tags/search-posts.tag | 6 +- src/web/app/mobile/tags/ui.tag | 2 +- src/web/docs/search.ja.pug | 38 ++++++++ 9 files changed, 172 insertions(+), 27 deletions(-) create mode 100644 src/web/app/common/scripts/parse-search-query.ts create mode 100644 src/web/docs/search.ja.pug diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts index b434f6434..dba7a53b5 100644 --- a/src/api/endpoints/posts/search.ts +++ b/src/api/endpoints/posts/search.ts @@ -5,6 +5,7 @@ import * as mongo from 'mongodb'; import $ from 'cafy'; const escapeRegexp = require('escape-regexp'); import Post from '../../models/post'; +import User from '../../models/user'; import serialize from '../../serializers/post'; import config from '../../../conf'; @@ -16,33 +17,98 @@ import config from '../../../conf'; * @return {Promise} */ module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'query' parameter - const [query, queryError] = $(params.query).string().pipe(x => x != '').$; - if (queryError) return rej('invalid query param'); + // Get 'text' parameter + const [text, textError] = $(params.text).optional.string().$; + if (textError) return rej('invalid text param'); + + // Get 'user_id' parameter + const [userId, userIdErr] = $(params.user_id).optional.id().$; + if (userIdErr) return rej('invalid user_id param'); + + // Get 'username' parameter + const [username, usernameErr] = $(params.username).optional.string().$; + if (usernameErr) return rej('invalid username param'); + + // Get 'include_replies' parameter + const [includeReplies = true, includeRepliesErr] = $(params.include_replies).optional.boolean().$; + if (includeRepliesErr) return rej('invalid include_replies param'); + + // Get 'with_media' parameter + const [withMedia = false, withMediaErr] = $(params.with_media).optional.boolean().$; + if (withMediaErr) return rej('invalid with_media param'); + + // Get 'since_date' parameter + const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$; + if (sinceDateErr) throw 'invalid since_date param'; + + // Get 'until_date' parameter + const [untilDate, untilDateErr] = $(params.until_date).optional.number().$; + if (untilDateErr) throw 'invalid until_date param'; // Get 'offset' parameter const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; if (offsetErr) return rej('invalid offset param'); - // Get 'max' parameter - const [max = 10, maxErr] = $(params.max).optional.number().range(1, 30).$; - if (maxErr) return rej('invalid max param'); + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 30).$; + if (limitErr) return rej('invalid limit param'); - // If Elasticsearch is available, search by $ + let user = userId; + + if (user == null && username != null) { + const _user = await User.findOne({ + username_lower: username.toLowerCase() + }); + if (_user) { + user = _user._id; + } + } + + // If Elasticsearch is available, search by it // If not, search by MongoDB (config.elasticsearch.enable ? byElasticsearch : byNative) - (res, rej, me, query, offset, max); + (res, rej, me, text, user, includeReplies, withMedia, sinceDate, untilDate, offset, limit); }); // Search by MongoDB -async function byNative(res, rej, me, query, offset, max) { - const escapedQuery = escapeRegexp(query); +async function byNative(res, rej, me, text, userId, includeReplies, withMedia, sinceDate, untilDate, offset, max) { + const q: any = {}; + + if (text) { + q.$and = text.split(' ').map(x => ({ + text: new RegExp(escapeRegexp(x)) + })); + } + + if (userId) { + q.user_id = userId; + } + + if (!includeReplies) { + q.reply_id = null; + } + + if (withMedia) { + q.media_ids = { + $exists: true, + $ne: null + }; + } + + if (sinceDate) { + q.created_at = { + $gt: new Date(sinceDate) + }; + } + + if (untilDate) { + if (q.created_at == undefined) q.created_at = {}; + q.created_at.$lt = new Date(untilDate); + } // Search posts const posts = await Post - .find({ - text: new RegExp(escapedQuery) - }, { + .find(q, { sort: { _id: -1 }, @@ -56,7 +122,7 @@ async function byNative(res, rej, me, query, offset, max) { } // Search by Elasticsearch -async function byElasticsearch(res, rej, me, query, offset, max) { +async function byElasticsearch(res, rej, me, text, userId, includeReplies, withMedia, sinceDate, untilDate, offset, max) { const es = require('../../db/elasticsearch'); es.search({ @@ -68,7 +134,7 @@ async function byElasticsearch(res, rej, me, query, offset, max) { query: { simple_query_string: { fields: ['text'], - query: query, + query: text, default_operator: 'and' } }, diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts new file mode 100644 index 000000000..adcbfbb8f --- /dev/null +++ b/src/web/app/common/scripts/parse-search-query.ts @@ -0,0 +1,41 @@ +export default function(qs: string) { + const q = { + text: '' + }; + + qs.split(' ').forEach(x => { + if (/^([a-z_]+?):(.+?)$/.test(x)) { + const [key, value] = x.split(':'); + switch (key) { + case 'user': + q['username'] = value; + break; + case 'reply': + q['include_replies'] = value == 'true'; + break; + case 'media': + q['with_media'] = value == 'true'; + break; + case 'until': + case 'since': + // YYYY-MM-DD + if (/^[0-9]+\-[0-9]+\-[0-9]+$/) { + const [yyyy, mm, dd] = value.split('-'); + q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime(); + } + break; + default: + q[key] = value; + break; + } + } else { + q.text += x + ' '; + } + }); + + if (q.text) { + q.text = q.text.trim(); + } + + return q; +} diff --git a/src/web/app/desktop/router.ts b/src/web/app/desktop/router.ts index 27b63ab2e..ce68c4f2d 100644 --- a/src/web/app/desktop/router.ts +++ b/src/web/app/desktop/router.ts @@ -16,7 +16,7 @@ export default (mios: MiOS) => { route('/i/messaging/:user', messaging); route('/i/mentions', mentions); route('/post::post', post); - route('/search::query', search); + route('/search', search); route('/:user', user.bind(null, 'home')); route('/:user/graphs', user.bind(null, 'graphs')); route('/:user/:post', post); @@ -47,7 +47,7 @@ export default (mios: MiOS) => { function search(ctx) { const el = document.createElement('mk-search-page'); - el.setAttribute('query', ctx.params.query); + el.setAttribute('query', ctx.querystring.substr(2)); mount(el); } diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag index 52f765d1a..c6b24837d 100644 --- a/src/web/app/desktop/tags/search-posts.tag +++ b/src/web/app/desktop/tags/search-posts.tag @@ -33,6 +33,8 @@ diff --git a/src/web/app/mobile/router.ts b/src/web/app/mobile/router.ts index d0c6add0b..afb9aa620 100644 --- a/src/web/app/mobile/router.ts +++ b/src/web/app/mobile/router.ts @@ -23,7 +23,7 @@ export default (mios: MiOS) => { route('/i/settings/authorized-apps', settingsAuthorizedApps); route('/post/new', newPost); route('/post::post', post); - route('/search::query', search); + route('/search', search); route('/:user', user.bind(null, 'overview')); route('/:user/graphs', user.bind(null, 'graphs')); route('/:user/followers', userFollowers); @@ -83,7 +83,7 @@ export default (mios: MiOS) => { function search(ctx) { const el = document.createElement('mk-search-page'); - el.setAttribute('query', ctx.params.query); + el.setAttribute('query', ctx.querystring.substr(2)); mount(el); } diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag index 967764bc2..023a35bf6 100644 --- a/src/web/app/mobile/tags/search-posts.tag +++ b/src/web/app/mobile/tags/search-posts.tag @@ -15,6 +15,8 @@ width calc(100% - 32px) diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug new file mode 100644 index 000000000..f7ec9519f --- /dev/null +++ b/src/web/docs/search.ja.pug @@ -0,0 +1,38 @@ +h1 検索 + +p 投稿を検索することができます。 +p + | キーワードを半角スペースで区切ると、and検索になります。 + | 例えば、「git コミット」と検索すると、「gitで編集したファイルの特定の行だけコミットする方法がわからない」などがマッチします。 + +section + h2 オプション + p + | オプションを使用して、より高度な検索をすることもできます。 + | オプションを指定するには、「オプション名:値」という形式でクエリに含めます。 + p 利用可能なオプション一覧です: + + table + thead + tr + th 名前 + th 説明 + tbody + tr + td user + td ユーザー名。投稿者を限定します。 + tr + td reply + td 返信を含めるか否か。(trueかfalse) + tr + td media + td メディアが添付されているか。(trueかfalse) + tr + td until + td 上限の日時。(YYYY-MM-DD) + tr + td since + td 下限の日時。(YYYY-MM-DD) + + p 例えば、「@syuiloの2017年11月1日から2017年12月31日までの『Misskey』というテキストを含む返信ではない投稿」を検索したい場合、クエリは以下のようになります: + code user:syuilo since:2017-11-01 until:2017-12-31 reply:false Misskey