From deff990a866cf67eca44d15b8701314aec1da80e Mon Sep 17 00:00:00 2001 From: Bastien Lutz Date: Fri, 22 Nov 2024 10:48:14 +0100 Subject: [PATCH] [FEATURE] Add timeframe filter to statistics module Relates: #4228 --- .../Backend/Search/InfoModuleController.php | 38 +++-- .../Search/Statistics/StatisticsFilterDto.php | 137 ++++++++++++++++++ .../Statistics/StatisticsRepository.php | 71 ++++++--- Resources/Private/Language/locallang.xlf | 12 ++ .../Backend/Search/InfoModule/Index.html | 74 ++++++++-- .../StatisticsRepositoryTest.php | 35 +++-- 6 files changed, 307 insertions(+), 60 deletions(-) create mode 100644 Classes/Domain/Search/Statistics/StatisticsFilterDto.php diff --git a/Classes/Controller/Backend/Search/InfoModuleController.php b/Classes/Controller/Backend/Search/InfoModuleController.php index 01acdf5c48..93cbf63c74 100644 --- a/Classes/Controller/Backend/Search/InfoModuleController.php +++ b/Classes/Controller/Backend/Search/InfoModuleController.php @@ -17,6 +17,7 @@ use ApacheSolrForTypo3\Solr\Api; use ApacheSolrForTypo3\Solr\Domain\Search\ApacheSolrDocument\Repository as ApacheSolrDocumentRepository; +use ApacheSolrForTypo3\Solr\Domain\Search\Statistics\StatisticsFilterDto; use ApacheSolrForTypo3\Solr\Domain\Search\Statistics\StatisticsRepository; use ApacheSolrForTypo3\Solr\System\Solr\ResponseAdapter; use ApacheSolrForTypo3\Solr\System\Validator\Path; @@ -49,15 +50,21 @@ protected function initializeAction() * * @return ResponseInterface */ - public function indexAction(): ResponseInterface - { + public function indexAction( + ?StatisticsFilterDto $statisticsFilter = null, + int $activeTabId = 0, + string $operation = '' + ): ResponseInterface { + $this->initializeAction(); + + $this->view->assign('activeTabId', $activeTabId); if ($this->selectedSite === null) { $this->view->assign('can_not_proceed', true); return $this->getModuleTemplateResponse(); } $this->collectConnectionInfos(); - $this->collectStatistics(); + $this->collectStatistics($statisticsFilter, $operation); $this->collectIndexFieldsInfo(); $this->collectIndexInspectorInfo(); @@ -129,37 +136,39 @@ protected function collectConnectionInfos(): void /** * Index action, shows an overview of the state of the Solr index */ - protected function collectStatistics(): void + protected function collectStatistics(?StatisticsFilterDto $statisticsFilterDto, string $operation): void { // TODO make time frame user adjustable, for now it's last 30 days + $statisticsFilter = $this->getStatisticsFilter($statisticsFilterDto, $operation); - $siteRootPageId = $this->selectedSite->getRootPageId(); - /* @var StatisticsRepository $statisticsRepository */ + /** @var StatisticsRepository $statisticsRepository */ $statisticsRepository = GeneralUtility::makeInstance(StatisticsRepository::class); // @TODO: Do we want Typoscript constants to restrict the results? $this->view->assign( 'top_search_phrases', - $statisticsRepository->getTopKeyWordsWithHits($siteRootPageId, 30, 5) + $statisticsRepository->getTopKeyWordsWithHits($statisticsFilter) ); $this->view->assign( 'top_search_phrases_without_hits', - $statisticsRepository->getTopKeyWordsWithoutHits($siteRootPageId, 30, 5) + $statisticsRepository->getTopKeyWordsWithoutHits($statisticsFilter) ); $this->view->assign( 'search_phrases_statistics', - $statisticsRepository->getSearchStatistics($siteRootPageId, 30, 100) + $statisticsRepository->getSearchStatistics($statisticsFilter) ); $labels = []; $data = []; - $chartData = $statisticsRepository->getQueriesOverTime($siteRootPageId, 30, 86400); + $chartData = $statisticsRepository->getQueriesOverTime($statisticsFilter, 86400); + foreach ($chartData as $bucket) { // @todo Replace deprecated strftime in php 8.1. Suppress warning for now $labels[] = @strftime('%x', $bucket['timestamp']); $data[] = (int)$bucket['numQueries']; } + $this->view->assign('statisticsFilter', $statisticsFilter); $this->view->assign('queriesChartLabels', json_encode($labels)); $this->view->assign('queriesChartData', json_encode($data)); } @@ -294,4 +303,13 @@ protected function getCoreMetrics(ResponseAdapter $lukeData, array $fields): arr 'numberOfFields' => count($fields), ]; } + + protected function getStatisticsFilter(?StatisticsFilterDto $statisticsFilterDto, string $operation): StatisticsFilterDto + { + if ($statisticsFilterDto === null || $operation === 'reset-filters') { + $statisticsFilterDto = GeneralUtility::makeInstance(StatisticsFilterDto::class); + } + + return $statisticsFilterDto->setSiteRootPageId($this->selectedSite->getRootPageId()); + } } diff --git a/Classes/Domain/Search/Statistics/StatisticsFilterDto.php b/Classes/Domain/Search/Statistics/StatisticsFilterDto.php new file mode 100644 index 0000000000..96c2dfe39c --- /dev/null +++ b/Classes/Domain/Search/Statistics/StatisticsFilterDto.php @@ -0,0 +1,137 @@ +startDate = DateTime::createFromFormat('U', (string)$this->getQueriesStartDate()); + $this->endDate = DateTime::createFromFormat('U', (string)$this->getEndDateTimestamp()); + } + + public function setSiteRootPageId(int $siteRootPageId): StatisticsFilterDto + { + $this->siteRootPageId = $siteRootPageId; + return $this; + } + + public function setStartDate(?DateTime $startDate): StatisticsFilterDto + { + $this->startDate = $startDate; + return $this; + } + + public function setEndDate(?DateTime $endDate): StatisticsFilterDto + { + $this->endDate = $endDate; + return $this; + } + + public function getTopHitsDays(): int + { + return $this->topHitsDays; + } + + public function getNoHitsDays(): int + { + return $this->noHitsDays; + } + + public function getQueriesDays(): int + { + return $this->queriesDays; + } + + public function getSiteRootPageId(): int + { + return $this->siteRootPageId; + } + + public function getTopHitsLimit(): int + { + return $this->topHitsLimit; + } + + public function getNoHitsLimit(): int + { + return $this->noHitsLimit; + } + + public function getQueriesLimit(): int + { + return $this->queriesLimit; + } + + public function getStartDate(): ?DateTime + { + return $this->startDate; + } + + public function getEndDate(): ?DateTime + { + return $this->endDate; + } + + public function getTopHitsStartDate(): int + { + if ($this->startDate !== null) { + return $this->startDate->getTimestamp(); + } + + return $this->getTimeStampSinceDays($this->topHitsDays); + } + + public function getNoHitsStartDate(): int + { + if ($this->startDate !== null) { + return $this->startDate->getTimestamp(); + } + + return $this->getTimeStampSinceDays($this->noHitsDays); + } + + public function getQueriesStartDate(): int + { + if ($this->startDate !== null) { + return $this->startDate->getTimestamp(); + } + + return $this->getTimeStampSinceDays($this->queriesDays); + } + + /** + * End date can not be set by default in typoscript constants and is always now, so one override getter is enough + */ + public function getEndDateTimestamp(): int + { + if ($this->endDate !== null) { + return $this->endDate->getTimestamp(); + } + + return $this->getTimeStampSinceDays(0); + } + + protected function getTimeStampSinceDays(int $days): int + { + $now = time(); + return $now - 86400 * $days; // 86400 seconds/day + } +} diff --git a/Classes/Domain/Search/Statistics/StatisticsRepository.php b/Classes/Domain/Search/Statistics/StatisticsRepository.php index c3bf0ec822..6ec7148484 100644 --- a/Classes/Domain/Search/Statistics/StatisticsRepository.php +++ b/Classes/Domain/Search/Statistics/StatisticsRepository.php @@ -44,12 +44,16 @@ class StatisticsRepository extends AbstractRepository * @throws DBALDriverException * @throws DBALException|\Doctrine\DBAL\DBALException */ - public function getSearchStatistics(int $rootPageId, int $days = 30, int $limit = 10) + public function getSearchStatistics(StatisticsFilterDto $statisticsFilterDto) { - $now = time(); - $timeStart = (int)($now - 86400 * $days); // 86400 seconds/day - return $this->getPreparedQueryBuilderForSearchStatisticsAndTopKeywords($rootPageId, $timeStart, $limit) - ->execute()->fetchAllAssociative(); + return $this->getPreparedQueryBuilderForSearchStatisticsAndTopKeywords( + $statisticsFilterDto->getSiteRootPageId(), + $statisticsFilterDto->getQueriesStartDate(), + $statisticsFilterDto->getEndDateTimestamp(), + $statisticsFilterDto->getQueriesLimit() + ) + ->execute() + ->fetchAllAssociative(); } /** @@ -63,8 +67,12 @@ public function getSearchStatistics(int $rootPageId, int $days = 30, int $limit * @throws DBALDriverException * @throws DBALException|\Doctrine\DBAL\DBALException */ - protected function getPreparedQueryBuilderForSearchStatisticsAndTopKeywords(int $rootPageId, int $timeStart, int $limit): QueryBuilder - { + protected function getPreparedQueryBuilderForSearchStatisticsAndTopKeywords( + int $rootPageId, + int $timeStart, + int $timeEnd, + int $limit + ): QueryBuilder { $countRows = $this->countByRootPageId($rootPageId); $queryBuilder = $this->getQueryBuilder(); return $queryBuilder @@ -75,6 +83,7 @@ protected function getPreparedQueryBuilderForSearchStatisticsAndTopKeywords(int ->from($this->table) ->andWhere( $queryBuilder->expr()->gt('tstamp', $timeStart), + $queryBuilder->expr()->lt('tstamp', $timeEnd), $queryBuilder->expr()->eq('root_pid', $rootPageId) ) ->groupBy('keywords') @@ -94,9 +103,14 @@ protected function getPreparedQueryBuilderForSearchStatisticsAndTopKeywords(int * @throws DBALDriverException * @throws DBALException|\Doctrine\DBAL\DBALException */ - public function getTopKeyWordsWithHits(int $rootPageId, int $days = 30, int $limit = 10): array + public function getTopKeyWordsWithHits(StatisticsFilterDto $filterDto): array { - return $this->getTopKeyWordsWithOrWithoutHits($rootPageId, $days, $limit); + return $this->getTopKeyWordsWithOrWithoutHits( + $filterDto->getSiteRootPageId(), + $filterDto->getTopHitsStartDate(), + $filterDto->getEndDateTimestamp(), + $filterDto->getTopHitsLimit() + ); } /** @@ -109,9 +123,15 @@ public function getTopKeyWordsWithHits(int $rootPageId, int $days = 30, int $lim * @throws DBALDriverException * @throws DBALException|\Doctrine\DBAL\DBALException */ - public function getTopKeyWordsWithoutHits(int $rootPageId, int $days = 30, int $limit = 10): array + public function getTopKeyWordsWithoutHits(StatisticsFilterDto $filterDto): array { - return $this->getTopKeyWordsWithOrWithoutHits($rootPageId, $days, $limit, true); + return $this->getTopKeyWordsWithOrWithoutHits( + $filterDto->getSiteRootPageId(), + $filterDto->getNoHitsStartDate(), + $filterDto->getEndDateTimestamp(), + $filterDto->getNoHitsLimit(), + true + ); } /** @@ -125,12 +145,20 @@ public function getTopKeyWordsWithoutHits(int $rootPageId, int $days = 30, int $ * @throws DBALException|\Doctrine\DBAL\DBALException * @throws DBALDriverException */ - protected function getTopKeyWordsWithOrWithoutHits(int $rootPageId, int $days = 30, int $limit = 10, bool $withoutHits = false): array - { - $now = time(); - $timeStart = $now - 86400 * $days; // 86400 seconds/day + protected function getTopKeyWordsWithOrWithoutHits( + int $rootPageId, + int $timeStart, + int $timeEnd, + int $limit = 10, + bool $withoutHits = false + ): array { + $queryBuilder = $this->getPreparedQueryBuilderForSearchStatisticsAndTopKeywords( + $rootPageId, + $timeStart, + $timeEnd, + $limit + ); - $queryBuilder = $this->getPreparedQueryBuilderForSearchStatisticsAndTopKeywords($rootPageId, $timeStart, $limit); // Check if we want without or with hits if ($withoutHits === true) { $queryBuilder->andWhere($queryBuilder->expr()->eq('num_found', 0)); @@ -151,12 +179,10 @@ protected function getTopKeyWordsWithOrWithoutHits(int $rootPageId, int $days = * @throws DBALException|\Doctrine\DBAL\DBALException * @throws DBALDriverException */ - public function getQueriesOverTime(int $rootPageId, int $days = 30, int $bucketSeconds = 3600): array + public function getQueriesOverTime(StatisticsFilterDto $statisticsFilterDto, int $bucketSeconds = 3600): array { - $now = time(); - $timeStart = $now - 86400 * $days; // 86400 seconds/day - $queryBuilder = $this->getQueryBuilder(); + return $queryBuilder ->addSelectLiteral( 'FLOOR(tstamp/' . $bucketSeconds . ') AS bucket', @@ -165,8 +191,9 @@ public function getQueriesOverTime(int $rootPageId, int $days = 30, int $bucketS ) ->from($this->table) ->andWhere( - $queryBuilder->expr()->gt('tstamp', $timeStart), - $queryBuilder->expr()->eq('root_pid', $rootPageId) + $queryBuilder->expr()->gt('tstamp', $statisticsFilterDto->getQueriesStartDate()), + $queryBuilder->expr()->lt('tstamp', $statisticsFilterDto->getEndDateTimestamp()), + $queryBuilder->expr()->eq('root_pid', $statisticsFilterDto->getSiteRootPageId()) ) ->groupBy('bucket', 'timestamp') ->orderBy('bucket', 'ASC') diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index f32895a622..304d619d48 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -373,6 +373,18 @@ Search Phrase Statistics + + Filter + + + Reset + + + Start + + + End + diff --git a/Resources/Private/Templates/Backend/Search/InfoModule/Index.html b/Resources/Private/Templates/Backend/Search/InfoModule/Index.html index 5f2b731474..a3f879dfaa 100644 --- a/Resources/Private/Templates/Backend/Search/InfoModule/Index.html +++ b/Resources/Private/Templates/Backend/Search/InfoModule/Index.html @@ -7,49 +7,49 @@
-
+
-
+
-
+
-
+
@@ -108,14 +108,68 @@

Site: {site.label}

- - +
+
+ +
+
+ +
+ + + +
+
+ +
+ +
+ + + +
+
+
+ {f:translate(key: 'solr.backend.search_statistics_module.filter')} + {f:translate(key: 'solr.backend.search_statistics_module.filter.reset')} +
+
+ +
+
+
+