Skip to content
Closed
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
14 changes: 14 additions & 0 deletions doc/02_Configuration/03_Index_Management.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,17 @@ bin/console generic-data-index:deployment:reindex

This updates the index structure for all class definitions modified since the last
deployment and reindexes data objects for affected 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
```
102 changes: 102 additions & 0 deletions src/Command/CleanupUnusedIndicesCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);

/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

namespace Pimcore\Bundle\GenericDataIndexBundle\Command;

use Exception;
use Pimcore\Bundle\GenericDataIndexBundle\Exception\CommandAlreadyRunningException;
use Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\UnusedIndexCleanupService;
use Pimcore\Console\AbstractCommand;
use Symfony\Component\Console\Command\LockableTrait;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
* @internal
*/
final class CleanupUnusedIndicesCommand extends AbstractCommand
{
use LockableTrait;

private const OPTION_DRY_RUN = 'dry-run';

public function __construct(
private readonly UnusedIndexCleanupService $unusedIndexCleanupService,
?string $name = null
) {
parent::__construct($name);
}

protected function configure(): void
{
$this
->setName('generic-data-index:cleanup:unused-indices')
->addOption(
self::OPTION_DRY_RUN,
null,
InputOption::VALUE_NONE,
'List unused indices without deleting them.'
)
->setDescription(
'Deletes managed Generic Data Index indices with the configured index prefix and a -odd/-even suffix that are not referenced by any alias.'

Check warning on line 52 in src/Command/CleanupUnusedIndicesCommand.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Split this 155 characters long line (which is greater than 120 authorized).

See more on https://sonarcloud.io/project/issues?id=pimcore_generic-data-index-bundle&issues=AZ70KWmYKdUojUIhsRsz&open=AZ70KWmYKdUojUIhsRsz&pullRequest=442
)
->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.'

Check warning on line 55 in src/Command/CleanupUnusedIndicesCommand.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Split this 179 characters long line (which is greater than 120 authorized).

See more on https://sonarcloud.io/project/issues?id=pimcore_generic-data-index-bundle&issues=AZ70KWmYKdUojUIhsRs0&open=AZ70KWmYKdUojUIhsRs0&pullRequest=442
);
}

/**
* @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('<info>No unused indices found.</info>');

return self::SUCCESS;
}

$output->writeln('<info>Unused indices:</info>');
foreach ($unusedIndices as $indexName) {
$output->writeln(sprintf(' - %s', $indexName));
}

if ($dryRun) {
$output->writeln(
sprintf('<comment>Dry run: %d indices would be deleted.</comment>', count($unusedIndices))
);
} else {
$output->writeln(
sprintf('<info>Deleted %d unused indices.</info>', count($unusedIndices))
);
}
} catch (Exception $e) {
$output->writeln('<error>' . $e->getMessage() . '</error>');
} finally {
$this->release();
}

return self::SUCCESS;
}
}
118 changes: 118 additions & 0 deletions src/Service/SearchIndex/UnusedIndexCleanupService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);

/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

namespace Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex;

use Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\IndexAliasServiceInterface;
use Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\SearchIndexServiceInterface;

/**
* @internal
*/
final readonly class UnusedIndexCleanupService
{
private const INDEX_SUFFIX_PATTERN = '/-(odd|even)$/';

public function __construct(
private SearchIndexServiceInterface $searchIndexService,
private IndexAliasServiceInterface $indexAliasService,
private SearchIndexConfigServiceInterface $searchIndexConfigService,
) {
}

/**
* @return string[]
*/
public function findUnusedIndices(): array
{
$allManagedIndices = $this->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();
if ($indexPrefix === '') {
return [];
}

$stats = $this->searchIndexService->getStats($indexPrefix . '*');

$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);
}
}
146 changes: 146 additions & 0 deletions tests/Unit/Service/SearchIndex/UnusedIndexCleanupServiceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);

/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

namespace Pimcore\Bundle\GenericDataIndexBundle\Tests\Unit\Service\SearchIndex;

use Codeception\Test\Unit;
use Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\IndexAliasServiceInterface;
use Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\SearchIndexServiceInterface;
use Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\SearchIndexConfigServiceInterface;
use Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\UnusedIndexCleanupService;

/**
* @internal
*/
final class UnusedIndexCleanupServiceTest extends Unit
{
public function testFindUnusedIndicesReturnsOnlyUnaliasedManagedIndices(): void
{
$searchIndexService = $this->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());
}
}
Loading