forked from FoundKeyGang/FoundKey
parent
c378e5fc94
commit
eaf0d5e637
9 changed files with 172 additions and 27 deletions
src
api/endpoints/posts
web
app
common/scripts
desktop
mobile
docs
|
@ -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<any>}
|
||||
*/
|
||||
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'
|
||||
}
|
||||
},
|
||||
|
|
41
src/web/app/common/scripts/parse-search-query.ts
Normal file
41
src/web/app/common/scripts/parse-search-query.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,8 @@
|
|||
|
||||
</style>
|
||||
<script>
|
||||
import parse from '../../common/scripts/parse-search-query';
|
||||
|
||||
this.mixin('api');
|
||||
|
||||
this.query = this.opts.query;
|
||||
|
@ -45,9 +47,7 @@
|
|||
document.addEventListener('keydown', this.onDocumentKeydown);
|
||||
window.addEventListener('scroll', this.onScroll);
|
||||
|
||||
this.api('posts/search', {
|
||||
query: this.query
|
||||
}).then(posts => {
|
||||
this.api('posts/search', parse(this.query)).then(posts => {
|
||||
this.update({
|
||||
isLoading: false,
|
||||
isEmpty: posts.length == 0
|
||||
|
|
|
@ -180,7 +180,7 @@
|
|||
|
||||
this.onsubmit = e => {
|
||||
e.preventDefault();
|
||||
this.page('/search:' + this.refs.q.value);
|
||||
this.page('/search?q=' + encodeURIComponent(this.refs.q.value));
|
||||
};
|
||||
</script>
|
||||
</mk-ui-header-search>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
width calc(100% - 32px)
|
||||
</style>
|
||||
<script>
|
||||
import parse from '../../common/scripts/parse-search-query';
|
||||
|
||||
this.mixin('api');
|
||||
|
||||
this.max = 30;
|
||||
|
@ -24,9 +26,7 @@
|
|||
this.withMedia = this.opts.withMedia;
|
||||
|
||||
this.init = new Promise((res, rej) => {
|
||||
this.api('posts/search', {
|
||||
query: this.query
|
||||
}).then(posts => {
|
||||
this.api('posts/search', parse(this.query)).then(posts => {
|
||||
res(posts);
|
||||
this.trigger('loaded');
|
||||
});
|
||||
|
|
|
@ -413,7 +413,7 @@
|
|||
this.search = () => {
|
||||
const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%');
|
||||
if (query == null || query == '') return;
|
||||
this.page('/search:' + query);
|
||||
this.page('/search?q=' + encodeURIComponent(query));
|
||||
};
|
||||
</script>
|
||||
</mk-ui-nav>
|
||||
|
|
38
src/web/docs/search.ja.pug
Normal file
38
src/web/docs/search.ja.pug
Normal file
|
@ -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
|
Loading…
Reference in a new issue