mirror of
https://github.com/flarum/framework.git
synced 2024-11-27 11:03:37 +08:00
Improve search performance (#1339)
* Improve fulltext gambit * Only search in visible posts This change relies on the `visibility-scoping` branch to be merged. * Change posts table to use InnoDB engine Doing a JOIN between an InnoDB table (discussions) and a MyISAM table (posts) is very very (very) bad for performance. FULLTEXT indexes are fully supported in InnoDB now, and it is a superior engine in every other way, so there is no longer any reason to be using MyISAM. * Use ::class * Only search for comment posts * Add fulltext index to discussions.title * Fix migration not working if there is a table prefix * Update frontend appearance * Apply fixes from StyleCI [ci skip] [skip ci] * Show search result excerpts on mobile
This commit is contained in:
parent
80ec3b5e17
commit
322a84f516
165
js/forum/dist/app.js
vendored
165
js/forum/dist/app.js
vendored
|
@ -19740,10 +19740,10 @@ System.register('flarum/components/CommentPost', ['flarum/components/Post', 'fla
|
|||
});;
|
||||
'use strict';
|
||||
|
||||
System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils/ItemList', 'flarum/components/ComposerButton', 'flarum/helpers/listItems', 'flarum/utils/classList', 'flarum/utils/computed'], function (_export, _context) {
|
||||
System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils/ItemList', 'flarum/components/ComposerButton', 'flarum/helpers/listItems', 'flarum/utils/classList'], function (_export, _context) {
|
||||
"use strict";
|
||||
|
||||
var Component, ItemList, ComposerButton, listItems, classList, computed, Composer;
|
||||
var Component, ItemList, ComposerButton, listItems, classList, Composer;
|
||||
return {
|
||||
setters: [function (_flarumComponent) {
|
||||
Component = _flarumComponent.default;
|
||||
|
@ -19755,8 +19755,6 @@ System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils
|
|||
listItems = _flarumHelpersListItems.default;
|
||||
}, function (_flarumUtilsClassList) {
|
||||
classList = _flarumUtilsClassList.default;
|
||||
}, function (_flarumUtilsComputed) {
|
||||
computed = _flarumUtilsComputed.default;
|
||||
}],
|
||||
execute: function () {
|
||||
Composer = function (_Component) {
|
||||
|
@ -19791,28 +19789,6 @@ System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils
|
|||
* @type {Boolean}
|
||||
*/
|
||||
this.active = false;
|
||||
|
||||
/**
|
||||
* Computed the composer's current height, based on the intended height, and
|
||||
* the composer's current state. This will be applied to the composer's
|
||||
* content's DOM element.
|
||||
*
|
||||
* @return {Integer}
|
||||
*/
|
||||
this.computedHeight = computed('height', 'position', function (height, position) {
|
||||
// If the composer is minimized, then we don't want to set a height; we'll
|
||||
// let the CSS decide how high it is. If it's fullscreen, then we need to
|
||||
// make it as high as the window.
|
||||
if (position === Composer.PositionEnum.MINIMIZED) {
|
||||
return '';
|
||||
} else if (position === Composer.PositionEnum.FULLSCREEN) {
|
||||
return $(window).height();
|
||||
}
|
||||
|
||||
// Otherwise, if it's normal or hidden, then we use the intended height.
|
||||
// We don't let the composer get too small or too big, though.
|
||||
return Math.max(200, Math.min(height, $(window).height() - $('#header').outerHeight()));
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: 'view',
|
||||
|
@ -19853,12 +19829,6 @@ System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils
|
|||
value: function config(isInitialized, context) {
|
||||
var _this2 = this;
|
||||
|
||||
var defaultHeight = void 0;
|
||||
|
||||
if (!isInitialized) {
|
||||
defaultHeight = this.$().height();
|
||||
}
|
||||
|
||||
// Set the height of the Composer element and its contents on each redraw,
|
||||
// so that they do not lose it if their DOM elements are recreated.
|
||||
this.updateHeight();
|
||||
|
@ -19869,11 +19839,8 @@ System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils
|
|||
// routes, we will flag the DOM to be retained across route changes.
|
||||
context.retain = true;
|
||||
|
||||
// Initialize the composer's intended height based on what the user has set
|
||||
// it at previously, or otherwise the composer's default height. After that,
|
||||
// we'll hide the composer.
|
||||
this.height = localStorage.getItem('composerHeight') || defaultHeight;
|
||||
this.$().hide().css('bottom', -this.height);
|
||||
this.initializeHeight();
|
||||
this.$().hide().css('bottom', -this.computedHeight());
|
||||
|
||||
// Whenever any of the inputs inside the composer are have focus, we want to
|
||||
// add a class to the composer to draw attention to it.
|
||||
|
@ -19932,8 +19899,7 @@ System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils
|
|||
// height so that it fills the height of the composer, and update the
|
||||
// body's padding.
|
||||
var deltaPixels = this.mouseStart - e.clientY;
|
||||
this.height = this.heightStart + deltaPixels;
|
||||
this.updateHeight();
|
||||
this.changeHeight(this.heightStart + deltaPixels);
|
||||
|
||||
// Update the body's padding-bottom so that no content on the page will ever
|
||||
// get permanently hidden behind the composer. If the user is already
|
||||
|
@ -19942,8 +19908,6 @@ System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils
|
|||
var scrollTop = $(window).scrollTop();
|
||||
var anchorToBottom = scrollTop > 0 && scrollTop + $(window).height() >= $(document).height();
|
||||
this.updateBodyPadding(anchorToBottom);
|
||||
|
||||
localStorage.setItem('composerHeight', this.height);
|
||||
}
|
||||
}, {
|
||||
key: 'onmouseup',
|
||||
|
@ -20172,6 +20136,54 @@ System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils
|
|||
|
||||
return items;
|
||||
}
|
||||
}, {
|
||||
key: 'initializeHeight',
|
||||
value: function initializeHeight() {
|
||||
this.height = localStorage.getItem('composerHeight');
|
||||
|
||||
if (!this.height) {
|
||||
this.height = this.defaultHeight();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: 'defaultHeight',
|
||||
value: function defaultHeight() {
|
||||
return this.$().height();
|
||||
}
|
||||
}, {
|
||||
key: 'minimumHeight',
|
||||
value: function minimumHeight() {
|
||||
return 200;
|
||||
}
|
||||
}, {
|
||||
key: 'maximumHeight',
|
||||
value: function maximumHeight() {
|
||||
return $(window).height() - $('#header').outerHeight();
|
||||
}
|
||||
}, {
|
||||
key: 'computedHeight',
|
||||
value: function computedHeight() {
|
||||
// If the composer is minimized, then we don't want to set a height; we'll
|
||||
// let the CSS decide how high it is. If it's fullscreen, then we need to
|
||||
// make it as high as the window.
|
||||
if (this.position === Composer.PositionEnum.MINIMIZED) {
|
||||
return '';
|
||||
} else if (this.position === Composer.PositionEnum.FULLSCREEN) {
|
||||
return $(window).height();
|
||||
}
|
||||
|
||||
// Otherwise, if it's normal or hidden, then we use the intended height.
|
||||
// We don't let the composer get too small or too big, though.
|
||||
return Math.max(this.minimumHeight(), Math.min(this.height, this.maximumHeight()));
|
||||
}
|
||||
}, {
|
||||
key: 'changeHeight',
|
||||
value: function changeHeight(height) {
|
||||
this.height = height;
|
||||
this.updateHeight();
|
||||
|
||||
localStorage.setItem('composerHeight', this.height);
|
||||
}
|
||||
}]);
|
||||
return Composer;
|
||||
}(Component);
|
||||
|
@ -20605,7 +20617,7 @@ System.register('flarum/components/DiscussionList', ['flarum/Component', 'flarum
|
|||
|
||||
return m(
|
||||
'div',
|
||||
{ className: 'DiscussionList' },
|
||||
{ className: 'DiscussionList' + (this.props.params.q ? ' DiscussionList--searchResults' : '') },
|
||||
m(
|
||||
'ul',
|
||||
{ className: 'DiscussionList-discussions' },
|
||||
|
@ -20634,7 +20646,7 @@ System.register('flarum/components/DiscussionList', ['flarum/Component', 'flarum
|
|||
if (this.props.params.q) {
|
||||
params.filter.q = this.props.params.q;
|
||||
|
||||
params.include.push('relevantPosts', 'relevantPosts.discussion', 'relevantPosts.user');
|
||||
params.include.push('mostRelevantPost', 'mostRelevantPost.user');
|
||||
}
|
||||
|
||||
return params;
|
||||
|
@ -20809,8 +20821,6 @@ System.register('flarum/components/DiscussionListItem', ['flarum/Component', 'fl
|
|||
}, {
|
||||
key: 'view',
|
||||
value: function view() {
|
||||
var _this3 = this;
|
||||
|
||||
var retain = this.subtree.retain();
|
||||
|
||||
if (retain) return retain;
|
||||
|
@ -20820,11 +20830,22 @@ System.register('flarum/components/DiscussionListItem', ['flarum/Component', 'fl
|
|||
var isUnread = discussion.isUnread();
|
||||
var isRead = discussion.isRead();
|
||||
var showUnread = !this.showRepliesCount() && isUnread;
|
||||
var jumpTo = Math.min(discussion.lastPostNumber(), (discussion.readNumber() || 0) + 1);
|
||||
var relevantPosts = this.props.params.q ? discussion.relevantPosts() : [];
|
||||
var jumpTo = 0;
|
||||
var controls = DiscussionControls.controls(discussion, this).toArray();
|
||||
var attrs = this.attrs();
|
||||
|
||||
if (this.props.params.q) {
|
||||
var post = discussion.mostRelevantPost();
|
||||
if (post) {
|
||||
jumpTo = post.number();
|
||||
}
|
||||
|
||||
var phrase = this.props.params.q;
|
||||
this.highlightRegExp = new RegExp(phrase + '|' + phrase.trim().replace(/\s+/g, '|'), 'gi');
|
||||
} else {
|
||||
jumpTo = Math.min(discussion.lastPostNumber(), (discussion.readNumber() || 0) + 1);
|
||||
}
|
||||
|
||||
return m(
|
||||
'div',
|
||||
attrs,
|
||||
|
@ -20867,7 +20888,7 @@ System.register('flarum/components/DiscussionListItem', ['flarum/Component', 'fl
|
|||
m(
|
||||
'h3',
|
||||
{ className: 'DiscussionListItem-title' },
|
||||
highlight(discussion.title(), this.props.params.q)
|
||||
highlight(discussion.title(), this.highlightRegExp)
|
||||
),
|
||||
m(
|
||||
'ul',
|
||||
|
@ -20881,14 +20902,7 @@ System.register('flarum/components/DiscussionListItem', ['flarum/Component', 'fl
|
|||
onclick: this.markAsRead.bind(this),
|
||||
title: showUnread ? app.translator.trans('core.forum.discussion_list.mark_as_read_tooltip') : '' },
|
||||
abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'repliesCount']())
|
||||
),
|
||||
relevantPosts && relevantPosts.length ? m(
|
||||
'div',
|
||||
{ className: 'DiscussionListItem-relevantPosts' },
|
||||
relevantPosts.map(function (post) {
|
||||
return PostPreview.component({ post: post, highlight: _this3.props.params.q });
|
||||
})
|
||||
) : ''
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -20940,10 +20954,19 @@ System.register('flarum/components/DiscussionListItem', ['flarum/Component', 'fl
|
|||
value: function infoItems() {
|
||||
var items = new ItemList();
|
||||
|
||||
items.add('terminalPost', TerminalPost.component({
|
||||
discussion: this.props.discussion,
|
||||
lastPost: !this.showStartPost()
|
||||
}));
|
||||
if (this.props.params.q) {
|
||||
var post = this.props.discussion.mostRelevantPost() || this.props.discussion.startPost();
|
||||
|
||||
if (post && post.contentType() === 'comment') {
|
||||
var excerpt = highlight(post.contentPlain(), this.highlightRegExp, 175);
|
||||
items.add('excerpt', excerpt, -100);
|
||||
}
|
||||
} else {
|
||||
items.add('terminalPost', TerminalPost.component({
|
||||
discussion: this.props.discussion,
|
||||
lastPost: !this.showStartPost()
|
||||
}));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
@ -21387,7 +21410,7 @@ System.register('flarum/components/DiscussionsSearchSource', ['flarum/helpers/hi
|
|||
var params = {
|
||||
filter: { q: query },
|
||||
page: { limit: 3 },
|
||||
include: 'relevantPosts,relevantPosts.discussion,relevantPosts.user'
|
||||
include: 'mostRelevantPost'
|
||||
};
|
||||
|
||||
return app.store.find('discussions', params).then(function (results) {
|
||||
|
@ -21414,24 +21437,23 @@ System.register('flarum/components/DiscussionsSearchSource', ['flarum/helpers/hi
|
|||
href: app.route('index', { q: query })
|
||||
})
|
||||
), results.map(function (discussion) {
|
||||
var relevantPosts = discussion.relevantPosts();
|
||||
var post = relevantPosts && relevantPosts[0];
|
||||
var mostRelevantPost = discussion.mostRelevantPost();
|
||||
|
||||
return m(
|
||||
'li',
|
||||
{ className: 'DiscussionSearchResult', 'data-index': 'discussions' + discussion.id() },
|
||||
m(
|
||||
'a',
|
||||
{ href: app.route.discussion(discussion, post && post.number()), config: m.route },
|
||||
{ href: app.route.discussion(discussion, mostRelevantPost && mostRelevantPost.number()), config: m.route },
|
||||
m(
|
||||
'div',
|
||||
{ className: 'DiscussionSearchResult-title' },
|
||||
highlight(discussion.title(), query)
|
||||
),
|
||||
post ? m(
|
||||
mostRelevantPost ? m(
|
||||
'div',
|
||||
{ className: 'DiscussionSearchResult-excerpt' },
|
||||
highlight(post.contentPlain(), query, 100)
|
||||
highlight(mostRelevantPost.contentPlain(), query, 100)
|
||||
) : ''
|
||||
)
|
||||
);
|
||||
|
@ -27313,6 +27335,11 @@ System.register('flarum/components/SignUpModal', ['flarum/components/Modal', 'fl
|
|||
this.footer()
|
||||
)];
|
||||
}
|
||||
}, {
|
||||
key: 'isProvided',
|
||||
value: function isProvided(field) {
|
||||
return this.props.identificationFields && this.props.identificationFields.indexOf(field) !== -1;
|
||||
}
|
||||
}, {
|
||||
key: 'body',
|
||||
value: function body() {
|
||||
|
@ -27325,7 +27352,7 @@ System.register('flarum/components/SignUpModal', ['flarum/components/Modal', 'fl
|
|||
m('input', { className: 'FormControl', name: 'username', type: 'text', placeholder: extractText(app.translator.trans('core.forum.sign_up.username_placeholder')),
|
||||
value: this.username(),
|
||||
onchange: m.withAttr('value', this.username),
|
||||
disabled: this.loading })
|
||||
disabled: this.loading || this.isProvided('username') })
|
||||
),
|
||||
m(
|
||||
'div',
|
||||
|
@ -27333,7 +27360,7 @@ System.register('flarum/components/SignUpModal', ['flarum/components/Modal', 'fl
|
|||
m('input', { className: 'FormControl', name: 'email', type: 'email', placeholder: extractText(app.translator.trans('core.forum.sign_up.email_placeholder')),
|
||||
value: this.email(),
|
||||
onchange: m.withAttr('value', this.email),
|
||||
disabled: this.loading || this.props.token && this.props.email })
|
||||
disabled: this.loading || this.isProvided('email') })
|
||||
),
|
||||
this.props.token ? '' : m(
|
||||
'div',
|
||||
|
@ -29395,7 +29422,7 @@ System.register('flarum/models/Discussion', ['flarum/Model', 'flarum/utils/compu
|
|||
return Math.max(0, commentsCount - 1);
|
||||
}),
|
||||
posts: Model.hasMany('posts'),
|
||||
relevantPosts: Model.hasMany('relevantPosts'),
|
||||
mostRelevantPost: Model.hasOne('mostRelevantPost'),
|
||||
|
||||
readTime: Model.attribute('readTime', Model.transformDate),
|
||||
readNumber: Model.attribute('readNumber'),
|
||||
|
|
|
@ -62,7 +62,7 @@ export default class DiscussionList extends Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="DiscussionList">
|
||||
<div className={'DiscussionList'+(this.props.params.q ? ' DiscussionList--searchResults' : '')}>
|
||||
<ul className="DiscussionList-discussions">
|
||||
{this.discussions.map(discussion => {
|
||||
return (
|
||||
|
@ -94,7 +94,7 @@ export default class DiscussionList extends Component {
|
|||
if (this.props.params.q) {
|
||||
params.filter.q = this.props.params.q;
|
||||
|
||||
params.include.push('relevantPosts', 'relevantPosts.discussion', 'relevantPosts.user');
|
||||
params.include.push('mostRelevantPost', 'mostRelevantPost.user');
|
||||
}
|
||||
|
||||
return params;
|
||||
|
|
|
@ -62,14 +62,24 @@ export default class DiscussionListItem extends Component {
|
|||
const isUnread = discussion.isUnread();
|
||||
const isRead = discussion.isRead();
|
||||
const showUnread = !this.showRepliesCount() && isUnread;
|
||||
const jumpTo = Math.min(discussion.lastPostNumber(), (discussion.readNumber() || 0) + 1);
|
||||
const relevantPosts = this.props.params.q ? discussion.relevantPosts() : [];
|
||||
let jumpTo = 0;
|
||||
const controls = DiscussionControls.controls(discussion, this).toArray();
|
||||
const attrs = this.attrs();
|
||||
|
||||
if (this.props.params.q) {
|
||||
const post = discussion.mostRelevantPost();
|
||||
if (post) {
|
||||
jumpTo = post.number();
|
||||
}
|
||||
|
||||
const phrase = this.props.params.q;
|
||||
this.highlightRegExp = new RegExp(phrase+'|'+phrase.trim().replace(/\s+/g, '|'), 'gi');
|
||||
} else {
|
||||
jumpTo = Math.min(discussion.lastPostNumber(), (discussion.readNumber() || 0) + 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...attrs}>
|
||||
|
||||
{controls.length ? Dropdown.component({
|
||||
icon: 'ellipsis-v',
|
||||
children: controls,
|
||||
|
@ -100,7 +110,7 @@ export default class DiscussionListItem extends Component {
|
|||
<a href={app.route.discussion(discussion, jumpTo)}
|
||||
config={m.route}
|
||||
className="DiscussionListItem-main">
|
||||
<h3 className="DiscussionListItem-title">{highlight(discussion.title(), this.props.params.q)}</h3>
|
||||
<h3 className="DiscussionListItem-title">{highlight(discussion.title(), this.highlightRegExp)}</h3>
|
||||
<ul className="DiscussionListItem-info">{listItems(this.infoItems().toArray())}</ul>
|
||||
</a>
|
||||
|
||||
|
@ -109,13 +119,6 @@ export default class DiscussionListItem extends Component {
|
|||
title={showUnread ? app.translator.trans('core.forum.discussion_list.mark_as_read_tooltip') : ''}>
|
||||
{abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'repliesCount']())}
|
||||
</span>
|
||||
|
||||
{relevantPosts && relevantPosts.length
|
||||
? <div className="DiscussionListItem-relevantPosts">
|
||||
{relevantPosts.map(post => PostPreview.component({post, highlight: this.props.params.q}))}
|
||||
</div>
|
||||
: ''}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -188,12 +191,21 @@ export default class DiscussionListItem extends Component {
|
|||
infoItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('terminalPost',
|
||||
TerminalPost.component({
|
||||
discussion: this.props.discussion,
|
||||
lastPost: !this.showStartPost()
|
||||
})
|
||||
);
|
||||
if (this.props.params.q) {
|
||||
const post = this.props.discussion.mostRelevantPost() || this.props.discussion.startPost();
|
||||
|
||||
if (post && post.contentType() === 'comment') {
|
||||
const excerpt = highlight(post.contentPlain(), this.highlightRegExp, 175);
|
||||
items.add('excerpt', excerpt, -100);
|
||||
}
|
||||
} else {
|
||||
items.add('terminalPost',
|
||||
TerminalPost.component({
|
||||
discussion: this.props.discussion,
|
||||
lastPost: !this.showStartPost()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ export default class DiscussionsSearchSource {
|
|||
const params = {
|
||||
filter: {q: query},
|
||||
page: {limit: 3},
|
||||
include: 'relevantPosts,relevantPosts.discussion,relevantPosts.user'
|
||||
include: 'mostRelevantPost'
|
||||
};
|
||||
|
||||
return app.store.find('discussions', params).then(results => this.results[query] = results);
|
||||
|
@ -41,14 +41,13 @@ export default class DiscussionsSearchSource {
|
|||
})}
|
||||
</li>,
|
||||
results.map(discussion => {
|
||||
const relevantPosts = discussion.relevantPosts();
|
||||
const post = relevantPosts && relevantPosts[0];
|
||||
const mostRelevantPost = discussion.mostRelevantPost();
|
||||
|
||||
return (
|
||||
<li className="DiscussionSearchResult" data-index={'discussions' + discussion.id()}>
|
||||
<a href={app.route.discussion(discussion, post && post.number())} config={m.route}>
|
||||
<a href={app.route.discussion(discussion, mostRelevantPost && mostRelevantPost.number())} config={m.route}>
|
||||
<div className="DiscussionSearchResult-title">{highlight(discussion.title(), query)}</div>
|
||||
{post ? <div className="DiscussionSearchResult-excerpt">{highlight(post.contentPlain(), query, 100)}</div> : ''}
|
||||
{mostRelevantPost ? <div className="DiscussionSearchResult-excerpt">{highlight(mostRelevantPost.contentPlain(), query, 100)}</div> : ''}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -21,7 +21,7 @@ Object.assign(Discussion.prototype, {
|
|||
commentsCount: Model.attribute('commentsCount'),
|
||||
repliesCount: computed('commentsCount', commentsCount => Math.max(0, commentsCount - 1)),
|
||||
posts: Model.hasMany('posts'),
|
||||
relevantPosts: Model.hasMany('relevantPosts'),
|
||||
mostRelevantPost: Model.hasOne('mostRelevantPost'),
|
||||
|
||||
readTime: Model.attribute('readTime', Model.transformDate),
|
||||
readNumber: Model.attribute('readNumber'),
|
||||
|
|
|
@ -50,27 +50,29 @@
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.read & {
|
||||
.DiscussionList:not(.DiscussionList--searchResults) .read {
|
||||
color: mix(@heading-color, @body-bg, 55%);
|
||||
}
|
||||
.unread & {
|
||||
.DiscussionList:not(.DiscussionList--searchResults) .unread & {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
mark {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
font-weight: bold;
|
||||
color: @text-color;
|
||||
}
|
||||
}
|
||||
.DiscussionListItem-info {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
color: @muted-more-color;
|
||||
|
||||
> li {
|
||||
display: inline;
|
||||
opacity: 0.7;
|
||||
.transition(opacity 0.2s);
|
||||
|
||||
.DiscussionListItem:hover &, .DiscussionListItem.active & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.username {
|
||||
font-weight: bold;
|
||||
|
@ -79,6 +81,24 @@
|
|||
font-size: 11px;
|
||||
margin-right: -1px;
|
||||
}
|
||||
.item-excerpt {
|
||||
margin-top: 4px;
|
||||
margin-right: 170px;
|
||||
white-space: normal;
|
||||
font-size: 12px;
|
||||
line-height: 1.5em;
|
||||
display: block;
|
||||
|
||||
.DiscussionPage-list & {
|
||||
margin-right: 0;
|
||||
}
|
||||
mark {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
font-weight: bold;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
.DiscussionListItem-count {
|
||||
float: right;
|
||||
|
@ -89,41 +109,6 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.DiscussionListItem-relevantPosts {
|
||||
padding-bottom: 15px;
|
||||
|
||||
@media @phone {
|
||||
margin-left: -45px;
|
||||
margin-right: -35px;
|
||||
}
|
||||
|
||||
.PostPreview {
|
||||
background: @control-bg;
|
||||
display: block;
|
||||
padding: 10px 15px;
|
||||
border-bottom: 2px dotted @body-bg;
|
||||
color: @muted-color;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
.DiscussionListItem:hover & {
|
||||
border-color: lighten(@control-bg, 3%);
|
||||
}
|
||||
|
||||
.Avatar, time {
|
||||
display: none;
|
||||
}
|
||||
.PostPreview-content {
|
||||
padding-left: 0;
|
||||
}
|
||||
&:first-child {
|
||||
border-radius: @border-radius @border-radius 0 0;
|
||||
}
|
||||
&:hover {
|
||||
background: darken(@control-bg, 3%);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media @phone {
|
||||
|
@ -212,7 +197,7 @@
|
|||
.DiscussionListItem-controls {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 15px;
|
||||
top: 5px;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
@ -244,10 +229,10 @@
|
|||
margin-right: -65px;
|
||||
}
|
||||
.DiscussionListItem-title {
|
||||
font-size: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.DiscussionListItem-count {
|
||||
margin-top: 21px;
|
||||
margin-top: 12px;
|
||||
margin-right: -70px;
|
||||
width: 55px;
|
||||
color: @muted-color;
|
||||
|
|
|
@ -36,8 +36,9 @@ return [
|
|||
$table->engine = 'MyISAM';
|
||||
});
|
||||
|
||||
$prefix = $schema->getConnection()->getTablePrefix();
|
||||
$schema->getConnection()->statement('ALTER TABLE '.$prefix.'posts ADD FULLTEXT content (content)');
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement('ALTER TABLE '.$prefix.'posts ADD FULLTEXT content (content)');
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement('ALTER TABLE '.$prefix.'posts ENGINE = InnoDB');
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement('ALTER TABLE '.$prefix.'posts ENGINE = MyISAM');
|
||||
}
|
||||
];
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement('ALTER TABLE '.$prefix.'discussions ADD FULLTEXT title (title)');
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement('ALTER TABLE '.$prefix.'discussions DROP INDEX title');
|
||||
}
|
||||
];
|
|
@ -31,9 +31,8 @@ class ListDiscussionsController extends AbstractListController
|
|||
public $include = [
|
||||
'startUser',
|
||||
'lastUser',
|
||||
'relevantPosts',
|
||||
'relevantPosts.discussion',
|
||||
'relevantPosts.user'
|
||||
'mostRelevantPost',
|
||||
'mostRelevantPost.user'
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -84,7 +83,7 @@ class ListDiscussionsController extends AbstractListController
|
|||
$offset = $this->extractOffset($request);
|
||||
$load = array_merge($this->extractInclude($request), ['state']);
|
||||
|
||||
$results = $this->searcher->search($criteria, $limit, $offset, $load);
|
||||
$results = $this->searcher->search($criteria, $limit, $offset);
|
||||
|
||||
$document->addPaginationLinks(
|
||||
$this->url->to('api')->route('discussions.index'),
|
||||
|
@ -94,7 +93,7 @@ class ListDiscussionsController extends AbstractListController
|
|||
$results->areMoreResults() ? null : 0
|
||||
);
|
||||
|
||||
$results = $results->getResults();
|
||||
$results = $results->getResults()->load($load);
|
||||
|
||||
if ($relations = array_intersect($load, ['startPost', 'lastPost'])) {
|
||||
foreach ($results as $discussion) {
|
||||
|
|
|
@ -84,9 +84,9 @@ class BasicDiscussionSerializer extends AbstractSerializer
|
|||
/**
|
||||
* @return \Tobscure\JsonApi\Relationship
|
||||
*/
|
||||
protected function relevantPosts($discussion)
|
||||
protected function mostRelevantPost($discussion)
|
||||
{
|
||||
return $this->hasMany($discussion, BasicPostSerializer::class);
|
||||
return $this->hasOne($discussion, PostSerializer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -382,6 +382,16 @@ class Discussion extends AbstractModel
|
|||
return $this->belongsTo(User::class, 'last_user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the relationship with the discussion's most relevant post.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function mostRelevantPost()
|
||||
{
|
||||
return $this->belongsTo(Post::class, 'most_relevant_post_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the relationship with the discussion's readers.
|
||||
*
|
||||
|
|
|
@ -11,26 +11,20 @@
|
|||
|
||||
namespace Flarum\Discussion\Search;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\DiscussionRepository;
|
||||
use Flarum\Discussion\Event\Searching;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Flarum\Search\ApplySearchParametersTrait;
|
||||
use Flarum\Search\GambitManager;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\Search\SearchResults;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
/**
|
||||
* Takes a DiscussionSearchCriteria object, performs a search using gambits,
|
||||
* and spits out a DiscussionSearchResults object.
|
||||
*/
|
||||
class DiscussionSearcher
|
||||
{
|
||||
use ApplySearchParametersTrait;
|
||||
|
||||
/**
|
||||
* @var \Flarum\Search\GambitManager
|
||||
* @var GambitManager
|
||||
*/
|
||||
protected $gambits;
|
||||
|
||||
|
@ -40,37 +34,34 @@ class DiscussionSearcher
|
|||
protected $discussions;
|
||||
|
||||
/**
|
||||
* @var PostRepository
|
||||
* @var Dispatcher
|
||||
*/
|
||||
protected $posts;
|
||||
protected $events;
|
||||
|
||||
/**
|
||||
* @param \Flarum\Search\GambitManager $gambits
|
||||
* @param GambitManager $gambits
|
||||
* @param DiscussionRepository $discussions
|
||||
* @param PostRepository $posts
|
||||
* @param Dispatcher $events
|
||||
*/
|
||||
public function __construct(
|
||||
GambitManager $gambits,
|
||||
DiscussionRepository $discussions,
|
||||
PostRepository $posts
|
||||
) {
|
||||
public function __construct(GambitManager $gambits, DiscussionRepository $discussions, Dispatcher $events)
|
||||
{
|
||||
$this->gambits = $gambits;
|
||||
$this->discussions = $discussions;
|
||||
$this->posts = $posts;
|
||||
$this->events = $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SearchCriteria $criteria
|
||||
* @param int|null $limit
|
||||
* @param int $offset
|
||||
* @param array $load An array of relationships to load on the results.
|
||||
*
|
||||
* @return SearchResults
|
||||
*/
|
||||
public function search(SearchCriteria $criteria, $limit = null, $offset = 0, array $load = [])
|
||||
public function search(SearchCriteria $criteria, $limit = null, $offset = 0)
|
||||
{
|
||||
$actor = $criteria->actor;
|
||||
|
||||
$query = $this->discussions->query()->whereVisibleTo($actor);
|
||||
$query = $this->discussions->query()->select('discussions.*')->whereVisibleTo($actor);
|
||||
|
||||
// Construct an object which represents this search for discussions.
|
||||
// Apply gambits to it, sort, and paging criteria. Also give extensions
|
||||
|
@ -82,8 +73,7 @@ class DiscussionSearcher
|
|||
$this->applyOffset($search, $offset);
|
||||
$this->applyLimit($search, $limit + 1);
|
||||
|
||||
// TODO: inject dispatcher
|
||||
event(new Searching($search, $criteria));
|
||||
$this->events->dispatch(new Searching($search, $criteria));
|
||||
|
||||
// Execute the search query and retrieve the results. We get one more
|
||||
// results than the user asked for, so that we can say if there are more
|
||||
|
@ -96,42 +86,6 @@ class DiscussionSearcher
|
|||
$discussions->pop();
|
||||
}
|
||||
|
||||
// The relevant posts relationship isn't a typical Eloquent
|
||||
// relationship; rather, we need to extract that information from our
|
||||
// search object. We will delegate that task and prevent Eloquent
|
||||
// from trying to load it.
|
||||
if (in_array('relevantPosts', $load)) {
|
||||
$this->loadRelevantPosts($discussions, $search);
|
||||
|
||||
$load = array_diff($load, ['relevantPosts', 'relevantPosts.discussion', 'relevantPosts.user']);
|
||||
}
|
||||
|
||||
Discussion::setStateUser($actor);
|
||||
$discussions->load($load);
|
||||
|
||||
return new SearchResults($discussions, $areMoreResults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load relevant posts onto each discussion using information from the
|
||||
* search.
|
||||
*
|
||||
* @param Collection $discussions
|
||||
* @param DiscussionSearch $search
|
||||
*/
|
||||
protected function loadRelevantPosts(Collection $discussions, DiscussionSearch $search)
|
||||
{
|
||||
$postIds = [];
|
||||
foreach ($search->getRelevantPostIds() as $relevantPostIds) {
|
||||
$postIds = array_merge($postIds, array_slice($relevantPostIds, 0, 2));
|
||||
}
|
||||
|
||||
$posts = $postIds ? $this->posts->findByIds($postIds, $search->getActor())->load('user')->all() : [];
|
||||
|
||||
foreach ($discussions as $discussion) {
|
||||
$discussion->relevantPosts = array_filter($posts, function ($post) use ($discussion) {
|
||||
return $post->discussion_id == $discussion->id;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Discussion\Search\Fulltext;
|
||||
|
||||
interface DriverInterface
|
||||
{
|
||||
/**
|
||||
* Return an array of arrays of post IDs, grouped by discussion ID, which
|
||||
* match the given string.
|
||||
*
|
||||
* @param string $string
|
||||
* @return array
|
||||
*/
|
||||
public function match($string);
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Discussion\Search\Fulltext;
|
||||
|
||||
use Flarum\Post\Post;
|
||||
|
||||
class MySqlFulltextDriver implements DriverInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function match($string)
|
||||
{
|
||||
$discussionIds = Post::where('type', 'comment')
|
||||
->whereRaw('MATCH (`content`) AGAINST (? IN BOOLEAN MODE)', [$string])
|
||||
->orderByRaw('MATCH (`content`) AGAINST (?) DESC', [$string])
|
||||
->pluck('discussion_id', 'id');
|
||||
|
||||
$relevantPostIds = [];
|
||||
|
||||
foreach ($discussionIds as $postId => $discussionId) {
|
||||
$relevantPostIds[$discussionId][] = $postId;
|
||||
}
|
||||
|
||||
return $relevantPostIds;
|
||||
}
|
||||
}
|
|
@ -12,26 +12,14 @@
|
|||
namespace Flarum\Discussion\Search\Gambit;
|
||||
|
||||
use Flarum\Discussion\Search\DiscussionSearch;
|
||||
use Flarum\Discussion\Search\Fulltext\DriverInterface;
|
||||
use Flarum\Event\ScopeModelVisibility;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Search\AbstractSearch;
|
||||
use Flarum\Search\GambitInterface;
|
||||
use LogicException;
|
||||
|
||||
class FulltextGambit implements GambitInterface
|
||||
{
|
||||
/**
|
||||
* @var \Flarum\Discussion\Search\Fulltext\DriverInterface
|
||||
*/
|
||||
protected $fulltext;
|
||||
|
||||
/**
|
||||
* @param \Flarum\Discussion\Search\Fulltext\DriverInterface $fulltext
|
||||
*/
|
||||
public function __construct(DriverInterface $fulltext)
|
||||
{
|
||||
$this->fulltext = $fulltext;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -41,14 +29,22 @@ class FulltextGambit implements GambitInterface
|
|||
throw new LogicException('This gambit can only be applied on a DiscussionSearch');
|
||||
}
|
||||
|
||||
$relevantPostIds = $this->fulltext->match($bit);
|
||||
$search->getQuery()
|
||||
->selectRaw('SUBSTRING_INDEX(GROUP_CONCAT(posts.id ORDER BY MATCH(posts.content) AGAINST (?) DESC), \',\', 1) as most_relevant_post_id', [$bit])
|
||||
->leftJoin('posts', 'posts.discussion_id', '=', 'discussions.id')
|
||||
->where('posts.type', 'comment')
|
||||
->where(function ($query) use ($search) {
|
||||
event(new ScopeModelVisibility(Post::query()->setQuery($query), $search->getActor(), 'view'));
|
||||
})
|
||||
->where(function ($query) use ($bit) {
|
||||
$query->whereRaw('MATCH(discussions.title) AGAINST (? IN BOOLEAN MODE)', [$bit])
|
||||
->orWhereRaw('MATCH(posts.content) AGAINST (? IN BOOLEAN MODE)', [$bit]);
|
||||
})
|
||||
->groupBy('posts.discussion_id');
|
||||
|
||||
$discussionIds = array_keys($relevantPostIds);
|
||||
|
||||
$search->setRelevantPostIds($relevantPostIds);
|
||||
|
||||
$search->getQuery()->whereIn('id', $discussionIds);
|
||||
|
||||
$search->setDefaultSort(['id' => $discussionIds]);
|
||||
$search->setDefaultSort(function ($query) use ($bit) {
|
||||
$query->orderByRaw('MATCH(discussions.title) AGAINST (?) desc', [$bit]);
|
||||
$query->orderByRaw('MATCH(posts.content) AGAINST (?) desc', [$bit]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,12 +85,12 @@ abstract class AbstractSearch
|
|||
* Set the default sort order for the search. This will only be applied if
|
||||
* a sort order has not been specified in the search criteria.
|
||||
*
|
||||
* @param array $defaultSort An array of sort-order pairs, where the column
|
||||
* @param mixed $defaultSort An array of sort-order pairs, where the column
|
||||
* is the key, and the order is the value. The order may be 'asc',
|
||||
* 'desc', or an array of IDs to order by.
|
||||
* @return mixed
|
||||
*/
|
||||
public function setDefaultSort(array $defaultSort)
|
||||
public function setDefaultSort($defaultSort)
|
||||
{
|
||||
$this->defaultSort = $defaultSort;
|
||||
}
|
||||
|
|
|
@ -23,13 +23,17 @@ trait ApplySearchParametersTrait
|
|||
{
|
||||
$sort = $sort ?: $search->getDefaultSort();
|
||||
|
||||
foreach ($sort as $field => $order) {
|
||||
if (is_array($order)) {
|
||||
foreach ($order as $value) {
|
||||
$search->getQuery()->orderByRaw(snake_case($field).' != ?', [$value]);
|
||||
if (is_callable($sort)) {
|
||||
$sort($search->getQuery());
|
||||
} else {
|
||||
foreach ($sort as $field => $order) {
|
||||
if (is_array($order)) {
|
||||
foreach ($order as $value) {
|
||||
$search->getQuery()->orderByRaw(snake_case($field).' != ?', [$value]);
|
||||
}
|
||||
} else {
|
||||
$search->getQuery()->orderBy(snake_case($field), $order);
|
||||
}
|
||||
} else {
|
||||
$search->getQuery()->orderBy(snake_case($field), $order);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user