From f2aa62aa568b5bb9f2787ed80da4b8d82b11e112 Mon Sep 17 00:00:00 2001 From: Alex Rothberg Date: Sat, 7 Feb 2026 20:30:16 -0500 Subject: [PATCH 1/4] feat(search-index): add command to clean up unused indices - add generic-data-index:cleanup:unused-indices command - default behavior deletes unmanaged stale -odd/-even indices not referenced by aliases - add --dry-run option to preview deletions without changes - implement UnusedIndexCleanupService to detect candidates via prefix + alias diff - add unit tests for selection, dry-run, and execute behavior - document new cleanup command in index management docs --- doc/02_Configuration/03_Index_Management.md | 14 ++ src/Command/CleanupUnusedIndicesCommand.php | 99 ++++++++++++ .../SearchIndex/UnusedIndexCleanupService.php | 120 ++++++++++++++ .../UnusedIndexCleanupServiceTest.php | 146 ++++++++++++++++++ 4 files changed, 379 insertions(+) create mode 100644 src/Command/CleanupUnusedIndicesCommand.php create mode 100644 src/Service/SearchIndex/UnusedIndexCleanupService.php create mode 100644 tests/Unit/Service/SearchIndex/UnusedIndexCleanupServiceTest.php diff --git a/doc/02_Configuration/03_Index_Management.md b/doc/02_Configuration/03_Index_Management.md index 83741e3dc..f8070c6c0 100644 --- a/doc/02_Configuration/03_Index_Management.md +++ b/doc/02_Configuration/03_Index_Management.md @@ -180,3 +180,17 @@ php bin/console generic-data-index:deployment:reindex ``` This command will update the index structure for all data object classes which were created/updated since the last deployment and reindex all data objects for relevant classes. + +### Cleaning Up Unused Indices + +To clean up old indices that are not referenced by any alias, use the following command: + +``` +php bin/console generic-data-index:cleanup:unused-indices +``` + +To preview what would be deleted without performing any changes, run: + +``` +php bin/console generic-data-index:cleanup:unused-indices --dry-run +``` diff --git a/src/Command/CleanupUnusedIndicesCommand.php b/src/Command/CleanupUnusedIndicesCommand.php new file mode 100644 index 000000000..cbb4162e3 --- /dev/null +++ b/src/Command/CleanupUnusedIndicesCommand.php @@ -0,0 +1,99 @@ +setName('generic-data-index:cleanup:unused-indices') + ->addOption( + self::OPTION_DRY_RUN, + null, + InputOption::VALUE_NONE, + 'List unused indices without deleting them.' + ) + ->setDescription( + 'Deletes Generic Data Index indices that are not referenced by any alias.' + ); + } + + /** + * @throws CommandAlreadyRunningException + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + if (!$this->lock()) { + throw new CommandAlreadyRunningException( + 'The command is already running in another process.' + ); + } + + try { + $dryRun = (bool) $input->getOption(self::OPTION_DRY_RUN); + $unusedIndices = $this->unusedIndexCleanupService->cleanupUnusedIndices($dryRun); + + if (empty($unusedIndices)) { + $output->writeln('No unused indices found.'); + + return self::SUCCESS; + } + + $output->writeln('Unused indices:'); + foreach ($unusedIndices as $indexName) { + $output->writeln(sprintf(' - %s', $indexName)); + } + + if ($dryRun) { + $output->writeln( + sprintf('Dry run: %d indices would be deleted.', count($unusedIndices)) + ); + } else { + $output->writeln( + sprintf('Deleted %d unused indices.', count($unusedIndices)) + ); + } + } catch (Exception $e) { + $output->writeln('' . $e->getMessage() . ''); + } finally { + $this->release(); + } + + return self::SUCCESS; + } +} diff --git a/src/Service/SearchIndex/UnusedIndexCleanupService.php b/src/Service/SearchIndex/UnusedIndexCleanupService.php new file mode 100644 index 000000000..5755ce9de --- /dev/null +++ b/src/Service/SearchIndex/UnusedIndexCleanupService.php @@ -0,0 +1,120 @@ +getAllManagedIndices(); + if (empty($allManagedIndices)) { + return []; + } + + $aliasedIndices = $this->getAliasedIndices(); + $unusedIndices = array_values(array_diff($allManagedIndices, $aliasedIndices)); + sort($unusedIndices); + + return $unusedIndices; + } + + /** + * @return string[] + */ + public function cleanupUnusedIndices(bool $dryRun = false): array + { + $unusedIndices = $this->findUnusedIndices(); + + if ($dryRun) { + return $unusedIndices; + } + + foreach ($unusedIndices as $indexName) { + $this->searchIndexService->deleteIndex($indexName); + } + + return $unusedIndices; + } + + /** + * @return string[] + */ + private function getAllManagedIndices(): array + { + $indexPrefix = $this->searchIndexConfigService->getIndexPrefix(); + + try { + $stats = $this->searchIndexService->getStats($indexPrefix . '*'); + } catch (Exception) { + return []; + } + + $indices = $stats['indices'] ?? null; + if (!is_array($indices)) { + return []; + } + + $indexNames = array_keys($indices); + + return array_values(array_filter( + $indexNames, + static fn (string $indexName): bool => str_starts_with($indexName, $indexPrefix) + && preg_match(self::INDEX_SUFFIX_PATTERN, $indexName) === 1 + )); + } + + /** + * @return string[] + */ + private function getAliasedIndices(): array + { + $indexPrefix = $this->searchIndexConfigService->getIndexPrefix(); + $aliases = $this->indexAliasService->getAllAliases(); + + $aliasedIndexMap = []; + foreach ($aliases as $aliasData) { + if (!is_array($aliasData)) { + continue; + } + + $indexName = $aliasData['index'] ?? null; + if (!is_string($indexName) || !str_starts_with($indexName, $indexPrefix)) { + continue; + } + + $aliasedIndexMap[$indexName] = true; + } + + return array_keys($aliasedIndexMap); + } +} diff --git a/tests/Unit/Service/SearchIndex/UnusedIndexCleanupServiceTest.php b/tests/Unit/Service/SearchIndex/UnusedIndexCleanupServiceTest.php new file mode 100644 index 000000000..295df8cc6 --- /dev/null +++ b/tests/Unit/Service/SearchIndex/UnusedIndexCleanupServiceTest.php @@ -0,0 +1,146 @@ +createMock(SearchIndexServiceInterface::class); + $searchIndexService + ->method('getStats') + ->with('pimcore_*') + ->willReturn([ + 'indices' => [ + 'pimcore_asset-odd' => [], + 'pimcore_asset-even' => [], + 'pimcore_document-even' => [], + 'pimcore_custom' => [], + 'other_prefix_asset-odd' => [], + ], + ]) + ; + $searchIndexService->expects($this->never())->method('deleteIndex'); + + $indexAliasService = $this->createMock(IndexAliasServiceInterface::class); + $indexAliasService + ->method('getAllAliases') + ->willReturn([ + ['alias' => 'pimcore_asset', 'index' => 'pimcore_asset-even'], + ['alias' => 'pimcore_document', 'index' => 'pimcore_document-even'], + ]) + ; + + $searchIndexConfigService = $this->createMock(SearchIndexConfigServiceInterface::class); + $searchIndexConfigService + ->method('getIndexPrefix') + ->willReturn('pimcore_') + ; + + $service = new UnusedIndexCleanupService( + $searchIndexService, + $indexAliasService, + $searchIndexConfigService + ); + + $this->assertSame(['pimcore_asset-odd'], $service->findUnusedIndices()); + } + + public function testDryRunDoesNotDeleteIndices(): void + { + $searchIndexService = $this->createMock(SearchIndexServiceInterface::class); + $searchIndexService + ->method('getStats') + ->willReturn([ + 'indices' => [ + 'pimcore_asset-odd' => [], + 'pimcore_asset-even' => [], + ], + ]) + ; + $searchIndexService->expects($this->never())->method('deleteIndex'); + + $indexAliasService = $this->createMock(IndexAliasServiceInterface::class); + $indexAliasService + ->method('getAllAliases') + ->willReturn([ + ['alias' => 'pimcore_asset', 'index' => 'pimcore_asset-even'], + ]) + ; + + $searchIndexConfigService = $this->createMock(SearchIndexConfigServiceInterface::class); + $searchIndexConfigService + ->method('getIndexPrefix') + ->willReturn('pimcore_') + ; + + $service = new UnusedIndexCleanupService( + $searchIndexService, + $indexAliasService, + $searchIndexConfigService + ); + + $this->assertSame(['pimcore_asset-odd'], $service->cleanupUnusedIndices(true)); + } + + public function testExecuteDeletesUnusedIndices(): void + { + $searchIndexService = $this->createMock(SearchIndexServiceInterface::class); + $searchIndexService + ->method('getStats') + ->willReturn([ + 'indices' => [ + 'pimcore_asset-odd' => [], + 'pimcore_asset-even' => [], + ], + ]) + ; + $searchIndexService + ->expects($this->once()) + ->method('deleteIndex') + ->with('pimcore_asset-odd') + ; + + $indexAliasService = $this->createMock(IndexAliasServiceInterface::class); + $indexAliasService + ->method('getAllAliases') + ->willReturn([ + ['alias' => 'pimcore_asset', 'index' => 'pimcore_asset-even'], + ]) + ; + + $searchIndexConfigService = $this->createMock(SearchIndexConfigServiceInterface::class); + $searchIndexConfigService + ->method('getIndexPrefix') + ->willReturn('pimcore_') + ; + + $service = new UnusedIndexCleanupService( + $searchIndexService, + $indexAliasService, + $searchIndexConfigService + ); + + $this->assertSame(['pimcore_asset-odd'], $service->cleanupUnusedIndices()); + } +} From d61ce86d64dd2e3b7316c558a28cc740b88fb661 Mon Sep 17 00:00:00 2001 From: JiaJia Ji Date: Mon, 22 Jun 2026 11:06:31 +0200 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Command/CleanupUnusedIndicesCommand.php | 5 ++++- src/Service/SearchIndex/UnusedIndexCleanupService.php | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Command/CleanupUnusedIndicesCommand.php b/src/Command/CleanupUnusedIndicesCommand.php index cbb4162e3..9720d960d 100644 --- a/src/Command/CleanupUnusedIndicesCommand.php +++ b/src/Command/CleanupUnusedIndicesCommand.php @@ -49,7 +49,10 @@ protected function configure(): void 'List unused indices without deleting them.' ) ->setDescription( - 'Deletes Generic Data Index indices that are not referenced by any alias.' + 'Deletes managed Generic Data Index indices with the configured index prefix and a -odd/-even suffix that are not referenced by any alias.' + ) + ->setHelp( + 'This command only targets managed Generic Data Index indices that use the configured index prefix and end with -odd or -even. It does not consider other indices.' ); } diff --git a/src/Service/SearchIndex/UnusedIndexCleanupService.php b/src/Service/SearchIndex/UnusedIndexCleanupService.php index 5755ce9de..535af1a20 100644 --- a/src/Service/SearchIndex/UnusedIndexCleanupService.php +++ b/src/Service/SearchIndex/UnusedIndexCleanupService.php @@ -72,6 +72,9 @@ public function cleanupUnusedIndices(bool $dryRun = false): array private function getAllManagedIndices(): array { $indexPrefix = $this->searchIndexConfigService->getIndexPrefix(); + if ($indexPrefix === '') { + return []; + } try { $stats = $this->searchIndexService->getStats($indexPrefix . '*'); From bda5444c338e45af859ee3c29b6d400fa2ba2a9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:04:36 +0000 Subject: [PATCH 3/4] Initial plan From 79f45f8ee5b2226b61c272fa2c1c5152b992edd0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:07:19 +0000 Subject: [PATCH 4/4] Merge PR #391 and fix exception swallowing in getAllManagedIndices() --- doc/02_Configuration/03_Index_Management.md | 4 ---- src/Service/SearchIndex/UnusedIndexCleanupService.php | 7 +------ 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/doc/02_Configuration/03_Index_Management.md b/doc/02_Configuration/03_Index_Management.md index b51414854..36642be47 100644 --- a/doc/02_Configuration/03_Index_Management.md +++ b/doc/02_Configuration/03_Index_Management.md @@ -188,11 +188,8 @@ After updating class definitions during deployment, run: bin/console generic-data-index:deployment:reindex ``` -<<<<<<< HEAD This updates the index structure for all class definitions modified since the last deployment and reindexes data objects for affected classes. -======= -This command will update the index structure for all data object classes which were created/updated since the last deployment and reindex all data objects for relevant classes. ### Cleaning Up Unused Indices @@ -207,4 +204,3 @@ To preview what would be deleted without performing any changes, run: ``` php bin/console generic-data-index:cleanup:unused-indices --dry-run ``` ->>>>>>> pr/391 diff --git a/src/Service/SearchIndex/UnusedIndexCleanupService.php b/src/Service/SearchIndex/UnusedIndexCleanupService.php index 535af1a20..dfb2c30f1 100644 --- a/src/Service/SearchIndex/UnusedIndexCleanupService.php +++ b/src/Service/SearchIndex/UnusedIndexCleanupService.php @@ -13,7 +13,6 @@ namespace Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex; -use Exception; use Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\IndexAliasServiceInterface; use Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\SearchIndexServiceInterface; @@ -76,11 +75,7 @@ private function getAllManagedIndices(): array return []; } - try { - $stats = $this->searchIndexService->getStats($indexPrefix . '*'); - } catch (Exception) { - return []; - } + $stats = $this->searchIndexService->getStats($indexPrefix . '*'); $indices = $stats['indices'] ?? null; if (!is_array($indices)) {