- 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.
- Default sort changed from relevance to updated_at desc. This lets
has_child use score_mode:none, which skips child scoring entirely
and lets ES short-circuit early on large corpora.
- When a field sort is requested, score_mode is also none for the same
reason. score_mode:sum is only used when sorting by relevance.
- track_total_hits:false avoids a full-index count on every query,
allowing ES to stop once it has collected enough results.
- Strip all gambit operators (tag:foo, author:bar, is:unread, etc.)
from the ES query string, not just is:private. Leaving them in caused
operator:and to require the gambit tokens to appear literally in post
content, producing zero results when gambits were combined with text.
Without minimum_should_match=1 on the inner post bool query, Elasticsearch
defaults MSM to 0 whenever a filter clause is present. This caused has_child
to score every non-hidden post on every search request, saturating CPU on
shared ES nodes with large corpora (3.7M+ posts).
- Add BoolQuery subclass with create() override and minimumShouldMatch()
(spatie/elasticsearch-query-builder 1.x uses `new self()` in create(),
so subclassing requires overriding it)
- Set minimumShouldMatch(1) on the inner post query after adding the
is_hidden filter, so only genuinely matching posts are scored
- Remove the operator('or') clause from buildShouldClauses() — with
Turkish min_ngram=2, 'or' generates 2-gram tokens that match nearly
every post, causing near-total index scans
- Add ES client timeouts (connect: 2s, query: 10s) to prevent Apache
mod_php worker saturation when ES is slow or unreachable
- Switch to ES parent/child join_field: discussion docs hold all metadata,
post docs hold content only. Routing ensures parent and child land on the
same shard. Removes the old flat-document approach where post data was
duplicated onto every comment document.
- Add HasChildQuery with inner_hits so the best-matching post ID is
surfaced as mostRelevantPost without a second DB query.
- Blue-green index rebuilds: --recreate writes into a timestamped pending
index, --swap atomically promotes it via alias. Interrupted builds are
resumable with --recreate --continue. --swap requires confirmation before
proceeding.
- Index hidden posts; non-moderators are filtered at query time via
is_hidden on the has_child clause. Core\Hidden and Core\Restored trigger
re-indexing so moderators with post.hide can search hidden posts.
- Rename SavingJob to UpdateSearchJob
- Add ViewsSearchJob for lightweight partial view_count updates
- Add probabilistic sync throttle in DiscussionSeeder::viewingOn()
to avoid an ES update on every page view
- Add view_count to mapping, DiscussionSeeder::toDocument(), and
translateSort
- Log a warning and skip instead of 500ing on unknown sort fields
When navigating to /t/{tag}?q=query the tag filter from the request
was silently ignored. Added a TermsQuery on the tags field using the
tag IDs resolved from the provided slugs.