providerManager = $providerManager; $this->responseMerger = $responseMerger; $this->cache = $cache; } public function search(SearchRequest $request): SearchResult { if ($this->shouldSkipBrandIncompatibleDomain($request)) { $domain = MediaType::normalize($request->getMediaType()); return new SearchResult( [], 0, null, null, 0, 0.0, [[ 'provider' => 'DomainEngine', 'domain' => $domain, 'search_type' => $request->getSearchType(), 'execution_mode' => $request->getExecutionMode(), 'brand' => $request->getBrand(), 'status' => 'SKIPPED_BRAND_DOMAIN', 'reason' => sprintf('Marchio/Azienda is not a search input for domain %s; provider calls skipped.', $domain), 'result_count' => 0, 'response_time_ms' => 0.0, ]] ); } $engineStart = microtime(true); $phaseStart = $engineStart; $cacheKey = $this->cache->createCacheKey($request); if ($this->cache->has($cacheKey)) { $cached = $this->cache->get($cacheKey); $cacheReadMs = (microtime(true) - $phaseStart) * 1000; $phaseStart = microtime(true); // Cache reuse must remain observable on the current UAS trace. // Without this hand-off, a valid cached identifier card reaches // Preview with no ProductDTO/PDI linkage at all. try { $this->inspector?->observeCachedResult($cached); } catch (\Throwable) {} return $this->withTimings($cached, 'SearchCache', [ 'cache_read_ms' => $cacheReadMs, 'cache_inspector_ms' => (microtime(true) - $phaseStart) * 1000, 'engine_ms' => (microtime(true) - $engineStart) * 1000, ]); } $cacheReadMs = (microtime(true) - $phaseStart) * 1000; $phaseStart = microtime(true); $context = new SearchContext($request); $providerResults = $this->providerManager->search($context); $providerMs = (microtime(true) - $phaseStart) * 1000; $phaseStart = microtime(true); // A physical barcode identifies one retail identity. Results returned by // different providers for the same exact code are merged before the // optional specialist enrichment pass, preventing duplicate cards and // repeated RAWG/IGDB/etc. title lookups for the same product. $result = $this->responseMerger->merge( $providerResults, $request->getLimit(), $request->getOffset(), true, $request->getQuery(), $request->isIdentifierSearch(), $request->isTitleSearch(), $request->getMediaType() ); $mergeMs = (microtime(true) - $phaseStart) * 1000; $phaseStart = microtime(true); if ($this->isCacheable($result)) { $this->cache->store($cacheKey, $result); } return $this->withTimings($result, 'SearchMerger', [ 'cache_read_ms' => $cacheReadMs, 'providers_ms' => $providerMs, 'merge_ms' => $mergeMs, 'cache_write_ms' => (microtime(true) - $phaseStart) * 1000, 'engine_ms' => (microtime(true) - $engineStart) * 1000, ]); } private function withTimings(SearchResult $result, string $target, array $timings): SearchResult { $trace = $result->getProviderTrace(); foreach ($trace as &$attempt) { if (! is_array($attempt) || ($attempt['provider'] ?? '') !== $target) { continue; } $parts = []; foreach ($timings as $key => $value) { $parts[] = $key . '=' . round(max(0.0, $value), 2); } $attempt['reason'] = trim((string) ($attempt['reason'] ?? '') . ' current_engine{' . implode(', ', $parts) . '}'); } unset($attempt); return new SearchResult( $result->getItems(), $result->getTotal(), $result->getProviderUsed(), $result->getProviderFallbackFrom(), $result->getProviderAttempts(), $result->getProviderResponseTimeMs(), $trace, $result->getAppliedOffset(), $result->getRemoteTotal(), $result->getKnownQualifiedTotal(), $result->hasRemoteContinuation(), $result->getProviderContinuations() ); } private function shouldSkipBrandIncompatibleDomain(SearchRequest $request): bool { if (! $request->isTitleSearch() || trim($request->getBrand()) === '') { return false; } return in_array(MediaType::normalize($request->getMediaType()), [ MediaType::BOOK, MediaType::MOVIE, MediaType::MUSIC, MediaType::GAME, ], true); } private function isCacheable(SearchResult $result): bool { // Never cache an empty response: an intermittent 503/timeout from the // only matching provider must not become a persistent "not found". // A positive partial response is valuable even when another provider // timed out, and stabilises repeated operator searches. return $result->getTotal() > 0 && $result->getItems() !== []; } }