79 lines
3.2 KiB
PHP
79 lines
3.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Database\QueryException;
|
|
|
|
/** Combines independent, already-authorised sources without loading complete catalogues. */
|
|
final class FederatedSearch
|
|
{
|
|
/** @param array<string, callable(): array{count:int, fetch:callable(int,int): iterable}> $factories */
|
|
public function page(array $factories, int $requestedPage, int $size = 5, ?callable $score = null): array
|
|
{
|
|
$sources = [];
|
|
$unavailable = [];
|
|
foreach ($factories as $label => $factory) {
|
|
try {
|
|
$sources[$label] = $factory();
|
|
} catch (QueryException $exception) {
|
|
report($exception);
|
|
$unavailable[] = $label;
|
|
}
|
|
}
|
|
|
|
// If a source fails after its count, remove it and recompute the page offsets.
|
|
do {
|
|
$failed = false;
|
|
$total = array_sum(array_column($sources, 'count'));
|
|
$last = max(1, (int) ceil($total / $size));
|
|
$page = min(max(1, $requestedPage), $last);
|
|
$first = ($page - 1) * $size;
|
|
$items = [];
|
|
$positions = array_fill_keys(array_keys($sources), 0);
|
|
$buffers = array_fill_keys(array_keys($sources), []);
|
|
for ($position = 0; $position < min($total, $first + $size); $position++) {
|
|
$best = null;
|
|
$bestScore = PHP_INT_MIN;
|
|
foreach ($sources as $label => $source) {
|
|
if ($buffers[$label] === [] && $positions[$label] < $source['count']) {
|
|
try {
|
|
$batch = iterator_to_array((function () use ($source, $positions, $label, $size) {
|
|
yield from ($source['fetch'])($positions[$label], $size);
|
|
})(), false);
|
|
$positions[$label] += count($batch);
|
|
$buffers[$label] = $batch;
|
|
if ($batch === []) {
|
|
$positions[$label] = $source['count'];
|
|
}
|
|
} catch (QueryException $exception) {
|
|
report($exception);
|
|
$unavailable[] = $label;
|
|
unset($sources[$label]);
|
|
$failed = true;
|
|
break;
|
|
}
|
|
}
|
|
if ($buffers[$label] === []) {
|
|
continue;
|
|
}
|
|
$candidateScore = $score ? $score($buffers[$label][0]) : 0;
|
|
if ($best === null || $candidateScore > $bestScore) {
|
|
$best = $label;
|
|
$bestScore = $candidateScore;
|
|
}
|
|
}
|
|
if ($failed || $best === null) {
|
|
break;
|
|
}
|
|
$item = array_shift($buffers[$best]);
|
|
if ($position >= $first) {
|
|
$items[] = [...$item, 'category' => $best];
|
|
}
|
|
}
|
|
} while ($failed);
|
|
|
|
return ['data' => $items, 'total' => $total, 'current_page' => $page, 'last_page' => $last,
|
|
'per_page' => $size, 'unavailable' => $unavailable];
|
|
}
|
|
}
|