Skip to content
Open
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 @@ -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
```
99 changes: 99 additions & 0 deletions src/Command/CleanupUnusedIndicesCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?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 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('<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;
}
}
120 changes: 120 additions & 0 deletions src/Service/SearchIndex/UnusedIndexCleanupService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?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 Exception;
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();

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);
}
}
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());
}
}