diff --git a/Classes/Common/Paginator.php b/Classes/Common/Paginator.php
index 09a41fb..93113a8 100644
--- a/Classes/Common/Paginator.php
+++ b/Classes/Common/Paginator.php
@@ -82,18 +82,28 @@ public function setPage(int $page): Paginator
return $this;
}
- public function getPagination(): array
+ public function getTotalPages(): int
{
if (
$this->totalItems < 0 ||
- $this->currentPage < 0 ||
$this->itemsPerPage < 0
) {
- throw new \Exception('Please specify total items, items per page and current page before retrieving the pagination.');
+ throw new \Exception('Please specify total items and items per page before retrieving the pagination.');
+ }
+
+ return (int) ceil($this->totalItems / $this->itemsPerPage);
+ }
+
+ public function getPagination(): array
+ {
+ if (
+ $this->currentPage < 0
+ ) {
+ throw new \Exception('Please specify current page before retrieving the pagination.');
}
$pagination = new Collection();
- $totalPages = (int) ceil($this->totalItems / $this->itemsPerPage);
+ $totalPages = $this->getTotalPages();
$currentPage = $this->currentPage;
$pagesBefore = $this->paginationRange->
diff --git a/Classes/Common/QueryParamsBuilder.php b/Classes/Common/QueryParamsBuilder.php
index 939389b..1484e5e 100644
--- a/Classes/Common/QueryParamsBuilder.php
+++ b/Classes/Common/QueryParamsBuilder.php
@@ -22,9 +22,11 @@ class QueryParamsBuilder
protected string $indexName = '';
protected bool $searchAll = false;
- public static function createQueryParamsBuilder(array $searchParams, array $settings): QueryParamsBuilder
+ // ToDo: @Matthias: check searchAll condition
+
+ public static function createQueryParamsBuilder(array $searchParams, array $settings): self
{
- $queryParamsBuilder = new QueryParamsBuilder();
+ $queryParamsBuilder = new self();
return $queryParamsBuilder->
setSettings($settings)->
@@ -65,7 +67,6 @@ public function setSearchParams($searchParams): QueryParamsBuilder
return $this;
}
- //Todo: get Config for bibIndex, aggs etc. from extension config?
public function getQueryParams(): array
{
$commonConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('liszt_common');
@@ -84,7 +85,7 @@ public function getQueryParams(): array
];
if ($this->searchAll == false) {
- $this->query['body']['aggs'] = self::getAggs($this->settings, $this->indexName);
+ $this->query['body']['aggs'] = $this->getAggs();
}
$this->setCommonParams();
@@ -95,78 +96,6 @@ public function getQueryParams(): array
return $this->query;
}
- public function getCountQueryParams(): array
- {
- $this->query = [ 'body' => [ ] ];
-
- $this->setCommonParams();
-
-/*
- $params = [
- //'index' => $bibIndex,
- 'body' => [ ]
- ];
- if (!isset($searchParams['searchText']) || $searchParams['searchText'] == '') {
- $params['body']['query'] = [
- 'bool' => [
- 'must' => [[
- 'match_all' => new \stdClass()
- ]]
- ]
- ];
- } else {
- // search in field "fulltext" exakt phrase match boost over all words must contain
- $params['body']['query'] = [
- 'bool' => [
- 'should' => [
- [
- 'match_phrase' => [
- 'tx_lisztcommon_searchable' => [
- 'query' => $searchParams['searchText'],
- 'boost' => 2.0 // boosting for exakt phrases
- ]
- ]
- ],
- [
- 'query_string' => [
- 'query' => $searchParams['searchText'],
- 'fields' => ['fulltext'],
- 'default_operator' => 'AND'
- ]
- ]
- ]
- ]
- ];
- }
-
- // Todo: automate the creation of parameters
- if (isset($searchParams['f_itemType']) && $searchParams['f_itemType'] !== "") {
- $params['body']['query']['bool']['filter'][] = ['term' => ['itemType.keyword' => $searchParams['f_itemType']]];
- }
- if (isset($searchParams['f_place']) && $searchParams['f_place'] !== "") {
- $params['body']['query']['bool']['filter'][] = ['term' => ['place.keyword' => $searchParams['f_place']]];
- }
- if (isset($searchParams['f_date']) && $searchParams['f_date'] !== "") {
- $params['body']['query']['bool']['filter'][] = ['term' => ['date.keyword' => $searchParams['f_date']]];
- }
- // filter creators name, Todo: its not a filter query because they need 100% match (with spaces from f_creators_name)
- // better would be to build the field 'fullName' at build time with PHP?
- if (isset($searchParams['f_creators_name']) && $searchParams['f_creators_name'] !== "") {
- $params['body']['query']['bool']['must'][] = [
- 'nested' => [
- 'path' => 'creators',
- 'query' => [
- 'match' => [
- 'creators.fullName' => $searchParams['f_creators_name']
- ]
- ]
- ]
- ];
- }
-
-*/
- return $this->query;
- }
private function getIndexName(): string
{
if (isset($this->params['index'])) {
@@ -178,88 +107,136 @@ private function getIndexName(): string
join(',');
}
- private static function getAggs(array $settings, string $index): array
+ private function getAggs(): array
{
- return Collection::wrap($settings)->
+ $settings = $this->settings;
+ $index = $this->indexName;
+ $filterParams = $this->params['filter'] ?? [];
+ $filterTypes = $this->getFilterTypes();
+ return Collection::wrap($settings)->
recursive()->
get('entityTypes')->
- filter(function($entityType) use ($index) {return $entityType->get('indexName') === $index;})->
+ filter(function($entityTypes) use ($index) {
+ return $entityTypes->get('indexName') === $index;
+ })->
values()->
get(0)->
get('filters')->
- mapWithKeys(function($entityType) {
- if ($entityType['type'] == 'terms') {
- return [$entityType['field'] => [
- 'terms' => [
- 'field' => $entityType['field'] . '.keyword'
- ]
- ]];
- }
- if ($entityType['type'] == 'keyword') {
- return [$entityType['field'] => [
- 'terms' => [
- 'field' => $entityType['field']
- ]
- ]];
- }
- return [
- $entityType['field'] => [
- 'nested' => [
- 'path' => $entityType['field']
- ],
- 'aggs' => [
- 'names' => [
- 'terms' => [
- 'script' => [
- 'source' => $entityType['script'],
- 'lang' => 'painless'
- ],
- 'size' => 15,
- ]
- ]
- ]
- ]
- ];
+ mapWithKeys(function ($entityType) use ($filterParams, $filterTypes) {
+ return self::retrieveFilterParamsForEntityType($entityType, $filterParams, $filterTypes);
})->
toArray();
}
- private static function getFilter(array $field): array
+ private static function retrieveFilterParamsForEntityType(
+ Collection $entityType,
+ array $filterParams,
+ array $filterTypes
+ ): array
{
- if (
- isset($field['type']) &&
- $field['type'] == 'terms'
- ) {
- return [
- 'term' => [
- $field['name'] . '.keyword' => $field['value']
- ]
+ $entityField = $entityType['field'];
+ $entityTypeKey = $entityType['key'] ?? null;
+ $entityTypeMultiselect = $entityType['multiselect'] ?? null;
+ $entityTypeSize = $entityType['maxSize'] ?? 10;
+
+ // create filter in aggs for filtering aggs (without filtering the current key for multiple selections if multiselect is set)
+ $filters = Collection::wrap($filterParams)->
+ map(function ($value, $key) use ($entityField, $filterTypes) {
+ return self::retrieveFilterParamForEntityField($key, $value, $entityField, $filterTypes);
+ })->
+ filter()->
+ values()->
+ toArray();
+
+ // return match_all if filters are empty because elasticsearch throws an error without the filter key
+ if (empty($filters)) {
+ $filters = [
+ ['match_all' => (object) []]
];
}
- if (
- isset($field['type']) &&
- $field['type'] == 'keyword'
- ) {
+ // special aggs for nested fields
+ if ($entityType['type'] === 'nested') {
+
return [
- 'term' => [
- $field['name'] => $field['value']
+ $entityType['field'] => [
+ 'filter' => [
+ 'bool' => [
+ 'filter' => $filters
+ ]
+ ],
+ 'aggs' => [
+ 'filtered_params' => [
+ 'nested' => [
+ 'path' => $entityField
+ ],
+ 'aggs' => [
+ $entityField => [
+ 'terms' => [
+ 'field' => $entityField . '.' . $entityTypeKey . '.keyword',
+ 'size' => $entityTypeSize,
+ ]
+ ]
+ ]
+ ]
+ ]
]
];
+
}
+ // all other (not nested fields)
return [
- 'nested' => [
- 'path' => $field['name'],
- 'query' => [
- 'match' => [
- $field['name'] . '.' . $field['path'] => $field['value']
+ $entityField => [
+ 'aggs' => [
+ $entityField => [
+ 'terms' => [
+ 'field' => $entityField . '.keyword',
+ // show docs with count 0 only for multiple select fields
+ 'min_doc_count' => $entityTypeMultiselect ? 0 : 1,
+ 'size' => $entityTypeSize,
+ ]
+ ]
+ ],
+ 'filter' => [
+ 'bool' => [
+ 'filter' => $filters
]
]
]
];
}
+ private static function retrieveFilterParamForEntityField (
+ string $key,
+ array $values,
+ string $entityField,
+ array $filterTypes
+ ): ?array
+ {
+ // exclude current key for multiple selects
+ if ($key === $entityField && isset($filterTypes[$key]['multiselect'])) {
+ return null;
+ }
+ // handle nested fields
+ if (($filterTypes[$key]['type'] == 'nested') && (isset($filterTypes[$key]['key']))) {
+ return [
+ 'nested' => [
+ 'path' => $key,
+ 'query' => [
+ 'bool' => [
+ 'filter' => [
+ 'terms' => [ $key.'.'.$filterTypes[$key]['key'].'.keyword' => array_keys($values)]
+ ]
+ ]
+ ]
+ ]
+ ];
+ }
+ // handle all other fields (not nested fields)
+ return ['terms' => [$key . '.keyword' => array_keys($values)]];
+ }
+
/**
* sets parameters needed for both search and count queries
*/
@@ -270,7 +247,7 @@ private function setCommonParams(): void
$this->query['index'] = $index;
// set body
- if (!isset($this->params['searchText']) || $this->params['searchText'] == '') {
+ if (empty($this->params['searchText'])) {
$this->query['body']['query'] = [
'bool' => [
'must' => [[
@@ -303,38 +280,69 @@ private function setCommonParams(): void
];
}
- // set filters
- if ($this->searchAll == false) {
- $filterTypes = Collection::wrap($this->settings)->
- recursive()->
- get('entityTypes')->
- filter(function($entityType) use ($index) {return $entityType->get('indexName') === $index;})->
- values()->
- get(0)->
- get('filters')->
- mapWithKeys(function($filter) { return [
- $filter['field'] => [
- 'type' => $filter['type'],
- 'path' => isset($filter['path']) ? $filter['path'] : ''
- ]];
- })->
- all();
-
- $query = $this->query;
- Collection::wrap($this->params)->
- filter(function($_, $key) { return Str::of($key)->startsWith('f_'); })->
- each(function($value, $key) use (&$query, $filterTypes) {
- $field = Str::of($key)->replace('f_', '')->__toString();
- $query['body']['query']['bool']['filter'][] = self::getFilter([
- 'name' => $field,
- 'type' => $filterTypes[$field]['type'],
- 'value' => $value,
- 'path' => $filterTypes[$field]['path']
- ]);
- });
- $this->query = $query;
+ $filterTypes = $this->getFilterTypes();
+ $query = $this->query;
+ Collection::wrap($this->params['filter'] ?? [])
+ ->each(function($value, $key) use (&$query, $filterTypes) {
+ $value = array_keys($value);
+ if (($filterTypes[$key]['type'] == 'nested') && (isset($filterTypes[$key]['key']))) {
+
+ // nested filter query (for multiple Names)
+ $query['body']['post_filter']['bool']['filter'][] = [
+ 'nested' => [
+ 'path' => $key,
+ 'query' => [
+ 'terms' => [
+ $key.'.'.$filterTypes[$key]['key'].'.keyword' => $value
+ ]
+ ]
+ ]
+ ];
+
+ } else {
+
+ // post_filter, runs the search without considering the aggregations, for muliple select aggregations we run the filters again on each agg in getAggs()
+ $query['body']['post_filter']['bool']['filter'][] = [
+ 'terms' => [
+ $key . '.keyword' => $value
+ ]
+ ];
+ }
+ });
+ $this->query = $query;
+
+ }
+
+ /**
+ * Retrieves filter types based on the current indexName and settings from extension.
+ *
+ * @return array
+ */
+ private function getFilterTypes(): array
+ {
+ $filters = Collection::wrap($this->settings)
+ ->recursive()
+ ->get('entityTypes')
+ ->filter(function ($entityType) {
+ return $entityType->get('indexName') === $this->indexName;
+ })
+ ->values();
+ if ($filters->count() === 0) {
+ return [];
}
+ return $filters->get(0)
+ ->get('filters')
+ ->mapWithKeys(function ($filter) {
+ return [
+ $filter['field'] => [
+ 'type' => $filter['type'],
+ 'key' => $filter['key'] ?? '',
+ 'multiselect' => $filter['multiselect'] ?? null
+ ]
+ ];
+ })
+ ->all();
}
}
diff --git a/Classes/Controller/SearchController.php b/Classes/Controller/SearchController.php
index 823bc96..2896502 100644
--- a/Classes/Controller/SearchController.php
+++ b/Classes/Controller/SearchController.php
@@ -42,17 +42,24 @@ public function indexAction(array $searchParams = []): ResponseInterface
$currentPage = 1;
}
- $totalItems = $this->elasticSearchService->count($searchParams, $this->settings);
- $pagination = Paginator::createPagination($currentPage, $totalItems, $this->extConf);
+ // $totalItems = $this->elasticSearchService->count($searchParams, $this->settings);
+ //$totalItems = 100;
$elasticResponse = $this->elasticSearchService->search($searchParams, $this->settings);
+ $paginator = (new Paginator())->
+ setPage($currentPage)->
+ setTotalItems($elasticResponse['hits']['total']['value'])->
+ setExtensionConfiguration($this->extConf);
+ $pagination = $paginator->getPagination();
+ $showPagination = $paginator->getTotalPages() > 1 ? true : false;
$this->view->assign('locale', $locale);
$this->view->assign('totalItems', $elasticResponse['hits']['total']['value']);
$this->view->assign('searchParams', $searchParams);
$this->view->assign('searchResults', $elasticResponse);
$this->view->assign('pagination', $pagination);
- $this->view->assign('totalItems', $totalItems);
+ $this->view->assign('showPagination', $showPagination);
+ // $this->view->assign('totalItems', $totalItems);
$this->view->assign('currentString', Paginator::CURRENT_PAGE);
$this->view->assign('dots', Paginator::DOTS);
diff --git a/Classes/Interfaces/ElasticSearchServiceInterface.php b/Classes/Interfaces/ElasticSearchServiceInterface.php
index 280e92d..90a7102 100644
--- a/Classes/Interfaces/ElasticSearchServiceInterface.php
+++ b/Classes/Interfaces/ElasticSearchServiceInterface.php
@@ -11,6 +11,6 @@ public function getElasticInfo(): array;
public function search(array $searchParams, array $settings): Collection;
- public function count(array $searchParams, array $settings): int;
+ // public function count(array $searchParams, array $settings): int;
}
diff --git a/Classes/Services/ElasticSearchService.php b/Classes/Services/ElasticSearchService.php
index 493ae55..82a4390 100644
--- a/Classes/Services/ElasticSearchService.php
+++ b/Classes/Services/ElasticSearchService.php
@@ -45,7 +45,6 @@ public function search(array $searchParams, array $settings): Collection
{
$this->init();
$this->params = QueryParamsBuilder::createQueryParamsBuilder($searchParams, $settings)->getQueryParams();
-
// ToDo: handle exceptions!
$response = $this->client->search($this->params)->asArray();
$aggs = $response['aggregations'];
@@ -64,13 +63,4 @@ public function search(array $searchParams, array $settings): Collection
return new Collection($response);
}
-
- public function count(array $searchParams, array $settings): int
- {
- $this->init();
- $this->params = QueryParamsBuilder::createQueryParamsBuilder($searchParams, $settings)->getCountQueryParams();
- $response = $this->client->count($this->params);
- return $response['count'];
- }
-
}
diff --git a/Classes/ViewHelpers/GetFilterEntitiesViewHelper.php b/Classes/ViewHelpers/GetFilterEntitiesViewHelper.php
new file mode 100644
index 0000000..f736216
--- /dev/null
+++ b/Classes/ViewHelpers/GetFilterEntitiesViewHelper.php
@@ -0,0 +1,37 @@
+registerArgument('entityTypes', 'array', 'The settings.entityTypes array', true);
+ $this->registerArgument('filterKey', 'string', 'The field key to filter for', true);
+ }
+
+ public static function renderStatic(
+ array $arguments,
+ \Closure $renderChildrenClosure,
+ RenderingContextInterface $renderingContext): array
+ {
+ $entityTypes = $arguments['entityTypes'];
+ $filterKey = $arguments['filterKey'];
+
+ foreach ($entityTypes as $entityType) {
+ if (!empty($entityType['filters']) && is_array($entityType['filters'])) {
+ foreach ($entityType['filters'] as $filter) {
+ if (!empty($filter['field']) && $filter['field'] === $filterKey) {
+ return array_merge($entityType, $filter);
+ }
+ }
+ }
+ }
+
+ return []; //if nothing is found
+ }
+}
diff --git a/Classes/ViewHelpers/GetValueByKeyPathViewHelper.php b/Classes/ViewHelpers/GetValueByKeyPathViewHelper.php
new file mode 100644
index 0000000..b524fdd
--- /dev/null
+++ b/Classes/ViewHelpers/GetValueByKeyPathViewHelper.php
@@ -0,0 +1,44 @@
+registerArgument('data', 'array', 'The array to search in', true);
+ $this->registerArgument('keys', 'array', 'An array of keys defining the path to the desired value', true);
+ }
+
+ /**
+ * Resolve a value in a deeply nested array by following an array of keys
+ *
+ * @return mixed|null
+ */
+ public function render()
+ {
+ $data = $this->arguments['data'];
+ $keys = $this->arguments['keys'];
+
+ foreach ($keys as $key) {
+ if (is_array($data) && array_key_exists($key, $data)) {
+ $data = $data[$key];
+ } else {
+ // Key does not exist, return null
+ return null;
+ }
+ }
+
+ return $data;
+ }
+}
diff --git a/Classes/ViewHelpers/ProcessFacetsViewHelper.php b/Classes/ViewHelpers/ProcessFacetsViewHelper.php
new file mode 100644
index 0000000..e29677e
--- /dev/null
+++ b/Classes/ViewHelpers/ProcessFacetsViewHelper.php
@@ -0,0 +1,65 @@
+registerArgument('filterGroup', 'array', 'Array with facets from solr', true);
+ $this->registerArgument('key', 'string', 'key of the current facet', true);
+ $this->registerArgument('searchParams', 'array', 'Array with search params from url', true);
+ $this->registerArgument('filterEntities', 'array', 'Array with settings for filters from setup.typoscript', false, []);
+ }
+
+ public static function renderStatic(
+ array $arguments,
+ \Closure $renderChildrenClosure,
+ RenderingContextInterface $renderingContext): array
+ {
+
+ $filterGroup = $arguments['filterGroup'];
+ $key = $arguments['key'];
+ $searchParams = $arguments['searchParams'];
+ $returnBucket = [];
+ $filterEntities = $arguments['filterEntities'];
+
+ // check if buckets exists on "normal" not nested filters
+ if (isset($filterGroup[$key]['buckets'])) {
+ $returnBucket = $filterGroup[$key]['buckets'];
+ }
+
+ // check buckets on nested filters
+ if (isset($filterGroup['filtered_params'][$key]['buckets'])) {
+ $returnBucket = $filterGroup['filtered_params'][$key]['buckets'];
+ }
+
+ // set size from entity settings in setup.typoscript or use 10 as default
+ $size = $filterEntities['size'] ?? $filterEntities['defaultFilterSize'] ?? 10;
+
+
+ // find active filter items and set selected and set hidden for items over size
+ foreach ($returnBucket as $index => &$item) {
+ $filterKey = $item['key'] ?? null;
+ $item['selected'] = isset($searchParams['filter'][$key][$filterKey])
+ && $searchParams['filter'][$key][$filterKey] == 1;
+
+ // if item is over $size set 'hidden' => true
+ if ($index >= $size && !$item['selected']) {
+ $item['hidden'] = true;
+ }
+ }
+
+ // Remove items that are not selected and have a doc_count of 0
+ $returnBucket = array_filter($returnBucket, function ($item) {
+ return $item['doc_count'] > 0 || ($item['selected'] ?? false);
+ });
+
+ return $returnBucket;
+
+ }
+}
diff --git a/Classes/ViewHelpers/SearchParamsViewHelper.php b/Classes/ViewHelpers/SearchParamsViewHelper.php
index 10b7fc2..0e16aa9 100644
--- a/Classes/ViewHelpers/SearchParamsViewHelper.php
+++ b/Classes/ViewHelpers/SearchParamsViewHelper.php
@@ -25,10 +25,25 @@ public static function renderStatic(
$value = $arguments['value'] ?? null;
$searchParamsArray = $arguments['searchParamsArray'];
- if ($action === 'add') {
- $searchParamsArray[$key] = $value;
- } elseif ($action === 'remove') {
- unset($searchParamsArray[$key]);
+ switch ($action) {
+ case 'add':
+ $searchParamsArray[$key] = $value;
+ break;
+
+ case 'remove':
+ unset($searchParamsArray[$key]);
+ break;
+ // later here are special values possible like "disableFilter" with value=0
+ case 'addFilter':
+ $searchParamsArray['filter'][$key][$value] = 1;
+ break;
+
+ case 'removeFilter':
+ unset($searchParamsArray['filter'][$key][$value]);
+ break;
+
+ default:
+ break;
}
// Convert the array to a string formatted as {key: 'value', key2: 'value2'}
diff --git a/Resources/Private/Language/de.locallang.xlf b/Resources/Private/Language/de.locallang.xlf
index 28c7178..a993c6d 100644
--- a/Resources/Private/Language/de.locallang.xlf
+++ b/Resources/Private/Language/de.locallang.xlf
@@ -30,6 +30,15 @@