parent
							
								
									c378e5fc94
								
							
						
					
					
						commit
						eaf0d5e637
					
				
					 9 changed files with 172 additions and 27 deletions
				
			
		| 
						 | 
					@ -5,6 +5,7 @@ import * as mongo from 'mongodb';
 | 
				
			||||||
import $ from 'cafy';
 | 
					import $ from 'cafy';
 | 
				
			||||||
const escapeRegexp = require('escape-regexp');
 | 
					const escapeRegexp = require('escape-regexp');
 | 
				
			||||||
import Post from '../../models/post';
 | 
					import Post from '../../models/post';
 | 
				
			||||||
 | 
					import User from '../../models/user';
 | 
				
			||||||
import serialize from '../../serializers/post';
 | 
					import serialize from '../../serializers/post';
 | 
				
			||||||
import config from '../../../conf';
 | 
					import config from '../../../conf';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,33 +17,98 @@ import config from '../../../conf';
 | 
				
			||||||
 * @return {Promise<any>}
 | 
					 * @return {Promise<any>}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
module.exports = (params, me) => new Promise(async (res, rej) => {
 | 
					module.exports = (params, me) => new Promise(async (res, rej) => {
 | 
				
			||||||
	// Get 'query' parameter
 | 
						// Get 'text' parameter
 | 
				
			||||||
	const [query, queryError] = $(params.query).string().pipe(x => x != '').$;
 | 
						const [text, textError] = $(params.text).optional.string().$;
 | 
				
			||||||
	if (queryError) return rej('invalid query param');
 | 
						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
 | 
						// Get 'offset' parameter
 | 
				
			||||||
	const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
 | 
						const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
 | 
				
			||||||
	if (offsetErr) return rej('invalid offset param');
 | 
						if (offsetErr) return rej('invalid offset param');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get 'max' parameter
 | 
						// Get 'limit' parameter
 | 
				
			||||||
	const [max = 10, maxErr] = $(params.max).optional.number().range(1, 30).$;
 | 
						const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 30).$;
 | 
				
			||||||
	if (maxErr) return rej('invalid max param');
 | 
						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
 | 
						// If not, search by MongoDB
 | 
				
			||||||
	(config.elasticsearch.enable ? byElasticsearch : byNative)
 | 
						(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
 | 
					// Search by MongoDB
 | 
				
			||||||
async function byNative(res, rej, me, query, offset, max) {
 | 
					async function byNative(res, rej, me, text, userId, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
 | 
				
			||||||
	const escapedQuery = escapeRegexp(query);
 | 
						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
 | 
						// Search posts
 | 
				
			||||||
	const posts = await Post
 | 
						const posts = await Post
 | 
				
			||||||
		.find({
 | 
							.find(q, {
 | 
				
			||||||
			text: new RegExp(escapedQuery)
 | 
					 | 
				
			||||||
		}, {
 | 
					 | 
				
			||||||
			sort: {
 | 
								sort: {
 | 
				
			||||||
				_id: -1
 | 
									_id: -1
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
| 
						 | 
					@ -56,7 +122,7 @@ async function byNative(res, rej, me, query, offset, max) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Search by Elasticsearch
 | 
					// 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');
 | 
						const es = require('../../db/elasticsearch');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	es.search({
 | 
						es.search({
 | 
				
			||||||
| 
						 | 
					@ -68,7 +134,7 @@ async function byElasticsearch(res, rej, me, query, offset, max) {
 | 
				
			||||||
			query: {
 | 
								query: {
 | 
				
			||||||
				simple_query_string: {
 | 
									simple_query_string: {
 | 
				
			||||||
					fields: ['text'],
 | 
										fields: ['text'],
 | 
				
			||||||
					query: query,
 | 
										query: text,
 | 
				
			||||||
					default_operator: 'and'
 | 
										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/messaging/:user',      messaging);
 | 
				
			||||||
	route('/i/mentions',             mentions);
 | 
						route('/i/mentions',             mentions);
 | 
				
			||||||
	route('/post::post',             post);
 | 
						route('/post::post',             post);
 | 
				
			||||||
	route('/search::query',          search);
 | 
						route('/search',                 search);
 | 
				
			||||||
	route('/:user',                  user.bind(null, 'home'));
 | 
						route('/:user',                  user.bind(null, 'home'));
 | 
				
			||||||
	route('/:user/graphs',           user.bind(null, 'graphs'));
 | 
						route('/:user/graphs',           user.bind(null, 'graphs'));
 | 
				
			||||||
	route('/:user/:post',            post);
 | 
						route('/:user/:post',            post);
 | 
				
			||||||
| 
						 | 
					@ -47,7 +47,7 @@ export default (mios: MiOS) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function search(ctx) {
 | 
						function search(ctx) {
 | 
				
			||||||
		const el = document.createElement('mk-search-page');
 | 
							const el = document.createElement('mk-search-page');
 | 
				
			||||||
		el.setAttribute('query', ctx.params.query);
 | 
							el.setAttribute('query', ctx.querystring.substr(2));
 | 
				
			||||||
		mount(el);
 | 
							mount(el);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,6 +33,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	</style>
 | 
						</style>
 | 
				
			||||||
	<script>
 | 
						<script>
 | 
				
			||||||
 | 
							import parse from '../../common/scripts/parse-search-query';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.mixin('api');
 | 
							this.mixin('api');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.query = this.opts.query;
 | 
							this.query = this.opts.query;
 | 
				
			||||||
| 
						 | 
					@ -45,9 +47,7 @@
 | 
				
			||||||
			document.addEventListener('keydown', this.onDocumentKeydown);
 | 
								document.addEventListener('keydown', this.onDocumentKeydown);
 | 
				
			||||||
			window.addEventListener('scroll', this.onScroll);
 | 
								window.addEventListener('scroll', this.onScroll);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			this.api('posts/search', {
 | 
								this.api('posts/search', parse(this.query)).then(posts => {
 | 
				
			||||||
				query: this.query
 | 
					 | 
				
			||||||
			}).then(posts => {
 | 
					 | 
				
			||||||
				this.update({
 | 
									this.update({
 | 
				
			||||||
					isLoading: false,
 | 
										isLoading: false,
 | 
				
			||||||
					isEmpty: posts.length == 0
 | 
										isEmpty: posts.length == 0
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -180,7 +180,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.onsubmit = e => {
 | 
							this.onsubmit = e => {
 | 
				
			||||||
			e.preventDefault();
 | 
								e.preventDefault();
 | 
				
			||||||
			this.page('/search:' + this.refs.q.value);
 | 
								this.page('/search?q=' + encodeURIComponent(this.refs.q.value));
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	</script>
 | 
						</script>
 | 
				
			||||||
</mk-ui-header-search>
 | 
					</mk-ui-header-search>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ export default (mios: MiOS) => {
 | 
				
			||||||
	route('/i/settings/authorized-apps', settingsAuthorizedApps);
 | 
						route('/i/settings/authorized-apps', settingsAuthorizedApps);
 | 
				
			||||||
	route('/post/new',                   newPost);
 | 
						route('/post/new',                   newPost);
 | 
				
			||||||
	route('/post::post',                 post);
 | 
						route('/post::post',                 post);
 | 
				
			||||||
	route('/search::query',              search);
 | 
						route('/search',                     search);
 | 
				
			||||||
	route('/:user',                      user.bind(null, 'overview'));
 | 
						route('/:user',                      user.bind(null, 'overview'));
 | 
				
			||||||
	route('/:user/graphs',               user.bind(null, 'graphs'));
 | 
						route('/:user/graphs',               user.bind(null, 'graphs'));
 | 
				
			||||||
	route('/:user/followers',            userFollowers);
 | 
						route('/:user/followers',            userFollowers);
 | 
				
			||||||
| 
						 | 
					@ -83,7 +83,7 @@ export default (mios: MiOS) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function search(ctx) {
 | 
						function search(ctx) {
 | 
				
			||||||
		const el = document.createElement('mk-search-page');
 | 
							const el = document.createElement('mk-search-page');
 | 
				
			||||||
		el.setAttribute('query', ctx.params.query);
 | 
							el.setAttribute('query', ctx.querystring.substr(2));
 | 
				
			||||||
		mount(el);
 | 
							mount(el);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,8 @@
 | 
				
			||||||
				width calc(100% - 32px)
 | 
									width calc(100% - 32px)
 | 
				
			||||||
	</style>
 | 
						</style>
 | 
				
			||||||
	<script>
 | 
						<script>
 | 
				
			||||||
 | 
							import parse from '../../common/scripts/parse-search-query';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.mixin('api');
 | 
							this.mixin('api');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.max = 30;
 | 
							this.max = 30;
 | 
				
			||||||
| 
						 | 
					@ -24,9 +26,7 @@
 | 
				
			||||||
		this.withMedia = this.opts.withMedia;
 | 
							this.withMedia = this.opts.withMedia;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.init = new Promise((res, rej) => {
 | 
							this.init = new Promise((res, rej) => {
 | 
				
			||||||
			this.api('posts/search', {
 | 
								this.api('posts/search', parse(this.query)).then(posts => {
 | 
				
			||||||
				query: this.query
 | 
					 | 
				
			||||||
			}).then(posts => {
 | 
					 | 
				
			||||||
				res(posts);
 | 
									res(posts);
 | 
				
			||||||
				this.trigger('loaded');
 | 
									this.trigger('loaded');
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -413,7 +413,7 @@
 | 
				
			||||||
		this.search = () => {
 | 
							this.search = () => {
 | 
				
			||||||
			const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%');
 | 
								const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%');
 | 
				
			||||||
			if (query == null || query == '') return;
 | 
								if (query == null || query == '') return;
 | 
				
			||||||
			this.page('/search:' + query);
 | 
								this.page('/search?q=' + encodeURIComponent(query));
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	</script>
 | 
						</script>
 | 
				
			||||||
</mk-ui-nav>
 | 
					</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…
	
	Add table
		Add a link
		
	
		Reference in a new issue