diff --git a/app/Services/Search/Drivers/ElasticSearchDriver.php b/app/Services/Search/Drivers/ElasticSearchDriver.php index ec7e93d28..5b2aa9dfe 100644 --- a/app/Services/Search/Drivers/ElasticSearchDriver.php +++ b/app/Services/Search/Drivers/ElasticSearchDriver.php @@ -2911,6 +2911,7 @@ class ElasticSearchDriver implements SearchDriverInterface 'query' => $query, 'sort' => [ [$sortField => ['order' => $order]], + ['id' => ['order' => $order]], ], 'from' => max(0, $offset), 'size' => max(1, min($limit, self::MAX_RESULTS)), diff --git a/app/Services/Search/Drivers/ManticoreSearchDriver.php b/app/Services/Search/Drivers/ManticoreSearchDriver.php index 239903e19..b0ca6140d 100644 --- a/app/Services/Search/Drivers/ManticoreSearchDriver.php +++ b/app/Services/Search/Drivers/ManticoreSearchDriver.php @@ -2430,7 +2430,12 @@ class ManticoreSearchDriver implements SearchDriverInterface if (! in_array($sortField, $allowedSort, true)) { $sortField = 'postdate_ts'; } - $query->sort($sortField, $sortDir === 'asc' ? 'asc' : 'desc'); + $order = $sortDir === 'asc' ? 'asc' : 'desc'; + $query->sort($sortField, $order); + if ($sortField !== 'id') { + // Stabilize ordering across pages when primary sort values are equal. + $query->sort('id', $order); + } $maxNeed = max(1000, $offset + $limit + 100); $query->maxMatches(min($maxNeed, (int) ($this->config['max_matches'] ?? 10000))); diff --git a/tests/Unit/ManticoreSearchQueryTest.php b/tests/Unit/ManticoreSearchQueryTest.php index 782347d0b..0a7d6e122 100644 --- a/tests/Unit/ManticoreSearchQueryTest.php +++ b/tests/Unit/ManticoreSearchQueryTest.php @@ -207,4 +207,22 @@ class ManticoreSearchQueryTest extends TestCase $this->assertSame(500, $method->invoke($driver, 750)); $this->assertSame(1, $method->invoke($driver, 0)); } + + #[Test] + public function search_releases_filtered_uses_stable_id_tiebreak_sort_for_manticore(): void + { + $driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ManticoreSearchDriver.php'); + + $this->assertIsString($driverSource); + $this->assertStringContainsString("\$query->sort('id', \$order);", $driverSource); + } + + #[Test] + public function search_releases_filtered_uses_stable_id_tiebreak_sort_for_elasticsearch(): void + { + $driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ElasticSearchDriver.php'); + + $this->assertIsString($driverSource); + $this->assertStringContainsString("['id' => ['order' => \$order]],", $driverSource); + } }