diff --git a/src/Repository/CardDocumentRepository.php b/src/Repository/CardDocumentRepository.php index f99ae7c..130da09 100644 --- a/src/Repository/CardDocumentRepository.php +++ b/src/Repository/CardDocumentRepository.php @@ -145,6 +145,34 @@ public function findDocument(int $cardId): ?array return $row !== false ? $this->hydrate($row) : null; } + /** + * Build documents for every card belonging to the given CardGroups, in one query. + * Used to bulk-reindex a small, targeted set of groups (e.g. after a gameplay-format + * import) without the N+1 HTTP-per-card cost of calling findDocument() in a loop. + * + * @param int[] $cardGroupIds + * @return array> + */ + public function findDocumentsByCardGroupIds(array $cardGroupIds): array + { + if (empty($cardGroupIds)) { + return []; + } + + $result = $this->connection->executeQuery( + $this->buildSql(whereCardGroupIds: true), + ['cardGroupIds' => $cardGroupIds], + ['cardGroupIds' => \Doctrine\DBAL\ArrayParameterType::INTEGER], + ); + + $docs = []; + while (($row = $result->fetchAssociative()) !== false) { + $docs[] = $this->hydrate($row); + } + + return $docs; + } + /** * Count total cards (for progress bars, etc.). */ @@ -171,9 +199,13 @@ private function sanitize(?string $text): ?string return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $clean); } - private function buildSql(bool $whereCardId = false): string + private function buildSql(bool $whereCardId = false, bool $whereCardGroupIds = false): string { - $where = $whereCardId ? 'WHERE c.id = :id' : ''; + $where = match (true) { + $whereCardId => 'WHERE c.id = :id', + $whereCardGroupIds => 'WHERE cg.id IN (:cardGroupIds)', + default => '', + }; return <<cardGroupRepository->findBy(['id' => $cardGroupIds]) as $cardGroup) { - $current = $cardGroup->getGameplayFormat(); - if (!in_array($formatName, $current, true)) { - $cardGroup->setGameplayFormat([...$current, $formatName]); - $updated++; + $updatedGroupIds = []; + + MeilisearchSyncListener::$disabled = true; + CardSearchListener::$disabled = true; + try { + foreach ($this->cardGroupRepository->findBy(['id' => $cardGroupIds]) as $cardGroup) { + $current = $cardGroup->getGameplayFormat(); + if (!in_array($formatName, $current, true)) { + $cardGroup->setGameplayFormat([...$current, $formatName]); + $updatedGroupIds[] = $cardGroup->getId(); + } } + $this->em->flush(); + } finally { + MeilisearchSyncListener::$disabled = false; + CardSearchListener::$disabled = false; + } + + if (empty($updatedGroupIds)) { + return 0; + } + + foreach ($updatedGroupIds as $cardGroupId) { + $this->cardSearchUpdater->upsertByCardGroupId($cardGroupId); + } + + try { + $this->meilisearch->indexDocuments( + $this->cardDocumentRepository->findDocumentsByCardGroupIds($updatedGroupIds) + ); + } catch (\Throwable $e) { + // The gameplayFormat write already succeeded in Postgres — a Meilisearch + // hiccup here shouldn't fail the import, just leave the index briefly stale. + $this->logger->error('Gameplay-format import: bulk Meilisearch reindex failed', [ + 'error' => $e->getMessage(), + ]); } - $this->em->flush(); - return $updated; + return count($updatedGroupIds); } /** @return string[] */ diff --git a/src/Service/MeilisearchService.php b/src/Service/MeilisearchService.php index cdca10e..740e755 100644 --- a/src/Service/MeilisearchService.php +++ b/src/Service/MeilisearchService.php @@ -113,6 +113,20 @@ public function indexCard(Card $card): void } } + /** + * Push multiple prebuilt documents in a single request (bulk update, e.g. after + * a batch write that disabled the per-entity MeilisearchSyncListener). + * + * @param array> $docs + */ + public function indexDocuments(array $docs): void + { + if (empty($docs)) { + return; + } + $this->getIndex()->addDocuments($docs); + } + /** * Delete a single card from the index. */