fix: default search sort to latest and keep dropdown label in sync

- Search with no explicit sort now defaults to latest (updated_at desc)
  on both backend and frontend, matching the forum UX expectation.
- "All discussions" link from the search box routes to /?q=...&sort=latest
  so the sort dropdown shows "Latest" immediately after submission.
- extendDiscussionState injects sort=-lastPostedAt into API params when
  no sort is present, keeping direct-URL navigation (?q=anan) consistent
  with the backend default.
- Backend guard changed from phpSortField===null to empty($sorts), which
  is the correct sentinel since $sorts is the authoritative ES sort list.
This commit is contained in:
Bart van Bragt 2026-04-15 17:21:10 +02:00
parent 6bcd2becd5
commit c5643bed31
5 changed files with 19 additions and 11 deletions

2
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -17,6 +17,12 @@ export default function extendDiscussionState() {
...params.page, ...params.page,
}; };
// Default to latest sort when searching without an explicit sort parameter.
// Matches the backend default and keeps the dropdown label in sync.
if (!params.sort) {
params.sort = '-lastPostedAt';
}
if (Array.isArray(params.include)) { if (Array.isArray(params.include)) {
params.include = params.include.join(','); params.include = params.include.join(',');
} }

View File

@ -66,7 +66,7 @@ export default class DiscussionsSearchSource implements SearchSource {
return [ return [
<li className="Dropdown-header">{app.translator.trans('core.forum.search.discussions_heading')}</li>, <li className="Dropdown-header">{app.translator.trans('core.forum.search.discussions_heading')}</li>,
<li> <li>
<LinkButton icon="fas fa-search" href={app.route('index', { q: query })}> <LinkButton icon="fas fa-search" href={app.route('index', { q: query, sort: 'latest' })}>
{app.translator.trans('core.forum.search.all_discussions_button', { query })} {app.translator.trans('core.forum.search.all_discussions_button', { query })}
</LinkButton> </LinkButton>
</li>, </li>,

View File

@ -103,9 +103,10 @@ class SearchController extends ListDiscussionsController
} }
} }
// Default to latest when no sort is specified — faster than relevance // Default to latest when no explicit sort is requested. This lets has_child use
// because has_child can use score_mode:none and ES can short-circuit early. // score_mode:none, which skips child scoring entirely and lets ES short-circuit
if ($phpSortField === null) { // early on large corpora. Relevance is still available via sort=relevant if added.
if (empty($sorts)) {
$needsScoring = false; $needsScoring = false;
$phpSortField = 'updated_at'; $phpSortField = 'updated_at';
$phpSortDir = 'desc'; $phpSortDir = 'desc';
@ -220,11 +221,12 @@ class SearchController extends ListDiscussionsController
* Build the text-matching portion of the query. * Build the text-matching portion of the query.
* *
* Discussion titles are matched directly (filtered to join_field=discussion). * Discussion titles are matched directly (filtered to join_field=discussion).
* Post bodies are matched via has_child. When $needsScoring is true (relevance * Post bodies are matched via has_child. When $needsScoring is true (no explicit
* sort), score_mode=sum accumulates child scores onto the parent. When false * sort, i.e. relevance ordering), score_mode=sum accumulates child scores onto the
* (any field sort, including the default updated_at), score_mode=none skips * parent so that discussions with many strongly-matching posts rank higher. When
* scoring entirely ES only checks whether a matching child exists, which is * false (any explicit field sort), score_mode=none skips scoring entirely ES only
* significantly cheaper on large corpora. * checks whether a matching child exists, which is significantly cheaper on large
* corpora.
* inner_hits returns the best-matching post for use as mostRelevantPost. * inner_hits returns the best-matching post for use as mostRelevantPost.
* Hidden posts are only included in matching for users with post.hide permission. * Hidden posts are only included in matching for users with post.hide permission.
*/ */