From 2947881e841656c4eeaa9f5ec70916c20adaad00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3js?= Date: Tue, 15 Aug 2023 13:23:43 +0200 Subject: [PATCH 1/4] IBX-3639: As an editor I'd like use facets to filter global search results --- .../Form/ChoiceList/View/FacetGroupView.php | 22 +++++ src/bundle/Form/ChoiceList/View/FacetView.php | 32 +++++++ src/bundle/Form/Data/SearchData.php | 13 +++ src/bundle/Resources/config/services.yaml | 1 + src/bundle/Resources/config/twig.yaml | 9 ++ .../Twig/Extension/SearchFacetsExtension.php | 88 +++++++++++++++++++ src/lib/QueryType/SearchQueryType.php | 32 +++++++ src/lib/View/SearchViewBuilder.php | 13 +-- 8 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 src/bundle/Form/ChoiceList/View/FacetGroupView.php create mode 100644 src/bundle/Form/ChoiceList/View/FacetView.php create mode 100644 src/bundle/Resources/config/twig.yaml create mode 100644 src/bundle/Twig/Extension/SearchFacetsExtension.php diff --git a/src/bundle/Form/ChoiceList/View/FacetGroupView.php b/src/bundle/Form/ChoiceList/View/FacetGroupView.php new file mode 100644 index 0000000..37d2d96 --- /dev/null +++ b/src/bundle/Form/ChoiceList/View/FacetGroupView.php @@ -0,0 +1,22 @@ +label, + $groupView->choices + ); + } +} diff --git a/src/bundle/Form/ChoiceList/View/FacetView.php b/src/bundle/Form/ChoiceList/View/FacetView.php new file mode 100644 index 0000000..0e8a37e --- /dev/null +++ b/src/bundle/Form/ChoiceList/View/FacetView.php @@ -0,0 +1,32 @@ +data, + $choiceView->value, + $choiceView->label, + $choiceView->attr, + $choiceView->labelTranslationParameters, + ); + + $facet->term = $term; + + return $facet; + } +} diff --git a/src/bundle/Form/Data/SearchData.php b/src/bundle/Form/Data/SearchData.php index 133fc81..35e1f5c 100644 --- a/src/bundle/Form/Data/SearchData.php +++ b/src/bundle/Form/Data/SearchData.php @@ -9,6 +9,7 @@ namespace Ibexa\Bundle\Search\Form\Data; use Ibexa\Contracts\Core\Repository\Values\Content\Language; +use Ibexa\Contracts\Core\Repository\Values\Content\Search\AggregationResultCollection; use Ibexa\Contracts\Core\Repository\Values\Content\Section; use Ibexa\Contracts\Core\Repository\Values\User\User; use Ibexa\Contracts\Search\SortingDefinition\SortingDefinitionInterface; @@ -55,6 +56,8 @@ class SearchData /** @var \Ibexa\Contracts\Core\Repository\Values\User\User[] */ private $searchUsersData; + private ?AggregationResultCollection $aggregations; + private ?SortingDefinitionInterface $sortingDefinition; public function __construct( @@ -230,6 +233,16 @@ public function isFiltered(): bool !empty($creator) || null !== $subtree; } + + public function getAggregations(): ?AggregationResultCollection + { + return $this->aggregations; + } + + public function setAggregations(?AggregationResultCollection $aggregations): void + { + $this->aggregations = $aggregations; + } } class_alias(SearchData::class, 'Ibexa\Platform\Bundle\Search\Form\Data\SearchData'); diff --git a/src/bundle/Resources/config/services.yaml b/src/bundle/Resources/config/services.yaml index e32d9e8..86e94a3 100644 --- a/src/bundle/Resources/config/services.yaml +++ b/src/bundle/Resources/config/services.yaml @@ -1,5 +1,6 @@ imports: - { resource: forms.yaml } + - { resource: twig.yaml } - { resource: sorting_definitions.yaml } - { resource: views.yaml } diff --git a/src/bundle/Resources/config/twig.yaml b/src/bundle/Resources/config/twig.yaml new file mode 100644 index 0000000..dc02fca --- /dev/null +++ b/src/bundle/Resources/config/twig.yaml @@ -0,0 +1,9 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + Ibexa\Bundle\Search\Twig\Extension\SearchFacetsExtension: + tags: + - name: twig.extension \ No newline at end of file diff --git a/src/bundle/Twig/Extension/SearchFacetsExtension.php b/src/bundle/Twig/Extension/SearchFacetsExtension.php new file mode 100644 index 0000000..f5577c9 --- /dev/null +++ b/src/bundle/Twig/Extension/SearchFacetsExtension.php @@ -0,0 +1,88 @@ +data == $term->getKey(); + }; + } + + $facets = []; + foreach ($choices as $key => $choice) { + if ($choice instanceof ChoiceGroupView) { + $group = FacetGroupView::createFromChoiceGroupView($choice); + $group->choices = $this->getChoicesAsFacets($group->choices, $terms, $comparator); + if (!empty($group->choices)) { + $facets[$key] = $group; + } + + continue; + } + + $term = $this->findTermEntry($terms, $choice, $comparator); + if ($term !== null) { + $facet = FacetView::createFromChoiceView($choice, $term); + $facet->label = sprintf('%s (%d)', $choice->label, $term->getCount()); + $facets[$key] = $facet; + } + } + + return $facets; + } + + /** + * @param callable(ChoiceView, TermAggregationResultEntry): bool $comparator + */ + private function findTermEntry( + TermAggregationResult $terms, + ChoiceView $choice, + callable $comparator + ): ?TermAggregationResultEntry { + foreach ($terms->getEntries() as $entry) { + if ($comparator($choice, $entry) === true) { + return $entry; + } + } + + return null; + } +} diff --git a/src/lib/QueryType/SearchQueryType.php b/src/lib/QueryType/SearchQueryType.php index 36458af..1375f84 100644 --- a/src/lib/QueryType/SearchQueryType.php +++ b/src/lib/QueryType/SearchQueryType.php @@ -10,6 +10,8 @@ use Ibexa\Bundle\Search\Form\Data\SearchData; use Ibexa\Contracts\Core\Repository\Values\Content\Query; +use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\ContentTypeTermAggregation; +use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\SectionTermAggregation; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause\ContentId; use Ibexa\Contracts\Core\Repository\Values\User\User; @@ -19,6 +21,8 @@ class SearchQueryType extends OptionsResolverBasedQueryType { + private SearchService $searchService; + private SortingDefinitionRegistryInterface $sortingDefinitionRegistry; public function __construct(SortingDefinitionRegistryInterface $sortingDefinitionRegistry) @@ -49,6 +53,11 @@ protected function doGetQuery(array $parameters): Query // Search results order MUST BE deterministic $query->sortClauses[] = new ContentId(Query::SORT_ASC); + if ($this->searchService->supports(SearchService::CAPABILITY_AGGREGATIONS)) { + $query->aggregations[] = $this->buildContentTypeTermAggregation($parameters); + $query->aggregations[] = $this->buildSectionTermAggregation($parameters); + } + return $query; } @@ -56,6 +65,7 @@ protected function configureOptions(OptionsResolver $optionsResolver): void { $optionsResolver->setDefaults([ 'search_data' => new SearchData(), + 'facets_limit' => 128, ]); $optionsResolver->setAllowedTypes('search_data', SearchData::class); @@ -134,6 +144,28 @@ protected function buildCriteria(SearchData $searchData): array return $criteria; } + + /** + * @param array $parameters + */ + private function buildContentTypeTermAggregation(array $parameters): ContentTypeTermAggregation + { + $aggregation = new ContentTypeTermAggregation('content_types'); + $aggregation->setLimit($parameters['facets_limit']); + + return $aggregation; + } + + /** + * @param array $parameters + */ + private function buildSectionTermAggregation(array $parameters): SectionTermAggregation + { + $aggregation = new SectionTermAggregation('sections'); + $aggregation->setLimit($parameters['facets_limit']); + + return $aggregation; + } } class_alias(SearchQueryType::class, 'Ibexa\Platform\Search\QueryType\SearchQueryType'); diff --git a/src/lib/View/SearchViewBuilder.php b/src/lib/View/SearchViewBuilder.php index c3db6b8..702b918 100644 --- a/src/lib/View/SearchViewBuilder.php +++ b/src/lib/View/SearchViewBuilder.php @@ -68,19 +68,20 @@ public function buildView(array $parameters): SearchView : null; $languageFilter = $this->getSearchLanguageFilter($searchLanguageCode); - $pagerfanta = new Pagerfanta( - new ContentSearchHitAdapter( - $this->searchQueryType->getQuery(['search_data' => $data]), - $this->searchService, - $languageFilter - ) + $adapter = new ContentSearchHitAdapter( + $this->searchQueryType->getQuery(['search_data' => $data]), + $this->searchService, + $languageFilter ); + + $pagerfanta = new Pagerfanta($adapter); $pagerfanta->setMaxPerPage($data->getLimit()); $pagerfanta->setCurrentPage(min($data->getPage(), $pagerfanta->getNbPages())); $view->addParameters([ 'results' => $this->pagerSearchContentToDataMapper->map($pagerfanta), 'pager' => $pagerfanta, + 'aggregations' => $adapter->getAggregations(), ]); } From 7c15b59790756dedc557329c3694a30c29b3307b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3js?= Date: Tue, 15 Aug 2023 13:35:08 +0200 Subject: [PATCH 2/4] fixup! IBX-3639: As an editor I'd like use facets to filter global search results --- src/bundle/Form/Data/SearchData.php | 13 ------------- src/bundle/Resources/config/twig.yaml | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/bundle/Form/Data/SearchData.php b/src/bundle/Form/Data/SearchData.php index 35e1f5c..133fc81 100644 --- a/src/bundle/Form/Data/SearchData.php +++ b/src/bundle/Form/Data/SearchData.php @@ -9,7 +9,6 @@ namespace Ibexa\Bundle\Search\Form\Data; use Ibexa\Contracts\Core\Repository\Values\Content\Language; -use Ibexa\Contracts\Core\Repository\Values\Content\Search\AggregationResultCollection; use Ibexa\Contracts\Core\Repository\Values\Content\Section; use Ibexa\Contracts\Core\Repository\Values\User\User; use Ibexa\Contracts\Search\SortingDefinition\SortingDefinitionInterface; @@ -56,8 +55,6 @@ class SearchData /** @var \Ibexa\Contracts\Core\Repository\Values\User\User[] */ private $searchUsersData; - private ?AggregationResultCollection $aggregations; - private ?SortingDefinitionInterface $sortingDefinition; public function __construct( @@ -233,16 +230,6 @@ public function isFiltered(): bool !empty($creator) || null !== $subtree; } - - public function getAggregations(): ?AggregationResultCollection - { - return $this->aggregations; - } - - public function setAggregations(?AggregationResultCollection $aggregations): void - { - $this->aggregations = $aggregations; - } } class_alias(SearchData::class, 'Ibexa\Platform\Bundle\Search\Form\Data\SearchData'); diff --git a/src/bundle/Resources/config/twig.yaml b/src/bundle/Resources/config/twig.yaml index dc02fca..d8735a4 100644 --- a/src/bundle/Resources/config/twig.yaml +++ b/src/bundle/Resources/config/twig.yaml @@ -6,4 +6,4 @@ services: Ibexa\Bundle\Search\Twig\Extension\SearchFacetsExtension: tags: - - name: twig.extension \ No newline at end of file + - name: twig.extension From faef5f237c7f663311df07e2bd44a0b288414343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3js?= Date: Fri, 8 Sep 2023 12:33:02 +0200 Subject: [PATCH 3/4] Fixed issues reported by QA --- src/bundle/Twig/Extension/SearchFacetsExtension.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/bundle/Twig/Extension/SearchFacetsExtension.php b/src/bundle/Twig/Extension/SearchFacetsExtension.php index f5577c9..bced76c 100644 --- a/src/bundle/Twig/Extension/SearchFacetsExtension.php +++ b/src/bundle/Twig/Extension/SearchFacetsExtension.php @@ -37,9 +37,13 @@ public function getFilters(): array */ public function getChoicesAsFacets( array $choices, - TermAggregationResult $terms, + ?TermAggregationResult $terms, ?callable $comparator = null ): array { + if ($terms === null) { + return $choices; + } + if ($comparator === null) { $comparator = static function (ChoiceView $choice, TermAggregationResultEntry $term): bool { return $choice->data == $term->getKey(); From 94edd93b105dc2c856674681812702458ec500ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20S=C5=82omka?= Date: Fri, 15 Sep 2023 15:49:33 +0200 Subject: [PATCH 4/4] Fixed PHPStan error --- src/lib/QueryType/SearchQueryType.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/QueryType/SearchQueryType.php b/src/lib/QueryType/SearchQueryType.php index 1375f84..ab33f83 100644 --- a/src/lib/QueryType/SearchQueryType.php +++ b/src/lib/QueryType/SearchQueryType.php @@ -17,6 +17,7 @@ use Ibexa\Contracts\Core\Repository\Values\User\User; use Ibexa\Contracts\Search\SortingDefinition\SortingDefinitionRegistryInterface; use Ibexa\Core\QueryType\OptionsResolverBasedQueryType; +use Ibexa\Core\Repository\SearchService; use Symfony\Component\OptionsResolver\OptionsResolver; class SearchQueryType extends OptionsResolverBasedQueryType @@ -25,9 +26,12 @@ class SearchQueryType extends OptionsResolverBasedQueryType private SortingDefinitionRegistryInterface $sortingDefinitionRegistry; - public function __construct(SortingDefinitionRegistryInterface $sortingDefinitionRegistry) - { + public function __construct( + SearchService $searchService, + SortingDefinitionRegistryInterface $sortingDefinitionRegistry + ) { $this->sortingDefinitionRegistry = $sortingDefinitionRegistry; + $this->searchService = $searchService; } protected function doGetQuery(array $parameters): Query