Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Entity/Card.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@
'mainCost', 'recallCost', 'oceanPower', 'mountainPower', 'forestPower',
])]
#[ApiFilter(\App\Filter\CardNameFilter::class, properties: ['name'])]
#[ApiFilter(OrderFilter::class, properties: ['cardNumber', 'collectorNumberFormatedId', 'setDate', 'set.date', 'random'])]
#[ApiFilter(OrderFilter::class, properties: ['cardNumber', 'collectorNumberFormatedId', 'setDate', 'random'])]
#[ApiFilter(\App\Filter\SetDateOrderFilter::class)]
#[ApiFilter(\App\Filter\CardGroupOrderFilter::class, properties: ['mainCost', 'recallCost', 'oceanPower', 'mountainPower', 'forestPower'])]
#[ApiFilter(\App\Filter\RandomCardFilter::class)]
#[ApiFilter(\App\Filter\EffectTriggerTypeFilter::class, properties: ['effectTriggerType' => 'cardGroup'])]
Expand Down
49 changes: 9 additions & 40 deletions src/State/SearchAwareCollectionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,14 @@

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Repository\FilteredCardCountRepository;
use App\Service\FilterCacheKeyService;
use App\Service\MeilisearchService;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
* Wraps CardCollectionProvider and resolves the total item count via Meilisearch
* when a name (full-text) search is active.
* Wraps CardCollectionProvider and delegates filtering + pagination to Meilisearch
* whenever all active filters are mapped in FILTER_MAP.
*
* Meilisearch is called with limit=0, which returns estimatedTotalHits instantly
* without fetching any documents. The total is injected into the context under
* '_meili_total' so CachedCountCollectionProvider can use it directly and skip
* the expensive Doctrine COUNT query.
* Meilisearch returns IDs (paginated) and estimatedTotalHits (~0.2% inaccuracy).
* Doctrine then fetches only those specific IDs — no JOIN, no COUNT, no OFFSET.
*
* Meilisearch filter conditions are built from the active API Platform filters
* so the total is accurate even when combined with structured filters (faction,
Expand Down Expand Up @@ -59,10 +53,6 @@ final class SearchAwareCollectionProvider implements ProviderInterface
public function __construct(
private readonly CardCollectionProvider $inner,
private readonly MeilisearchService $meilisearch,
private readonly FilterCacheKeyService $cacheKeyService,
#[Autowire(service: 'cache.card_counts')]
private readonly CacheItemPoolInterface $cachePool,
private readonly FilteredCardCountRepository $filteredCountRepo,
) {}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
Expand Down Expand Up @@ -97,32 +87,11 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
$context['_meili_ids'] = $ids;
$context['filters']['page'] = 1; // Meilisearch already paginated; Doctrine fetches at OFFSET 0

// Accurate total: Redis-cached DB count for structural filters.
// Name-search queries use Meilisearch estimate (too varied to cache per-combination).
if ($nameQuery === null) {
$cacheKey = $this->cacheKeyService->make($operation->getClass() ?? '', $filters);
$cacheItem = $this->cachePool->getItem($cacheKey);
if ($cacheItem->isHit()) {
$context['_meili_total'] = (float) $cacheItem->get();
} else {
try {
// Slow on first request for this filter combination, cached indefinitely after.
$count = $this->filteredCountRepo->count($filters);
$cacheItem->set((float) $count)->expiresAfter(null);
$this->cachePool->save($cacheItem);
$context['_meili_total'] = (float) $count;
} catch (\Throwable) {
$total = $this->fetchTotal($query, $meiliFilter, $attrs);
if ($total !== null) {
$context['_meili_total'] = $total;
}
}
}
} else {
$total = $this->fetchTotal($query, $meiliFilter, $attrs);
if ($total !== null) {
$context['_meili_total'] = $total;
}
// Total: Meilisearch estimatedTotalHits for all cases.
// ~0.2% inaccuracy is acceptable; avoids expensive DB COUNT on every first request.
$total = $this->fetchTotal($query, $meiliFilter, $attrs);
if ($total !== null) {
$context['_meili_total'] = $total;
}

// Strip filters that Meilisearch already handled so Doctrine doesn't
Expand Down
Loading