From 0f29edd91ccdc7ec951623d2cd2a0c10bda200ea Mon Sep 17 00:00:00 2001 From: thomas-sc Date: Wed, 8 Jan 2025 16:54:17 +0100 Subject: [PATCH] Complete revision of the QueryParamsBuilder and the templates for nested fields and multiple facets --- Classes/Common/QueryParamsBuilder.php | 262 +++++++++++------- .../ElasticSearchServiceInterface.php | 2 +- Classes/Services/ElasticSearchService.php | 7 +- .../GetValueByKeyPathViewHelper.php | 40 +++ Resources/Private/Partials/FilterBlock.html | 36 ++- Resources/Private/Templates/Search/Index.html | 6 - 6 files changed, 228 insertions(+), 125 deletions(-) create mode 100644 Classes/ViewHelpers/GetValueByKeyPathViewHelper.php diff --git a/Classes/Common/QueryParamsBuilder.php b/Classes/Common/QueryParamsBuilder.php index 67a3a98..646f671 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,14 +96,15 @@ public function getQueryParams(): array return $this->query; } - public function getCountQueryParams(): array + // count is not needed anymore because we use this parameter from search request +/* public function getCountQueryParams(): array { $this->query = [ 'body' => [ ] ]; $this->setCommonParams(); return $this->query; - } + }*/ private function getIndexName(): string { if (isset($this->params['index'])) { @@ -114,86 +116,117 @@ private function getIndexName(): string join(','); } - private static function getAggs(array $settings, string $index): array + + + private function getAggs(): array { - return Collection::wrap($settings)-> - recursive()-> - get('entityTypes')-> - filter(function($entityType) use ($index) {return $entityType->get('indexName') === $index;})-> - values()-> - get(0)-> - get('filters')-> - mapWithKeys(function($entityType) { - if ($entityType['type'] == 'terms') { - return [$entityType['field'] => [ - 'terms' => [ - 'field' => $entityType['field'] . '.keyword' - ] - ]]; + $settings = $this->settings; + $index = $this->indexName; + $filterParams = $this->params['filter'] ?? []; + $filterTypes = $this->getFilterTypes(); + + return Collection::wrap($settings) + ->recursive() + ->get('entityTypes') + ->filter(function($entityTypes) use ($index) { + return $entityTypes->get('indexName') === $index; + }) + ->values() + ->get(0) + ->get('filters') + ->mapWithKeys(function($entityType) use ($filterParams, $filterTypes) { + + $entityField = $entityType['field']; + $entityTypeKey = $entityType['key'] ?? null; + + // create filter in aggs for filtering aggs (without filtering the own key for multiple selections) + $filters = array_values( + array_filter( + array_map( + function ($key, $values) use ($entityField, $filterTypes) { + if ($key !== $entityField) { + // 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)]]; + } + return null; // exclude own key for multiple selects + }, + array_keys($filterParams), + $filterParams + ) + ) + ); + + // match_all if filters are empty because elastics error without the filter key + if (empty($filters)) { + $filters = [ + ['match_all' => (object) []] + ]; } - if ($entityType['type'] == 'keyword') { - return [$entityType['field'] => [ - 'terms' => [ - 'field' => $entityType['field'] + + // special aggs for nested fields + if ($entityType['type'] === 'nested') { + + return [ + $entityType['field'] => [ + 'filter' => [ + 'bool' => [ + 'filter' => $filters + ] + ], + 'aggs' => [ + 'filtered_params' => [ + 'nested' => [ + 'path' => $entityField + ], + 'aggs' => [ + $entityField => [ + 'terms' => [ + 'field' => $entityField . '.' . $entityTypeKey . '.keyword', + 'size' => 15, + ] + ] + ] + ] + ] ] - ]]; + ]; + } + + // all other (not nested fields) return [ - $entityType['field'] => [ - 'nested' => [ - 'path' => $entityType['field'] - ], + $entityField => [ 'aggs' => [ - 'names' => [ + $entityField => [ 'terms' => [ - 'script' => [ - 'source' => $entityType['script'], - 'lang' => 'painless' - ], - 'size' => 15, + 'field' => $entityField . '.keyword', + 'min_doc_count' => 0 ] ] + ], + 'filter' => [ + 'bool' => [ + 'filter' => $filters + ] ] ] ]; - })-> - toArray(); - } - - private static function getFilter(array $field): array - { - if ( - isset($field['type']) && - $field['type'] == 'terms' - ) { - return [ - 'terms' => [ - $field['name'] . '.keyword' => $field['value'] - ] - ]; - } - - if ( - isset($field['type']) && - $field['type'] == 'keyword' - ) { - return [ - 'terms' => [ - $field['name'] => $field['value'] - ] - ]; - } - - return [ - 'nested' => [ - 'path' => $field['name'], - 'query' => [ - 'match' => [ - $field['name'] . '.' . $field['path'] => $field['value'] - ] - ] - ] - ]; + })->toArray(); } /** @@ -206,7 +239,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' => [[ @@ -239,43 +272,31 @@ private function setCommonParams(): void ]; } - // set filters + $filterTypes = $this->getFilterTypes(); $query = $this->query; Collection::wrap($this->params['filter'] ?? []) - ->each(function($value, $key) use (&$query) { - // get array keys from $value as new array for multiple facettes + ->each(function($value, $key) use (&$query, $filterTypes) { $value = array_keys($value); - // $value = array('Rochester','Bonn'); - - if ($key !== 'creators') { - $query['body']['query']['bool']['filter'][] = self::getFilter([ - 'name' => $key, - //'type' => $field['type'], - 'type' => 'terms', - 'value' => $value - ]); - - // post_filter for multiple selection facettes and OR function to combine results from multiple facettes - /* $query['body']['post_filter']['bool']['should'][] = self::getFilter([ - 'name' => $key, - //'type' => $field['type'], - 'type' => 'terms', - 'value' => $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 { - // 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? - $query['body']['query']['bool']['must'][] = [ - 'nested' => [ - 'path' => 'creators', - 'query' => [ - 'match' => [ - 'creators.fullName' => $value - ] - ] + + // 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 ] ]; } @@ -284,4 +305,33 @@ private function setCommonParams(): void } + + + /** + * Retrieves filter types based on the current indexName and settings from extension. + * + * @return array + */ + private function getFilterTypes(): array + { + return Collection::wrap($this->settings) + ->recursive() + ->get('entityTypes') + ->filter(function ($entityType) { + return $entityType->get('indexName') === $this->indexName; + }) + ->values() + ->get(0) + ->get('filters') + ->mapWithKeys(function ($filter) { + return [ + $filter['field'] => [ + 'type' => $filter['type'], + 'key' => $filter['key'] ?? '' + ] + ]; + }) + ->all(); + } + } 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 5f8c25a..a7ecc39 100644 --- a/Classes/Services/ElasticSearchService.php +++ b/Classes/Services/ElasticSearchService.php @@ -45,7 +45,7 @@ public function search(array $searchParams, array $settings): Collection { $this->init(); $this->params = QueryParamsBuilder::createQueryParamsBuilder($searchParams, $settings)->getQueryParams(); -print_r($this->params); +//print_r($this->params); // ToDo: handle exceptions! $response = $this->client->search($this->params)->asArray(); $aggs = $response['aggregations']; @@ -65,12 +65,13 @@ public function search(array $searchParams, array $settings): Collection return new Collection($response); } - public function count(array $searchParams, array $settings): int + // Count is not needed, we use this parameter from search request +/* 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/GetValueByKeyPathViewHelper.php b/Classes/ViewHelpers/GetValueByKeyPathViewHelper.php new file mode 100644 index 0000000..807a874 --- /dev/null +++ b/Classes/ViewHelpers/GetValueByKeyPathViewHelper.php @@ -0,0 +1,40 @@ +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/Resources/Private/Partials/FilterBlock.html b/Resources/Private/Partials/FilterBlock.html index 520d38a..f115c58 100644 --- a/Resources/Private/Partials/FilterBlock.html +++ b/Resources/Private/Partials/FilterBlock.html @@ -1,14 +1,17 @@ {namespace lc=Slub\LisztCommon\ViewHelpers} + +ToDo: refactor with view helper from double code for ul content

{key}

{lc:searchParams(action: 'remove', searchParamsArray: searchParams, key: 'page')} {paramsRemovePage.searchParams}
- diff --git a/Resources/Private/Templates/Search/Index.html b/Resources/Private/Templates/Search/Index.html index ba13d77..86791c9 100644 --- a/Resources/Private/Templates/Search/Index.html +++ b/Resources/Private/Templates/Search/Index.html @@ -13,7 +13,6 @@ -
- - - - -