diff --git a/config/vufind/facets.ini b/config/vufind/facets.ini index d99dd52eeb2..7ced6130fb4 100644 --- a/config/vufind/facets.ini +++ b/config/vufind/facets.ini @@ -148,9 +148,6 @@ facet_limit = 30 ; Limit facets based on a prefix on a per-field basis: ;facet_prefix_by_field[building] = 22 -; Filter facet values to those matching a regular expression on a per-field basis: -;facet_matches_by_field[era_facet] = ".+0" - ; By default, the side facets will only show 6 facets and then the "show more" ; button. This can be configured with the showMore settings. A positive value is ; required for "normal" facets, but for hierarchical facets you can use 0 to disable diff --git a/config/vufind/searches.ini b/config/vufind/searches.ini index 963739c633b..5775a27bd6d 100644 --- a/config/vufind/searches.ini +++ b/config/vufind/searches.ini @@ -138,9 +138,10 @@ always_display_reset_filters = false ; caveats described at ; https://opensourceconnections.com/blog/2018/02/20/edismax-and-multiterm-synonyms-oddities/ ; -; Parameters are entered as query parameters and they can be repeated like in Solr's -; query string syntax: -; default_parameters[search] = "fq=field%3A%2Fmatch%2F&fq=another%3A2" +; Parameters are entered as a JSON object according to Solr's JSON Request API. +; See https://solr.apache.org/guide/solr/latest/query-guide/json-request-api.html +; Parameters with multiple values can be entered using an array like: +; default_parameters[search] = '{ "filter": ["format:DVD", "author:Smith"] }' ; ; Parameters can be defined for the following search contexts: ; * All contexts not otherwise defined here @@ -150,8 +151,8 @@ always_display_reset_filters = false ; terms Return terms ; alphabeticBrowse Return information from the browse index ; workExpressions Return work expressions (record versions) -;default_parameters[*] = "echoParams=none&debugQuery=false" -;default_parameters[search] = "sow=false" +;default_parameters[*] = '{ "params": { "echoParams": "none", "debugQuery": true } }' +;default_parameters[search] = '{ "params": { "sow": false } }' ; If you set this setting, VuFind will use a secondary Solr field to try to find ; previous record IDs when primary ID lookups fail. If you leave the setting diff --git a/module/VuFind/src/VuFind/ChannelProvider/SimilarItems.php b/module/VuFind/src/VuFind/ChannelProvider/SimilarItems.php index 2d7e7b6eb5f..8e35de24c78 100644 --- a/module/VuFind/src/VuFind/ChannelProvider/SimilarItems.php +++ b/module/VuFind/src/VuFind/ChannelProvider/SimilarItems.php @@ -212,7 +212,7 @@ protected function buildChannelFromRecord( } $retVal['limit'] = $this->batchSize; - $params = new \VuFindSearch\ParamBag(['rows' => $this->batchSize]); + $params = new \VuFindSearch\ParamBag(['limit' => $this->batchSize]); $command = new SimilarCommand( $driver->getSourceIdentifier(), $driver->getUniqueID(), diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Solr.php b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Solr.php index ac952e829d4..f20dcb35b43 100644 --- a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Solr.php +++ b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Solr.php @@ -31,7 +31,7 @@ use VuFind\Hierarchy\TreeDataFormatter\PluginManager as FormatterManager; use VuFindSearch\Backend\Solr\Command\RawJsonSearchCommand; -use VuFindSearch\ParamBag; +use VuFindSearch\NestingParamBag; use VuFindSearch\Query\Query; use VuFindSearch\Service; @@ -152,12 +152,14 @@ public function getXML($id, $options = []) protected function getDefaultSearchParams(): array { return [ - 'fq' => $this->filters, - 'hl' => ['false'], - 'fl' => ['title,id,hierarchy_parent_id,hierarchy_top_id,' + 'filter' => $this->filters, + 'fields' => ['title,id,hierarchy_parent_id,hierarchy_top_id,' . 'is_hierarchy_id,hierarchy_sequence,title_in_hierarchy'], - 'wt' => ['json'], - 'json.nl' => ['arrarr'], + 'params' => [ + 'hl' => ['false'], + 'wt' => ['json'], + 'json.nl' => ['arrarr'], + ], ]; } @@ -172,7 +174,7 @@ protected function getDefaultSearchParams(): array */ protected function searchSolrLegacy(Query $query, $rows): array { - $params = new ParamBag($this->getDefaultSearchParams()); + $params = NestingParamBag::fromArray($this->getDefaultSearchParams()); $command = new RawJsonSearchCommand( $this->backendId, $query, @@ -198,16 +200,15 @@ protected function searchSolrCursor(Query $query, $rows): array $cursorMark = '*'; $records = []; while ($cursorMark !== $prevCursorMark) { - $params = new ParamBag( - $this->getDefaultSearchParams() + [ - // Sort is required - 'sort' => ['id asc'], - // Override any default timeAllowed since it cannot be used with - // cursorMark - 'timeAllowed' => -1, - 'cursorMark' => $cursorMark, - ] - ); + $params = NestingParamBag::fromArray($this->getDefaultSearchParams()); + // Sort is required + $params->add('sort', 'id asc'); + $params->addMultiNested('params', [ + // Override any default timeAllowed since it cannot be used with + // cursorMark + 'timeAllowed' => -1, + 'cursorMark' => $cursorMark, + ]); $command = new RawJsonSearchCommand( $this->backendId, $query, diff --git a/module/VuFind/src/VuFind/RecordDriver/SolrDefault.php b/module/VuFind/src/VuFind/RecordDriver/SolrDefault.php index 645f680397a..2bc61268e98 100644 --- a/module/VuFind/src/VuFind/RecordDriver/SolrDefault.php +++ b/module/VuFind/src/VuFind/RecordDriver/SolrDefault.php @@ -33,6 +33,8 @@ namespace VuFind\RecordDriver; use VuFindSearch\Command\SearchCommand; +use VuFindSearch\NestingParamBag; +use VuFindSearch\ParamBag; use function count; use function in_array; @@ -317,7 +319,7 @@ public function getChildRecordCount() 'hierarchy_parent_id:"' . $safeId . '"' ); // Disable highlighting for efficiency; not needed here: - $params = new \VuFindSearch\ParamBag(['hl' => ['false']]); + $params = new NestingParamBag(['params' => new ParamBag(['hl' => ['false']])]); $command = new SearchCommand($this->sourceIdentifier, $query, 0, 0, $params); return $this->searchService ->invoke($command)->getResult()->getTotal(); diff --git a/module/VuFind/src/VuFind/Search/Blender/Params.php b/module/VuFind/src/VuFind/Search/Blender/Params.php index 31442387a7b..2c8222c857b 100644 --- a/module/VuFind/src/VuFind/Search/Blender/Params.php +++ b/module/VuFind/src/VuFind/Search/Blender/Params.php @@ -446,7 +446,7 @@ public function getBackendParameters(): ParamBag $result = parent::getBackendParameters(); foreach ($this->unsupportedFilters as $backendId => $filters) { if ($filters) { - $result->add('fq', "-blender_backend:\"$backendId\""); + $result->add('filter', "-blender_backend:\"$backendId\""); } } foreach ($this->searchParams as $params) { diff --git a/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php index 3b819062e26..c9c2fa84201 100644 --- a/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php +++ b/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php @@ -494,8 +494,8 @@ protected function createConnector() $handlers = [ 'select' => [ 'fallback' => true, - 'defaults' => ['fl' => $defaultFields], - 'appends' => ['fq' => []], + 'defaults' => ['fields' => $defaultFields], + 'appends' => ['filter' => []], ], 'terms' => [ 'functions' => ['terms'], @@ -503,7 +503,7 @@ protected function createConnector() ]; foreach ($this->getHiddenFilters() as $filter) { - array_push($handlers['select']['appends']['fq'], $filter); + array_push($handlers['select']['appends']['filter'], $filter); } $connector = new $this->connectorClass( diff --git a/module/VuFind/src/VuFind/Search/Params/FacetLimitTrait.php b/module/VuFind/src/VuFind/Search/Params/FacetLimitTrait.php index 8618e24b3ea..a664b137684 100644 --- a/module/VuFind/src/VuFind/Search/Params/FacetLimitTrait.php +++ b/module/VuFind/src/VuFind/Search/Params/FacetLimitTrait.php @@ -77,10 +77,10 @@ trait FacetLimitTrait protected function initFacetLimitsFromConfig(?Config $config = null) { if (is_numeric($config->facet_limit ?? null)) { - $this->setFacetLimit($config->facet_limit); + $this->setFacetLimit((int)($config->facet_limit)); } foreach ($config->facet_limit_by_field ?? [] as $k => $v) { - $this->facetLimitByField[$k] = $v; + $this->facetLimitByField[$k] = (int)$v; } } @@ -91,7 +91,7 @@ protected function initFacetLimitsFromConfig(?Config $config = null) * * @return void */ - public function setFacetLimit($l) + public function setFacetLimit(int $l): void { $this->facetLimit = $l; } @@ -125,7 +125,7 @@ public function getHierarchicalFacetLimit() * * @return void */ - public function setHierarchicalFacetLimit($limit) + public function setHierarchicalFacetLimit(int $limit) { $this->hierarchicalFacetLimit = $limit; } diff --git a/module/VuFind/src/VuFind/Search/Params/FacetRestrictionsTrait.php b/module/VuFind/src/VuFind/Search/Params/FacetRestrictionsTrait.php index a310b1c691b..03208b3e0fe 100644 --- a/module/VuFind/src/VuFind/Search/Params/FacetRestrictionsTrait.php +++ b/module/VuFind/src/VuFind/Search/Params/FacetRestrictionsTrait.php @@ -50,13 +50,6 @@ trait FacetRestrictionsTrait */ protected $facetPrefixByField = []; - /** - * Per-field facet matches - * - * @var array - */ - protected $facetMatchesByField = []; - /** * Initialize facet prefix and matches from a Config object. * @@ -69,9 +62,6 @@ protected function initFacetRestrictionsFromConfig(?Config $config = null) foreach ($config->facet_prefix_by_field ?? [] as $k => $v) { $this->facetPrefixByField[$k] = $v; } - foreach ($config->facet_matches_by_field ?? [] as $k => $v) { - $this->facetMatchesByField[$k] = $v; - } } /** @@ -86,18 +76,6 @@ public function setFacetPrefixByField(array $new) $this->facetPrefixByField = $new; } - /** - * Set Facet Matches by Field - * - * @param array $new Associative array of $field name => $limit - * - * @return void - */ - public function setFacetMatchesByField(array $new) - { - $this->facetMatchesByField = $new; - } - /** * Get the facet prefix for the specified field. * @@ -110,17 +88,4 @@ protected function getFacetPrefixForField($field) $prefix = $this->facetPrefixByField[$field] ?? ''; return $prefix; } - - /** - * Get the facet matches for the specified field. - * - * @param string $field Field to look up - * - * @return string - */ - protected function getFacetMatchesForField($field) - { - $matches = $this->facetMatchesByField[$field] ?? ''; - return $matches; - } } diff --git a/module/VuFind/src/VuFind/Search/Solr/CustomFilterListener.php b/module/VuFind/src/VuFind/Search/Solr/CustomFilterListener.php index 4c5b03702cf..bfe486a16a8 100644 --- a/module/VuFind/src/VuFind/Search/Solr/CustomFilterListener.php +++ b/module/VuFind/src/VuFind/Search/Solr/CustomFilterListener.php @@ -74,7 +74,7 @@ class CustomFilterListener * * @var string */ - protected $filterParam = 'fq'; + protected $filterParam = 'filter'; /** * Constructor. diff --git a/module/VuFind/src/VuFind/Search/Solr/DeduplicationListener.php b/module/VuFind/src/VuFind/Search/Solr/DeduplicationListener.php index ed653963e50..1936e8ec4df 100644 --- a/module/VuFind/src/VuFind/Search/Solr/DeduplicationListener.php +++ b/module/VuFind/src/VuFind/Search/Solr/DeduplicationListener.php @@ -154,15 +154,15 @@ public function onSearchPre(EventInterface $event) $this->enabled && 'getids' !== $context && !$this->hasChildFilter($params) ) { - $fq = '-merged_child_boolean:true'; + $filter = '-merged_child_boolean:true'; if ($context == 'similar' && $id = $event->getParam('id')) { - $fq .= ' AND -local_ids_str_mv:"' + $filter .= ' AND -local_ids_str_mv:"' . addcslashes($id, '"') . '"'; } } else { - $fq = '-merged_boolean:true'; + $filter = '-merged_boolean:true'; } - $params->add('fq', $fq); + $params->add('filter', $filter); } } return $event; @@ -177,7 +177,7 @@ public function onSearchPre(EventInterface $event) */ public function hasChildFilter($params) { - $filters = $params->get('fq'); + $filters = $params->get('filter'); return $filters != null && in_array('merged_child_boolean:true', $filters); } diff --git a/module/VuFind/src/VuFind/Search/Solr/DefaultParametersListener.php b/module/VuFind/src/VuFind/Search/Solr/DefaultParametersListener.php index 89ce70a75c2..e588300f0c8 100644 --- a/module/VuFind/src/VuFind/Search/Solr/DefaultParametersListener.php +++ b/module/VuFind/src/VuFind/Search/Solr/DefaultParametersListener.php @@ -29,9 +29,11 @@ namespace VuFind\Search\Solr; +use JsonException; use Laminas\EventManager\EventInterface; use Laminas\EventManager\SharedEventManagerInterface; use VuFindSearch\Backend\Solr\Backend; +use VuFindSearch\NestingParamBag; use VuFindSearch\Service; /** @@ -119,16 +121,21 @@ public function onSearchPre(EventInterface $event): EventInterface $context = null; } $context = $this->contextMap[$context] ?? $context; - $defaultParams = $this->defaultParams[$context] + $defaultParamsText = $this->defaultParams[$context] ?? $this->defaultParams['*'] ?? ''; - if ($defaultParams && $params = $command->getSearchParameters()) { - foreach (explode('&', $defaultParams) as $keyVal) { - $parts = explode('=', $keyVal, 2); - if (!isset($parts[1])) { - continue; - } - $params->add(urldecode($parts[0]), urldecode($parts[1])); + if ($defaultParamsText && $params = NestingParamBag::from($command->getSearchParameters())) { + $command->setSearchParameters($params); + try { + $defaultParams = json_decode($defaultParamsText, true, flags: JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new \Exception( + 'Default parameters must be expressed in JSON, using Solr\'s JSON Request API ' + . '(starting in VuFind 12)' + ); + } + foreach ($defaultParams as $name => $param) { + $params->addMultiNested($name, $param); } } } diff --git a/module/VuFind/src/VuFind/Search/Solr/Explanation.php b/module/VuFind/src/VuFind/Search/Solr/Explanation.php index a639d167e1f..8b857542259 100644 --- a/module/VuFind/src/VuFind/Search/Solr/Explanation.php +++ b/module/VuFind/src/VuFind/Search/Solr/Explanation.php @@ -31,6 +31,7 @@ namespace VuFind\Search\Solr; use VuFindSearch\Backend\Solr\Command\RawJsonSearchCommand; +use VuFindSearch\NestingParamBag; use VuFindSearch\ParamBag; use function count; @@ -249,13 +250,16 @@ public function performRequest($recordId) // prepare search params $params = $this->getParams()->getBackendParameters(); - $params->set('spellcheck', 'false'); - $explainParams = new ParamBag([ - 'fl' => 'id,score', - 'debug' => 'true', - 'indent' => 'true', - 'echoParams' => 'all', - 'explainOther' => 'id:"' . addcslashes($recordId, '"') . '"', + $params = NestingParamBag::from($params); + $params->setNested('params', 'spellcheck', 'false'); + $explainParams = new NestingParamBag([ + 'fields' => 'id,score', + 'params' => new ParamBag([ + 'debug' => 'true', + 'indent' => 'true', + 'echoParams' => 'all', + 'explainOther' => 'id:"' . addcslashes($recordId, '"') . '"', + ]), ]); $params->mergeWith($explainParams); diff --git a/module/VuFind/src/VuFind/Search/Solr/FilterFieldConversionListener.php b/module/VuFind/src/VuFind/Search/Solr/FilterFieldConversionListener.php index febfa19f488..d60259e8f86 100644 --- a/module/VuFind/src/VuFind/Search/Solr/FilterFieldConversionListener.php +++ b/module/VuFind/src/VuFind/Search/Solr/FilterFieldConversionListener.php @@ -91,12 +91,12 @@ public function attach(SharedEventManagerInterface $manager) public function onSearchPre(EventInterface $event) { $params = $event->getParam('command')->getSearchParameters(); - $fq = $params->get('fq'); - if (is_array($fq) && !empty($fq)) { + $filters = $params->get('filter'); + if (is_array($filters) && !empty($filters)) { // regex lookahead to ignore strings inside quotes: $lookahead = '(?=(?:[^\"]*+\"[^\"]*+\")*+[^\"]*+$)'; - $new_fq = []; - foreach ($fq as $currentFilter) { + $new_filters = []; + foreach ($filters as $currentFilter) { foreach ($this->map as $oldField => $newField) { $currentFilter = preg_replace( "/\b$oldField:$lookahead/", @@ -104,9 +104,9 @@ public function onSearchPre(EventInterface $event) $currentFilter ); } - $new_fq[] = $currentFilter; + $new_filters[] = $currentFilter; } - $params->set('fq', $new_fq); + $params->set('filter', $new_filters); } return $event; diff --git a/module/VuFind/src/VuFind/Search/Solr/InjectConditionalFilterListener.php b/module/VuFind/src/VuFind/Search/Solr/InjectConditionalFilterListener.php index 47470d9938b..3f31ef4b35a 100644 --- a/module/VuFind/src/VuFind/Search/Solr/InjectConditionalFilterListener.php +++ b/module/VuFind/src/VuFind/Search/Solr/InjectConditionalFilterListener.php @@ -147,12 +147,12 @@ public function onSearchPre(EventInterface $event) } $params = $command->getSearchParameters(); - $fq = $params->get('fq'); - if (!is_array($fq)) { - $fq = []; + $filters = $params->get('filter'); + if (!is_array($filters)) { + $filters = []; } - $new_fq = array_merge($fq, $this->filterList); - $params->set('fq', $new_fq); + $new_filters = array_merge($filters, $this->filterList); + $params->set('filter', $new_filters); return $event; } diff --git a/module/VuFind/src/VuFind/Search/Solr/InjectHighlightingListener.php b/module/VuFind/src/VuFind/Search/Solr/InjectHighlightingListener.php index 0e286f545c2..aafa3b016e1 100644 --- a/module/VuFind/src/VuFind/Search/Solr/InjectHighlightingListener.php +++ b/module/VuFind/src/VuFind/Search/Solr/InjectHighlightingListener.php @@ -32,6 +32,7 @@ use Laminas\EventManager\EventInterface; use Laminas\EventManager\SharedEventManagerInterface; use VuFindSearch\Backend\BackendInterface; +use VuFindSearch\NestingParamBag; use VuFindSearch\Service; /** @@ -120,18 +121,19 @@ public function onSearchPre(EventInterface $event) } if ($command->getTargetIdentifier() === $this->backend->getIdentifier()) { if ($params = $command->getSearchParameters()) { + $params = NestingParamBag::from($params); // Set highlighting parameters unless explicitly disabled: - $hl = $params->get('hl'); + $hl = $params->getNested('params', 'hl'); if (($hl[0] ?? 'true') != 'false') { $this->active = true; // Set extra parameters first so they don't override necessary // core parameters: foreach ($this->extraHighlightingParameters as $key => $val) { - $params->set($key, $val); + $params->addMultiNested($key, $val); } - $params->set('hl', 'true'); - $params->set('hl.simple.pre', '{{{{START_HILITE}}}}'); - $params->set('hl.simple.post', '{{{{END_HILITE}}}}'); + $params->setNested('params', 'hl', 'true'); + $params->setNested('params', 'hl.simple.pre', '{{{{START_HILITE}}}}'); + $params->setNested('params', 'hl.simple.post', '{{{{END_HILITE}}}}'); // Turn on hl.q generation in query builder: $this->backend->getQueryBuilder() diff --git a/module/VuFind/src/VuFind/Search/Solr/InjectSpellingListener.php b/module/VuFind/src/VuFind/Search/Solr/InjectSpellingListener.php index bee6156eeeb..d7cc8a74a7f 100644 --- a/module/VuFind/src/VuFind/Search/Solr/InjectSpellingListener.php +++ b/module/VuFind/src/VuFind/Search/Solr/InjectSpellingListener.php @@ -35,7 +35,7 @@ use VuFind\Log\LoggerAwareTrait; use VuFindSearch\Backend\BackendInterface; use VuFindSearch\Backend\Solr\Response\Json\Spellcheck; -use VuFindSearch\ParamBag; +use VuFindSearch\NestingParamBag; use VuFindSearch\Query\Query; use VuFindSearch\Service; @@ -128,8 +128,10 @@ public function onSearchPre(EventInterface $event) } if ($command->getTargetIdentifier() === $this->backend->getIdentifier()) { if ($params = $command->getSearchParameters()) { + $params = NestingParamBag::from($params); + // Set spelling parameters when enabled: - $sc = $params->get('spellcheck'); + $sc = $params->getNested('params', 'spellcheck'); if (isset($sc[0]) && $sc[0] != 'false') { $this->active = true; if (empty($this->dictionaries)) { @@ -140,8 +142,9 @@ public function onSearchPre(EventInterface $event) // Set relevant Solr parameters: reset($this->dictionaries); - $params->set('spellcheck', 'true'); - $params->set( + $params->setNested('params', 'spellcheck', 'true'); + $params->setNested( + 'params', 'spellcheck.dictionary', current($this->dictionaries) ); @@ -175,7 +178,8 @@ public function onSearchPost(EventInterface $event) if ($command->getTargetIdentifier() === $this->backend->getIdentifier()) { $result = $command->getResult(); $params = $command->getSearchParameters(); - $spellcheckQuery = $params->get('spellcheck.q'); + $params = NestingParamBag::from($params); + $spellcheckQuery = $params->getNested('params', 'spellcheck.q'); if (!empty($spellcheckQuery)) { $this->aggregateSpellcheck( $result->getSpellcheck(), @@ -197,9 +201,9 @@ public function onSearchPost(EventInterface $event) protected function aggregateSpellcheck(Spellcheck $spellcheck, $query) { while (next($this->dictionaries) !== false) { - $params = new ParamBag(); - $params->set('spellcheck', 'true'); - $params->set('spellcheck.dictionary', current($this->dictionaries)); + $params = new NestingParamBag(); + $params->setNested('params', 'spellcheck', 'true'); + $params->setNested('params', 'spellcheck.dictionary', current($this->dictionaries)); $queryObj = new Query($query, 'AllFields'); try { $collection = $this->backend->search($queryObj, 0, 0, $params); diff --git a/module/VuFind/src/VuFind/Search/Solr/MultiIndexListener.php b/module/VuFind/src/VuFind/Search/Solr/MultiIndexListener.php index cee2cbb8f27..745caf73c60 100644 --- a/module/VuFind/src/VuFind/Search/Solr/MultiIndexListener.php +++ b/module/VuFind/src/VuFind/Search/Solr/MultiIndexListener.php @@ -32,6 +32,8 @@ use Laminas\EventManager\EventInterface; use Laminas\EventManager\SharedEventManagerInterface; use VuFindSearch\Backend\BackendInterface; +use VuFindSearch\NestingParamBag; +use VuFindSearch\ParamBag; use VuFindSearch\Service; use function in_array; @@ -126,11 +128,12 @@ public function onSearchPre(EventInterface $event) $command = $event->getParam('command'); if ($command->getTargetIdentifier() === $this->backend->getIdentifier()) { $params = $command->getSearchParameters(); + $params = NestingParamBag::from($params); $allShardsContexts = ['retrieve', 'retrieveBatch']; if (in_array($command->getContext(), $allShardsContexts)) { // If we're retrieving by id(s), we should pull all shards to be // sure we find the right record(s). - $params->set('shards', implode(',', $this->shards)); + $params->setNested('params', 'shards', implode(',', $this->shards)); } else { // In any other context, we should make sure our field values are // all legal. @@ -138,7 +141,7 @@ public function onSearchPre(EventInterface $event) // Normalize array of strings containing comma-separated values to // simple array of values; check if $params->get('shards') returns // an array to prevent invalid argument warnings. - $shards = $params->get('shards'); + $shards = $params->getNested('params', 'shards'); $shards = explode( ',', implode(',', (is_array($shards) ? $shards : [])) @@ -146,8 +149,16 @@ public function onSearchPre(EventInterface $event) $fields = $this->getFields($shards); $specs = $this->getSearchSpecs($fields); $this->backend->getQueryBuilder()->setSpecs($specs); - $facets = $params->get('facet.field') ?: []; - $params->set('facet.field', array_diff($facets, $fields)); + if ($params->get('facet')) { + $facets = $params->get('facet')[0]->getArrayCopy(); + $facets = array_filter( + $facets, + fn ($facet) => + (!($facet[0] instanceof ParamBag)) + || !in_array($facet[0]->getArrayCopy()['field'][0], $fields) + ); + $params->set('facet', new ParamBag($facets)); + } } } return $event; diff --git a/module/VuFind/src/VuFind/Search/Solr/Params.php b/module/VuFind/src/VuFind/Search/Solr/Params.php index f4136e8942d..3f1e0bcf95c 100644 --- a/module/VuFind/src/VuFind/Search/Solr/Params.php +++ b/module/VuFind/src/VuFind/Search/Solr/Params.php @@ -32,7 +32,7 @@ use VuFind\Config\Config; use VuFind\Config\ConfigManagerInterface; -use VuFindSearch\ParamBag; +use VuFindSearch\NestingParamBag; use function count; use function in_array; @@ -58,17 +58,17 @@ class Params extends \VuFind\Search\Base\Params * Search with facet.contains * cf. https://lucene.apache.org/solr/guide/7_3/faceting.html * - * @var string + * @var ?string */ - protected $facetContains = null; + protected ?string $facetContains = null; /** * Ignore Case when using facet.contains * cf. https://lucene.apache.org/solr/guide/7_3/faceting.html * - * @var bool + * @var ?bool */ - protected $facetContainsIgnoreCase = null; + protected ?bool $facetContainsIgnoreCase = null; /** * Offset for facet results @@ -237,20 +237,8 @@ public function getFacetSettings() if (!empty($this->facetConfig)) { $dateRangeTypes = $this->getOptions()->getDateRangeFieldTypes(); - $facetSet['limit'] = $this->facetLimit; foreach (array_keys($this->facetConfig) as $facetField) { - $fieldLimit = $this->getFacetLimitForField($facetField); - if ($fieldLimit != $this->facetLimit) { - $facetSet["f.{$facetField}.facet.limit"] = $fieldLimit; - } - $fieldPrefix = $this->getFacetPrefixForField($facetField); - if (!empty($fieldPrefix)) { - $facetSet["f.{$facetField}.facet.prefix"] = $fieldPrefix; - } - $fieldMatches = $this->getFacetMatchesForField($facetField); - if (!empty($fieldMatches)) { - $facetSet["f.{$facetField}.facet.matches"] = $fieldMatches; - } + // TODO Figure out date range field if ('DateRangeField' === ($dateRangeTypes[$facetField] ?? null)) { $startYear = $this->getOptions()->getDateRangeSliderMinValue($facetField) ?? VUFIND_DEFAULT_EARLIEST_YEAR; @@ -263,29 +251,36 @@ public function getFacetSettings() $facetSet["f.{$facetField}.facet.range.gap"] = '+1YEAR'; $facetSet['range'][] = $facetField; } else { + $facetFieldName = $facetField; + $fieldLimit = $this->getFacetLimitForField($facetField); + + $facet = [ + 'type' => 'terms', + 'field' => $facetField, + 'limit' => $fieldLimit, + ]; + + $facet['sort'] = in_array($facetField, $this->indexSortedFacets ?? []) + ? 'index' + : ($this->facetSort ?: 'count'); + + $fieldPrefix = $this->getFacetPrefixForField($facetField); + if (!empty($fieldPrefix)) { + $facet['prefix'] = $fieldPrefix; + } elseif ($this->facetPrefix != null) { + $facet['prefix'] = $this->facetPrefix; + } + + if ($this->facetOffset != null) { + $facet['offset'] = $this->facetOffset; + } + if ($this->getFacetOperator($facetField) == 'OR') { - $facetField = '{!ex=' . $facetField . '_filter}' . $facetField; + $facet['domain'] ??= []; + $facet['domain']['excludeTags'] = $facetField . '_filter'; } - $facetSet['field'][] = $facetField; - } - } - if ($this->facetContains != null) { - $facetSet['contains'] = $this->facetContains; - } - if ($this->facetContainsIgnoreCase != null) { - $facetSet['contains.ignoreCase'] - = $this->facetContainsIgnoreCase ? 'true' : 'false'; - } - if ($this->facetOffset != null) { - $facetSet['offset'] = $this->facetOffset; - } - if ($this->facetPrefix != null) { - $facetSet['prefix'] = $this->facetPrefix; - } - $facetSet['sort'] = $this->facetSort ?: 'count'; - if ($this->indexSortedFacets != null) { - foreach ($this->indexSortedFacets as $field) { - $facetSet["f.{$field}.facet.sort"] = 'index'; + + $facetSet[$facetFieldName] = $facet; } } } @@ -337,6 +332,16 @@ public function setFacetContains($p) $this->facetContains = $p; } + /** + * Get Facet Contains + * + * @return ?string The contains value + */ + public function getFacetContains() + { + return $this->facetContains; + } + /** * Set Facet Contains Ignore Case * @@ -349,6 +354,16 @@ public function setFacetContainsIgnoreCase($val) $this->facetContainsIgnoreCase = $val; } + /** + * Get Facet Contains Ignore Case + * + * @return ?bool The boolean value + */ + public function getFacetContainsIgnoreCase() + { + return $this->facetContainsIgnoreCase; + } + /** * Set Facet Offset * @@ -567,14 +582,15 @@ protected function normalizeSort($sort) /** * Create search backend parameters for advanced features. * - * @return ParamBag + * @return NestingParamBag */ public function getBackendParameters() { - $backendParams = new ParamBag(); + $backendParams = new NestingParamBag(); // Spellcheck - $backendParams->set( + $backendParams->setNested( + 'params', 'spellcheck', $this->getOptions()->spellcheckEnabled() ? 'true' : 'false' ); @@ -582,20 +598,13 @@ public function getBackendParameters() // Facets $facets = $this->getFacetSettings(); if (!empty($facets)) { - $backendParams->add('facet', 'true'); - - foreach ($facets as $key => $value) { - // prefix keys with "facet" unless they already have a "f." prefix: - $fullKey = str_starts_with($key, 'f.') ? $key : "facet.$key"; - $backendParams->add($fullKey, $value); - } - $backendParams->add('facet.mincount', 1); + $backendParams->addMultiNested('facet', $facets); } // Filters $filters = $this->getFilterSettings(); foreach ($filters as $filter) { - $backendParams->add('fq', $filter); + $backendParams->add('filter', $filter); } // Shards @@ -611,7 +620,7 @@ public function getBackendParameters() foreach ($shards as $current) { $selectedShards[$current] = $allShards[$current]; } - $backendParams->add('shards', implode(',', $selectedShards)); + $backendParams->addNested('params', 'shards', implode(',', $selectedShards)); } // Sort @@ -633,14 +642,14 @@ public function getBackendParameters() // Highlighting -- on by default, but we should disable if necessary: if (!$this->getOptions()->highlightEnabled()) { - $backendParams->add('hl', 'false'); + $backendParams->addNested('params', 'hl', 'false'); } // Pivot facets for visual results if ($pf = $this->getPivotFacets()) { - $backendParams->add('facet.pivot', $pf); - $backendParams->set('facet', 'true'); + $backendParams->addNested('params', 'facet.pivot', $pf); + $backendParams->setNested('params', 'facet', 'true'); } return $backendParams; diff --git a/module/VuFind/src/VuFind/Search/Solr/Results.php b/module/VuFind/src/VuFind/Search/Solr/Results.php index f0041be3cc5..c5612ea727c 100644 --- a/module/VuFind/src/VuFind/Search/Solr/Results.php +++ b/module/VuFind/src/VuFind/Search/Solr/Results.php @@ -30,6 +30,7 @@ namespace VuFind\Search\Solr; use VuFindSearch\Command\SearchCommand; +use VuFindSearch\NestingParamBag; use VuFindSearch\Query\AbstractQuery; use VuFindSearch\Query\QueryGroup; @@ -194,13 +195,14 @@ protected function performSearch() $limit = $this->getParams()->getLimit(); $offset = $this->getStartRecord() - 1; $params = $this->getParams()->getBackendParameters(); + $params = NestingParamBag::from($params); $searchService = $this->getSearchService(); $cursorMark = $this->getCursorMark(); if (null !== $cursorMark) { - $params->set('cursorMark', '' === $cursorMark ? '*' : $cursorMark); + $params->setNested('params', 'cursorMark', '' === $cursorMark ? '*' : $cursorMark); // Override any default timeAllowed since it cannot be used with // cursorMark - $params->set('timeAllowed', -1); + $params->setNested('params', 'timeAllowed', -1); } try { @@ -432,9 +434,22 @@ public function getPartialFieldFacets( // Do search $result = $clone->getFacetList(); - $filteredCounts = $clone->getFilteredFacetCounts(); + + // Apply "facet contains" + if ($facetContains = $params->getFacetContains()) { + $ignoreCase = $params->getFacetContainsIgnoreCase(); + foreach ($result as &$values) { + $values['list'] = array_filter( + $values['list'], + fn ($value) => $ignoreCase + ? stripos($value['value'], $facetContains) !== false + : str_contains($value['value'], $facetContains) + ); + } + } // Reformat into a hash: + $filteredCounts = $clone->getFilteredFacetCounts(); foreach ($result as $key => $value) { // Detect next page and crop results if necessary $more = false; diff --git a/module/VuFind/src/VuFindTest/Feature/MockSearchCommandTrait.php b/module/VuFind/src/VuFindTest/Feature/MockSearchCommandTrait.php index 584cfaa1b5e..98bab166025 100644 --- a/module/VuFind/src/VuFindTest/Feature/MockSearchCommandTrait.php +++ b/module/VuFind/src/VuFindTest/Feature/MockSearchCommandTrait.php @@ -59,12 +59,20 @@ protected function getMockSearchCommand( string $backendId = 'foo', $result = null ): Command { + $storedParams = $params; $command = $this->getMockBuilder(Command::class) ->disableOriginalConstructor() ->getMock(); - if ($params) { - $command->expects($this->any())->method('getSearchParameters')->willReturn($params); - } + $command->expects($this->any())->method('getSearchParameters')->willReturnCallback( + function () use (&$storedParams) { + return $storedParams; + } + ); + $command->expects($this->any())->method('setSearchParameters')->willReturnCallback( + function ($params) use (&$storedParams): void { + $storedParams = $params; + } + ); if ($context) { $command->expects($this->any())->method('getContext')->willReturn($context); } diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ChannelProvider/SimilarItemsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ChannelProvider/SimilarItemsTest.php index 5fd1547809a..18bad6d9c9c 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/ChannelProvider/SimilarItemsTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ChannelProvider/SimilarItemsTest.php @@ -53,7 +53,7 @@ class SimilarItemsTest extends \PHPUnit\Framework\TestCase */ public function testGetFromRecord(): void { - [$similar, $expectedResult] = $this->configureTestTargetAndExpectations(['rows' => 24]); + [$similar, $expectedResult] = $this->configureTestTargetAndExpectations(['limit' => 24]); $recordDriver = $this->getDriver(); $this->assertSame($expectedResult, $similar->getFromRecord($recordDriver)); } @@ -82,7 +82,7 @@ public function testGetFromSearch(): void $recordDriver = $this->getDriver(); $results->expects($this->once())->method('getResults') ->willReturn([$recordDriver]); - [$similar, $expectedResult] = $this->configureTestTargetAndExpectations(['rows' => 24]); + [$similar, $expectedResult] = $this->configureTestTargetAndExpectations(['limit' => 24]); $this->assertSame($expectedResult, $similar->getFromSearch($results)); } @@ -122,7 +122,7 @@ public function testGetFromSearchWhenChannelsIsEmpty(): void $results->expects($this->once())->method('getResults') ->willReturn([$recordDriver]); [$similar, $expectedResult] = $this->configureTestTargetAndExpectations( - ['maxRecordsToExamine' => 0, 'rows' => 24], + ['maxRecordsToExamine' => 0, 'limit' => 24], true ); $this->assertSame( @@ -145,14 +145,14 @@ public function configureTestTargetAndExpectations( $options = [], $fetchFromSearchService = false ) { - $options = array_merge(['maxRecordsToExamine' => 1, 'rows' => 20], $options); + $options = array_merge(['maxRecordsToExamine' => 1, 'limit' => 20], $options); $mockObjects = $this->getSimilarItems($options); $similar = $mockObjects['similar']; $search = $mockObjects['search']; $url = $mockObjects['url']; $router = $mockObjects['router']; $similar->setProviderId('foo_ProviderId'); - $params = new ParamBag(['rows' => $options['rows']]); + $params = new ParamBag(['limit' => $options['limit']]); $retrieveParams = new ParamBag(); $commandObj = $this->createMock(\VuFindSearch\Command\AbstractBase::class); $collection = $this->createMock(\VuFindSearch\Response\RecordCollectionInterface::class); diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/RandomRecommendTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/RandomRecommendTest.php index f49b09556c3..e512b139bb6 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/RandomRecommendTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/RandomRecommendTest.php @@ -164,10 +164,13 @@ public function testCanInitialise() && $command->getTargetIdentifier() === 'Solr' && $command->getArguments()[0]->getAllTerms() === 'john smith' && $command->getArguments()[1] === 10 - && $command->getArguments()[2]->getArrayCopy() === - ['spellcheck' => ['true'], - 'fq' => ['facet1:"value1"', 'facet2:"value2"'], - 'hl' => ['false']]; + && $command->getArguments()[2]->jsonSerialize() == [ + 'params' => [ + 'spellcheck' => 'true', + 'hl' => 'false', + ], + 'filter' => ['facet1:"value1"', 'facet2:"value2"'], + ]; }; $service->expects($this->once())->method('invoke') ->with($this->callback($checkCommand)) @@ -208,10 +211,14 @@ public function testCanInitialiseInDisregardMode() && $command->getTargetIdentifier() === 'Solr' && $command->getArguments()[0]->getAllTerms() === 'john smith' && $command->getArguments()[1] === 10 - && $command->getArguments()[2]->getArrayCopy() === - ['spellcheck' => ['true'], - 'fq' => ['facet1:"value1"', - 'facet2:"value2"'], 'hl' => ['false']]; + && $command->getArguments()[2]->jsonSerialize() == + [ + 'params' => [ + 'spellcheck' => 'true', + 'hl' => 'false', + ], + 'filter' => ['facet1:"value1"', 'facet2:"value2"'], + ]; }; $service->expects($this->once())->method('invoke') ->with($this->callback($checkCommand)) @@ -255,10 +262,14 @@ protected function getConfiguredModule($recConfig): Random && $command->getTargetIdentifier() === 'Solr' && $command->getArguments()[0]->getAllTerms() === 'john smith' && $command->getArguments()[1] === 10 - && $command->getArguments()[2]->getArrayCopy() === - ['spellcheck' => ['true'], - 'fq' => ['facet1:"value1"', - 'facet2:"value2"'], 'hl' => ['false']]; + && $command->getArguments()[2]->jsonSerialize() == + [ + 'params' => [ + 'spellcheck' => 'true', + 'hl' => 'false', + ], + 'filter' => ['facet1:"value1"', 'facet2:"value2"'], + ]; }; $service->expects($this->once())->method('invoke') ->with($this->callback($checkCommand)) diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Blender/ParamsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Blender/ParamsTest.php index d4be227f282..1ce560221cf 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Blender/ParamsTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Blender/ParamsTest.php @@ -307,7 +307,7 @@ public function testFacetsAndFilters(): void 'format:"bar"', '-blender_backend:"EDS"', ], - $backendParams->get('fq') + $backendParams->get('filter') ); $solrParams = $backendParams->get('params_Solr')[0]; @@ -317,7 +317,7 @@ public function testFacetsAndFilters(): void $this->assertEquals( ['formatSolr:"bar"'], - $solrParams->get('fq') + $solrParams->get('filter') ); $this->assertEquals( [ @@ -338,7 +338,7 @@ public function testFacetsAndFilters(): void // Remove a filter and check that EDS is enabled again: $params->removeFilter('format:bar'); $backendParams = $params->getBackendParameters(); - $this->assertNull($backendParams->get('fq')); + $this->assertNull($backendParams->get('filter')); // Add multiple filters: $params->addFilter('~format:bar'); @@ -357,7 +357,7 @@ public function testFacetsAndFilters(): void '{!tag=formatSolr_filter}formatSolr:(formatSolr:"bar"' . ' OR formatSolr:"baz")', ], - $solrParams->get('fq') + $solrParams->get('filter') ); $this->assertEquals( [ @@ -383,12 +383,12 @@ public function testFacetsAndFilters(): void $params->removeFilter('~format:bar'); $params->removeFilter('~format:baz'); $backendParams = $params->getBackendParameters(); - $this->assertEquals(['fulltext:"1"'], $backendParams->get('fq')); + $this->assertEquals(['fulltext:"1"'], $backendParams->get('filter')); $solrParams = $backendParams->get('params_Solr')[0]; $primoParams = $backendParams->get('params_Primo')[0]; $edsParams = $backendParams->get('params_EDS')[0]; - $this->assertEquals(['fulltext_boolean:"1"'], $solrParams->get('fq')); + $this->assertEquals(['fulltext_boolean:"1"'], $solrParams->get('filter')); $this->assertEquals( [ [ @@ -404,12 +404,12 @@ public function testFacetsAndFilters(): void $params->removeAllFilters('building'); $params->removeFilter('fulltext:1'); $backendParams = $params->getBackendParameters(); - $this->assertNull($backendParams->get('fq')); + $this->assertNull($backendParams->get('filter')); $solrParams = $backendParams->get('params_Solr')[0]; $primoParams = $backendParams->get('params_Primo')[0]; $edsParams = $backendParams->get('params_EDS')[0]; - $this->assertNull($solrParams->get('fq')); + $this->assertNull($solrParams->get('filter')); $this->assertEquals( [ [ @@ -426,12 +426,12 @@ public function testFacetsAndFilters(): void $params = $this->getParams(); $params->addFilter('fulltext:1'); $backendParams = $params->getBackendParameters(); - $this->assertEquals(['fulltext:"1"'], $backendParams->get('fq')); + $this->assertEquals(['fulltext:"1"'], $backendParams->get('filter')); $solrParams = $backendParams->get('params_Solr')[0]; $primoParams = $backendParams->get('params_Primo')[0]; $edsParams = $backendParams->get('params_EDS')[0]; - $this->assertEquals(['fulltext_boolean:"1"'], $solrParams->get('fq')); + $this->assertEquals(['fulltext_boolean:"1"'], $solrParams->get('filter')); $this->assertEquals( [ [ @@ -455,7 +455,7 @@ public function testFacetsAndFilters(): void '-blender_backend:"Primo"', '-blender_backend:"EDS"', ], - $backendParams->get('fq') + $backendParams->get('filter') ); // Add a filter that maps to two values: @@ -485,7 +485,7 @@ public function testFacetsAndFilters(): void $params->addFilter('-format:double'); $backendParams = $params->getBackendParameters(); $solrParams = $backendParams->get('params_Solr')[0]; - $this->assertEquals(['-formatSolr:"double"'], $solrParams->get('fq')); + $this->assertEquals(['-formatSolr:"double"'], $solrParams->get('filter')); $primoParams = $backendParams->get('params_Primo')[0]; $this->assertEquals( [ @@ -510,7 +510,7 @@ public function testFacetsAndFilters(): void $params->removeFilter('-format:double'); $backendParams = $params->getBackendParameters(); $solrParams = $backendParams->get('params_Solr')[0]; - $this->assertEquals(null, $solrParams->get('fq')); + $this->assertEquals(null, $solrParams->get('filter')); $primoParams = $backendParams->get('params_Primo')[0]; $this->assertEquals( [ @@ -528,12 +528,12 @@ public function testFacetsAndFilters(): void $params->addFilter('format:double'); $params->removeAllFilters(); $backendParams = $params->getBackendParameters(); - $this->assertNull($backendParams->get('fq')); + $this->assertNull($backendParams->get('filter')); $solrParams = $backendParams->get('params_Solr')[0]; $primoParams = $backendParams->get('params_Primo')[0]; $edsParams = $backendParams->get('params_EDS')[0]; - $this->assertNull($solrParams->get('fq')); + $this->assertNull($solrParams->get('filter')); $this->assertEquals( [ [ @@ -555,7 +555,7 @@ public function testFacetsAndFilters(): void [ 'fulltext:"1"', ], - $backendParams->get('fq') + $backendParams->get('filter') ); // Test a daterange filter: @@ -564,13 +564,13 @@ public function testFacetsAndFilters(): void $backendParams = $params->getBackendParameters(); $this->assertEquals( ['publish_date:[2020 TO 2022]'], - $backendParams->get('fq') + $backendParams->get('filter') ); $solrParams = $backendParams->get('params_Solr')[0]; $primoParams = $backendParams->get('params_Primo')[0]; $edsParams = $backendParams->get('params_EDS')[0]; - $this->assertEquals(['publishDate:[2020 TO 2022]'], $solrParams->get('fq')); + $this->assertEquals(['publishDate:[2020 TO 2022]'], $solrParams->get('filter')); $this->assertEquals( [ [ @@ -609,7 +609,7 @@ public function testHiddenFilters(): void 'format:"bar"', '-blender_backend:"EDS"', ], - $backendParams->get('fq') + $backendParams->get('filter') ); $solrParams = $backendParams->get('params_Solr')[0]; @@ -619,7 +619,7 @@ public function testHiddenFilters(): void $this->assertEquals( ['formatSolr:"bar"'], - $solrParams->get('fq') + $solrParams->get('filter') ); $this->assertEquals( [ @@ -650,15 +650,15 @@ public function testBlenderFilters(): void $params->addFilter('blender_backend:Primo'); $backendParams = $params->getBackendParameters(); - $this->assertEquals(['blender_backend:"Primo"'], $backendParams->get('fq')); + $this->assertEquals(['blender_backend:"Primo"'], $backendParams->get('filter')); $solrParams = $backendParams->get('params_Solr')[0]; $this->assertInstanceOf(ParamBag::class, $solrParams); - $this->assertNull($solrParams->get('fq')); + $this->assertNull($solrParams->get('filter')); // Remove the filter and check: $params->removeFilter('blender_backend:Primo'); $backendParams = $params->getBackendParameters(); - $this->assertNull($backendParams->get('fq')); + $this->assertNull($backendParams->get('filter')); // Add as a hidden filter: $params = $this->getParams(); @@ -671,11 +671,11 @@ public function testBlenderFilters(): void '{!tag=blender_backend_filter}blender_backend:(' . 'blender_backend:"Primo" OR blender_backend:"EDS")', ], - $backendParams->get('fq') + $backendParams->get('filter') ); $solrParams = $backendParams->get('params_Solr')[0]; $this->assertInstanceOf(ParamBag::class, $solrParams); - $this->assertNull($solrParams->get('fq')); + $this->assertNull($solrParams->get('filter')); } /** @@ -702,7 +702,7 @@ public function testBlenderFacets(): void $backendParams = $params->getBackendParameters(); // Make sure no backend was disabled: - $this->assertNull($backendParams->get('fq')); + $this->assertNull($backendParams->get('filter')); $primoParams = $backendParams->get('params_Primo')[0]; $this->assertInstanceOf(ParamBag::class, $primoParams); $this->assertEquals( @@ -733,7 +733,7 @@ public function testIgnoredFilters(): void 'format:"bar"', '-blender_backend:"EDS"', ], - $backendParams->get('fq') + $backendParams->get('filter') ); // Test ignoring all values: @@ -749,7 +749,7 @@ public function testIgnoredFilters(): void [ 'format:"bar"', ], - $backendParams->get('fq') + $backendParams->get('filter') ); $edsParams = $backendParams->get('params_EDS')[0]; $this->assertInstanceOf(ParamBag::class, $edsParams); @@ -770,7 +770,7 @@ public function testIgnoredFilters(): void [ 'format:"bar"', ], - $backendParams->get('fq') + $backendParams->get('filter') ); $edsParams = $backendParams->get('params_EDS')[0]; $this->assertInstanceOf(ParamBag::class, $edsParams); @@ -786,7 +786,7 @@ public function testIgnoredFilters(): void 'format:"baz"', '-blender_backend:"EDS"', ], - $backendParams->get('fq') + $backendParams->get('filter') ); } @@ -837,7 +837,7 @@ public function testFacetMappings(): void 'building:"0/Main/"', '-blender_backend:"Primo"', ], - $backendParams->get('fq') + $backendParams->get('filter') ); $solrParams = $backendParams->get('params_Solr')[0]; @@ -845,32 +845,24 @@ public function testFacetMappings(): void $this->assertEquals( [ - 'spellcheck' => [ - 'true', - ], 'facet' => [ - 'true', - ], - 'facet.limit' => [ - 30, - ], - 'facet.field' => [ - '{!ex=building_filter}building', - ], - 'facet.sort' => [ - 'count', - ], - 'facet.mincount' => [ - 1, - ], - 'fq' => [ - 'building:"0/Main/"', + 'building' => [ + 'type' => 'terms', + 'field' => 'building', + 'limit' => 30, + 'sort' => 'count', + 'domain' => [ + 'excludeTags' => 'building_filter', + ], + ], ], - 'hl' => [ - 'false', + 'filter' => 'building:"0/Main/"', + 'params' => [ + 'spellcheck' => 'true', + 'hl' => 'false', ], ], - $solrParams->getArrayCopy() + $solrParams->jsonSerialize() ); $this->assertEquals( @@ -899,7 +891,7 @@ public function testFacetMappings(): void 'building:"0/Sub/"', '-blender_backend:"Primo"', ], - $backendParams->get('fq') + $backendParams->get('filter') ); $edsParams = $backendParams->get('params_EDS')[0]; @@ -993,7 +985,7 @@ public function testInitFromRequest(): void $this->assertInstanceOf(ParamBag::class, $solrParams); $this->assertInstanceOf(ParamBag::class, $primoParams); $this->assertInstanceOf(ParamBag::class, $edsParams); - $this->assertNull($solrParams->get('fq')); + $this->assertNull($solrParams->get('filter')); $solrQuery = $backendParams->get('query_Solr')[0]; $primoQuery = $backendParams->get('query_Primo')[0]; diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ConditionalFilterListenerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ConditionalFilterListenerTest.php index ed0ff32be26..a87ed873296 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ConditionalFilterListenerTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ConditionalFilterListenerTest.php @@ -144,8 +144,8 @@ public function testConditionalFilterWithoutAuthorizationService() $event = $this->getMockPreEvent($params); $listener->onSearchPre($event); - $fq = $params->get('fq'); - $this->assertEquals([], $fq); + $filter = $params->get('filter'); + $this->assertEquals([], $filter); } /** @@ -159,7 +159,7 @@ public function testConditionalFilterWithoutAuthorizationServiceWithParams() { $params = new ParamBag( [ - 'fq' => ['fulltext:VuFind', 'field2:novalue'], + 'filter' => ['fulltext:VuFind', 'field2:novalue'], ] ); $listener = new InjectConditionalFilterListener($this->backend, self::$searchConfig); @@ -167,11 +167,11 @@ public function testConditionalFilterWithoutAuthorizationServiceWithParams() $event = $this->getMockPreEvent($params); $listener->onSearchPre($event); - $fq = $params->get('fq'); + $filter = $params->get('filter'); $this->assertEquals( [0 => 'fulltext:VuFind', 1 => 'field2:novalue'], - $fq + $filter ); } @@ -190,8 +190,8 @@ public function testConditionalFilterEmptyConfig() $event = $this->getMockPreEvent($params); $listener->onSearchPre($event); - $fq = $params->get('fq'); - $this->assertEquals([], $fq); + $filter = $params->get('filter'); + $this->assertEquals([], $filter); } /** @@ -204,7 +204,7 @@ public function testConditionalFilterEmptyConfigWithFQ() { $params = new ParamBag( [ - 'fq' => ['fulltext:VuFind', 'field2:novalue'], + 'filter' => ['fulltext:VuFind', 'field2:novalue'], ] ); $listener = new InjectConditionalFilterListener($this->backend, self::$emptySearchConfig); @@ -214,11 +214,11 @@ public function testConditionalFilterEmptyConfigWithFQ() $event = $this->getMockPreEvent($params); $listener->onSearchPre($event); - $fq = $params->get('fq'); + $filter = $params->get('filter'); $this->assertEquals( [0 => 'fulltext:VuFind', 1 => 'field2:novalue'], - $fq + $filter ); } @@ -241,17 +241,17 @@ public function testConditionalFilter() $event = $this->getMockPreEvent($params); $listener->onSearchPre($event); - $fq = $params->get('fq'); + $filter = $params->get('filter'); $this->assertEquals( [0 => 'institution:"MyInst"'], - $fq + $filter ); // Check that a filter is not added for wrong backend: $params = new ParamBag([]); $event = $this->getMockPreEvent($params, 'Other'); $listener->onSearchPre($event); - $this->assertEmpty($params->get('fq')); + $this->assertEmpty($params->get('filter')); } /** @@ -273,8 +273,8 @@ public function testNegativeConditionalFilter() $event = $this->getMockPreEvent($params); $listener->onSearchPre($event); - $fq = $params->get('fq'); - $this->assertEquals([0 => '(NOT institution:"MyInst")'], $fq); + $filter = $params->get('filter'); + $this->assertEquals([0 => '(NOT institution:"MyInst")'], $filter); } /** @@ -287,7 +287,7 @@ public function testNegativeConditionalFilterWithFQ() { $params = new ParamBag( [ - 'fq' => ['fulltext:VuFind', 'field2:novalue'], + 'filter' => ['fulltext:VuFind', 'field2:novalue'], ] ); @@ -300,13 +300,13 @@ public function testNegativeConditionalFilterWithFQ() $event = $this->getMockPreEvent($params); $listener->onSearchPre($event); - $fq = $params->get('fq'); + $filter = $params->get('filter'); $this->assertEquals( [0 => 'fulltext:VuFind', 1 => 'field2:novalue', 2 => '(NOT institution:"MyInst")', ], - $fq + $filter ); } @@ -320,7 +320,7 @@ public function testConditionalFilterWithFQ() { $params = new ParamBag( [ - 'fq' => ['fulltext:VuFind', 'field2:novalue'], + 'filter' => ['fulltext:VuFind', 'field2:novalue'], ] ); @@ -333,13 +333,13 @@ public function testConditionalFilterWithFQ() $event = $this->getMockPreEvent($params); $listener->onSearchPre($event); - $fq = $params->get('fq'); + $filter = $params->get('filter'); $this->assertEquals( [0 => 'fulltext:VuFind', 1 => 'field2:novalue', 2 => 'institution:"MyInst"', ], - $fq + $filter ); } } diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/CustomFilterListenerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/CustomFilterListenerTest.php index bcb8495949c..c62f6649dca 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/CustomFilterListenerTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/CustomFilterListenerTest.php @@ -104,13 +104,13 @@ public function testRemapping(): void 'vufind:"normal"' => 'field1:normal OR field2:alsoNormal', ]; $listener = $this->getListener($normal); - $params = new ParamBag(['fq' => ['foo:"bar"', 'vufind:"normal"']]); + $params = new ParamBag(['filter' => ['foo:"bar"', 'vufind:"normal"']]); $command = $this->getMockSearchCommand($params, 'search', 'Solr'); $event = new Event(null, null, compact('command')); $listener->onSearchPre($event); $this->assertEquals( ['foo:"bar"', 'field1:normal OR field2:alsoNormal'], - $params->get('fq') + $params->get('filter') ); } @@ -125,13 +125,13 @@ public function testMismatchedBackendIsIgnored(): void 'vufind:"normal"' => 'field1:normal OR field2:alsoNormal', ]; $listener = $this->getListener($normal); - $params = new ParamBag(['fq' => ['foo:"bar"', 'vufind:"normal"']]); + $params = new ParamBag(['filter' => ['foo:"bar"', 'vufind:"normal"']]); $command = $this->getMockSearchCommand($params, 'search', 'Search2'); $event = new Event(null, null, compact('command')); $listener->onSearchPre($event); $this->assertEquals( ['foo:"bar"', 'vufind:"normal"'], - $params->get('fq') + $params->get('filter') ); } @@ -146,13 +146,13 @@ public function testWrongContextIsIgnored(): void 'vufind:"normal"' => 'field1:normal OR field2:alsoNormal', ]; $listener = $this->getListener($normal); - $params = new ParamBag(['fq' => ['foo:"bar"', 'vufind:"normal"']]); + $params = new ParamBag(['filter' => ['foo:"bar"', 'vufind:"normal"']]); $command = $this->getMockSearchCommand($params, 'weird', 'Solr'); $event = new Event(null, null, compact('command')); $listener->onSearchPre($event); $this->assertEquals( ['foo:"bar"', 'vufind:"normal"'], - $params->get('fq') + $params->get('filter') ); } @@ -168,13 +168,13 @@ public function testMissingInvertedFilterAddsContent(): void 'vufind:"inverted"' => 'field3:invertedFilter', ]; $listener = $this->getListener([], $inverted); - $params = new ParamBag(['fq' => ['foo:"bar"']]); + $params = new ParamBag(['filter' => ['foo:"bar"']]); $command = $this->getMockSearchCommand($params, 'search', 'Solr'); $event = new Event(null, null, compact('command')); $listener->onSearchPre($event); $this->assertEquals( ['foo:"bar"', 'field3:invertedFilter'], - $params->get('fq') + $params->get('filter') ); } @@ -190,10 +190,10 @@ public function testInvertedFilterPreventsAdditionOfContent(): void 'vufind:"inverted"' => 'field3:invertedFilter', ]; $listener = $this->getListener([], $inverted); - $params = new ParamBag(['fq' => ['foo:"bar"', 'vufind:"inverted"']]); + $params = new ParamBag(['filter' => ['foo:"bar"', 'vufind:"inverted"']]); $command = $this->getMockSearchCommand($params, 'search', 'Solr'); $event = new Event(null, null, compact('command')); $listener->onSearchPre($event); - $this->assertEquals(['foo:"bar"'], $params->get('fq')); + $this->assertEquals(['foo:"bar"'], $params->get('filter')); } } diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/DefaultParametersListenerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/DefaultParametersListenerTest.php index 3d500d9e6f9..15429b76131 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/DefaultParametersListenerTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/DefaultParametersListenerTest.php @@ -141,10 +141,10 @@ public function testSearch( // Set up listener $listenerConfig = [ - 'search' => 'foo=1&foo=2', + 'search' => '{"foo": ["1", "2"]}', ]; if ($catchAllConfig) { - $listenerConfig['*'] = 'bar=3&bar'; + $listenerConfig['*'] = '{"bar": ["3"]}'; } $listener = new DefaultParametersListener($this->backends['primary'], $listenerConfig); @@ -164,6 +164,7 @@ public function testSearch( ); $listener->onSearchPre($event); + $params = $command->getSearchParameters(); $this->assertEquals($expectFoo, $params->get('foo')); $this->assertEquals($expectBar, $params->get('bar')); } diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/FilterFieldConversionListenerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/FilterFieldConversionListenerTest.php index 7c34f211350..3879181548c 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/FilterFieldConversionListenerTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/FilterFieldConversionListenerTest.php @@ -74,7 +74,7 @@ public function testFilterTranslation() { $params = new ParamBag( [ - 'fq' => [ + 'filter' => [ 'foo:value', 'baz:"foo:value"', 'foofoo:value', @@ -97,7 +97,7 @@ public function testFilterTranslation() ); $listener->onSearchPre($event); - $fq = $params->get('fq'); + $fq = $params->get('filter'); $expected = [ 'bar:value', 'boo:"foo:value"', diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/InjectHighlightingListenerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/InjectHighlightingListenerTest.php index dbf2615be6a..0b1e0c9be20 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/InjectHighlightingListenerTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/InjectHighlightingListenerTest.php @@ -32,7 +32,7 @@ use Laminas\EventManager\Event; use VuFind\Search\Solr\InjectHighlightingListener; use VuFindSearch\Backend\Solr\QueryBuilder; -use VuFindSearch\ParamBag; +use VuFindSearch\NestingParamBag; use VuFindSearch\Service; /** @@ -72,7 +72,7 @@ protected function setUp(): void { $this->backend = $this->createMock(\VuFindSearch\Backend\Solr\Backend::class); $this->backend->method('getIdentifier')->willReturn('foo'); - $this->listener = new InjectHighlightingListener($this->backend, 'bar,baz', ['xyzzy' => 'true']); + $this->listener = new InjectHighlightingListener($this->backend, 'bar,baz', ['params' => ['xyzzy' => 'true']]); } /** @@ -96,9 +96,11 @@ public function testAttach() */ public function testParameters() { - $params = new ParamBag( - [ - 'hl' => 'true', + $params = NestingParamBag::fromArray( + ['params' => + [ + 'hl' => 'true', + ], ] ); $command = $this->getMockSearchCommand( @@ -119,12 +121,14 @@ public function testParameters() $this->listener->onSearchPre($event); $this->assertEquals( [ - 'hl' => ['true'], - 'xyzzy' => ['true'], - 'hl.simple.pre' => ['{{{{START_HILITE}}}}'], - 'hl.simple.post' => ['{{{{END_HILITE}}}}'], + 'params' => [ + 'hl' => 'true', + 'xyzzy' => 'true', + 'hl.simple.pre' => '{{{{START_HILITE}}}}', + 'hl.simple.post' => '{{{{END_HILITE}}}}', + ], ], - $params->getArrayCopy() + $params->jsonSerialize() ); } } diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/MultiIndexListenerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/MultiIndexListenerTest.php index f2de8ed703c..26f5e185811 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/MultiIndexListenerTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/MultiIndexListenerTest.php @@ -34,7 +34,7 @@ use VuFindSearch\Backend\Solr\Backend; use VuFindSearch\Backend\Solr\Connector; use VuFindSearch\Backend\Solr\HandlerMap; -use VuFindSearch\ParamBag; +use VuFindSearch\NestingParamBag; use VuFindSearch\Service; /** @@ -146,10 +146,16 @@ function () { */ public function testStripFacetFields() { - $params = new ParamBag( + $params = NestingParamBag::fromArray( [ - 'facet.field' => ['field_1', 'field_2', 'field_3'], - 'shards' => [self::$shards['b'], self::$shards['c']], + 'facet' => [ + 'field_1' => ['field' => 'field_1'], + 'field_2' => ['field' => 'field_2'], + 'field_3' => ['field' => 'field_3'], + ], + 'params' => [ + 'shards' => [self::$shards['b'], self::$shards['c']], + ], ] ); $command = $this->getMockSearchCommand($params); @@ -160,9 +166,10 @@ public function testStripFacetFields() ); $this->listener->onSearchPre($event); - $facets = $params->get('facet.field'); - sort($facets); - $this->assertEquals(['field_1', 'field_2'], $facets); + $facets = $params->jsonSerialize()['facet']; + $facetFields = array_map(fn ($facet) => $facet['field'], $facets); + sort($facetFields); + $this->assertSame(['field_1', 'field_2'], $facetFields); } /** @@ -172,10 +179,12 @@ public function testStripFacetFields() */ public function testAllShardsUsedForRecordRetrieval() { - $params = new ParamBag( - [ - 'shards' => [self::$shards['b'], self::$shards['c']], - ] + $params = NestingParamBag::fromArray( + ['params' => + [ + 'shards' => [self::$shards['b'], self::$shards['c']], + ], + ], ); $command = $this->getMockSearchCommand($params, 'retrieve'); $event = new Event( @@ -185,8 +194,8 @@ public function testAllShardsUsedForRecordRetrieval() ); $this->listener->onSearchPre($event); - $shards = $params->get('shards'); - $this->assertEquals( + $shards = $params->getNested('params', 'shards'); + $this->assertSame( [implode(',', [self::$shards['a'], self::$shards['b'], self::$shards['c']])], $shards ); diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ParamsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ParamsTest.php index 50f35d120a9..5823242abe3 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ParamsTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ParamsTest.php @@ -59,7 +59,7 @@ public function testFilters(): void $params->addFacet('building', 'building_label'); // No filters: - $this->assertEquals(null, $params->getBackendParameters()->get('fq')); + $this->assertEquals(null, $params->getBackendParameters()->get('filter')); // Add multiple filters: $params->addFilter('~format:bar'); @@ -70,7 +70,7 @@ public function testFilters(): void 'building:"main"', '{!tag=format_filter}format:(format:"bar" OR format:"baz")', ], - $params->getBackendParameters()->get('fq') + $params->getBackendParameters()->get('filter') ); // Add a hidden filter: @@ -81,7 +81,7 @@ public function testFilters(): void 'building:"main"', '{!tag=format_filter}format:(format:"bar" OR format:"baz")', ], - $params->getBackendParameters()->get('fq') + $params->getBackendParameters()->get('filter') ); // Remove format filters: @@ -91,7 +91,7 @@ public function testFilters(): void 'building:"sub"', 'building:"main"', ], - $params->getBackendParameters()->get('fq') + $params->getBackendParameters()->get('filter') ); // Remove building filter: @@ -100,7 +100,7 @@ public function testFilters(): void [ 'building:"sub"', ], - $params->getBackendParameters()->get('fq') + $params->getBackendParameters()->get('filter') ); } diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ResultsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ResultsTest.php index 601234713f6..c019956ea17 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ResultsTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ResultsTest.php @@ -86,16 +86,18 @@ class ResultsTest extends \PHPUnit\Framework\TestCase 'response' => [ 'numFound' => 5, ], - 'facet_counts' => [ - 'facet_fields' => [ - 'topic_facet' => [ - ['Research', 16], - ['Psychotherapy', 8], + 'facets' => [ + 'topic_facet' => [ + 'buckets' => [ + ['val' => 'Research', 'count' => 16], + ['val' => 'Psychotherapy', 'count' => 8], ], - 'building' => [ - ['0/Main/', 11], - ['1/Main/Fiction/', 5], - ['0/Sub/', 2], + ], + 'building' => [ + 'buckets' => [ + ['val' => '0/Main/', 'count' => 11], + ['val' => '1/Main/Fiction/', 'count' => 5], + ['val' => '0/Sub/', 'count' => 2], ], ], ], @@ -143,22 +145,27 @@ public function testFacetTranslations(): void $searchService = $this->getSearchServiceWithMockSearchMethod( [ 'response' => ['numFound' => 5], - 'facet_counts' => [ - 'facet_fields' => [ - 'dewey-raw' => [ - ['000', 100], + 'facets' => [ + 'dewey-raw' => [ + 'buckets' => [ + ['val' => '000', 'count' => 100], ], ], ], ], [ - 'spellcheck' => ['true'], - 'hl' => ['false'], - 'facet' => ['true'], - 'facet.limit' => [30], - 'facet.field' => ['dewey-raw'], - 'facet.sort' => ['count'], - 'facet.mincount' => [1], + 'facet' => [ + 'dewey-raw' => [ + 'type' => 'terms', + 'field' => 'dewey-raw', + 'limit' => 30, + 'sort' => 'count', + ], + ], + 'params' => [ + 'spellcheck' => 'true', + 'hl' => 'false', + ], ] ); $results = $this->getResults($params, $searchService); @@ -198,10 +205,10 @@ public function testGetResultTotal(): void { $searchService = $this->getSearchServiceWithMockSearchMethod( ['response' => ['numFound' => 5]], - [ - 'spellcheck' => ['true'], - 'hl' => ['false'], - ] + [ 'params' => [ + 'spellcheck' => 'true', + 'hl' => 'false', + ]] ); $results = $this->getResults(null, $searchService); $this->assertEquals(5, $results->getResultTotal()); @@ -232,7 +239,7 @@ protected function getSearchServiceWithMockSearchMethod( && get_class($command->getArguments()[0]) === \VuFindSearch\Query\Query::class && $command->getArguments()[1] === 0 && $command->getArguments()[2] === 20 - && $command->getArguments()[3]->getArrayCopy() == $expectedParams; + && $command->getArguments()[3]->jsonSerialize() == $expectedParams; }; $searchService->expects($this->once())->method('invoke') ->with($this->callback($checkCommand)) diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Blender/Response/Json/RecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Backend/Blender/Response/Json/RecordCollection.php index f21219b53de..86572070172 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/Blender/Response/Json/RecordCollection.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Blender/Response/Json/RecordCollection.php @@ -142,7 +142,7 @@ public function initBlended( } } - $this->response['facet_counts']['facet_fields'] + $this->response['facets'] = $this->getMergedFacets($collections); return $backendRecords; @@ -341,14 +341,17 @@ function ($a, $b) { $mergedFacets['blender_backend'] = $this->getBlenderFacetStats($collections); - // Convert the array back to Solr-style array with two elements + // Convert the array back to Solr-style array from JSON Facet API $facetFields = []; foreach ($mergedFacets as $facet => $values) { - $list = []; - foreach ($values as $key => $value) { - $list[] = [$key, $value]; + $buckets = []; + foreach ($values as $key => $count) { + $buckets[] = [ + 'val' => $key, + 'count' => $count, + ]; } - $facetFields[$facet] = $list; + $facetFields[$facet] = ['buckets' => $buckets]; } return $facetFields; diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php index 6658f99708a..ea92d496c7c 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php @@ -40,6 +40,7 @@ use VuFindSearch\Feature\RandomInterface; use VuFindSearch\Feature\RetrieveBatchInterface; use VuFindSearch\Feature\SimilarInterface; +use VuFindSearch\NestingParamBag; use VuFindSearch\ParamBag; use VuFindSearch\Query\AbstractQuery; use VuFindSearch\Query\WorkKeysQuery; @@ -135,6 +136,7 @@ public function search( $limit, ?ParamBag $params = null ) { + $params = NestingParamBag::from($params, false); if ($query instanceof WorkKeysQuery) { return $this->workKeysSearch($query, $offset, $limit, $params); } @@ -161,11 +163,11 @@ public function rawJsonSearch( $limit, ?ParamBag $params = null ) { - $params = $params ?: new ParamBag(); + $params = NestingParamBag::from($params); $this->injectResponseWriter($params); - $params->set('rows', $limit); - $params->set('start', $offset); + $params->set('limit', $limit); + $params->set('offset', $offset); $params->mergeWith($this->getQueryBuilder()->build($query, $params)); return $this->connector->search($params); } @@ -208,17 +210,17 @@ public function getIds( $limit, ?ParamBag $params = null ) { - $params = $params ?: new ParamBag(); + $params = NestingParamBag::from($params); $this->injectResponseWriter($params); - $params->set('rows', $limit); - $params->set('start', $offset); - $flParts = [$this->getConnector()->getUniqueKey()]; - if ($fl = $params->get('fl')) { + $params->set('limit', $limit); + $params->set('offset', $offset); + $fieldParts = [$this->getConnector()->getUniqueKey()]; + if ($fields = $params->get('fields')) { // Merge multiple values if necessary, then split on delimiter: - $flParts = array_unique(array_merge($flParts, explode(',', implode(',', $fl)))); + $fieldParts = array_unique(array_merge($fieldParts, explode(',', implode(',', $fields)))); } - $params->set('fl', implode(',', $flParts)); + $params->set('fields', implode(',', $fieldParts)); $params->mergeWith($this->getQueryBuilder()->build($query)); $response = $this->connector->search($params); $collection = $this->createRecordCollection($response); @@ -241,7 +243,7 @@ public function random( $limit, ?ParamBag $params = null ) { - $params = $params ?: new ParamBag(); + $params = NestingParamBag::from($params); $this->injectResponseWriter($params); $random = rand(0, 1000000); @@ -261,7 +263,7 @@ public function random( */ public function retrieve($id, ?ParamBag $params = null) { - $params = $params ?: new ParamBag(); + $params = NestingParamBag::from($params); $this->injectResponseWriter($params); $response = $this->connector->retrieve($id, $params); @@ -280,7 +282,7 @@ public function retrieve($id, ?ParamBag $params = null) */ public function retrieveBatch($ids, ?ParamBag $params = null) { - $params = $params ?: new ParamBag(); + $params = NestingParamBag::from($params); // Callback function for formatting IDs: $formatIds = function ($i) { @@ -292,9 +294,9 @@ public function retrieveBatch($ids, ?ParamBag $params = null) while (count($ids) > 0) { $currentPage = array_splice($ids, 0, $this->pageSize, []); $currentPage = array_map($formatIds, $currentPage); - $params->set('q', 'id:(' . implode(' OR ', $currentPage) . ')'); - $params->set('start', 0); - $params->set('rows', $this->pageSize); + $params->set('query', 'id:(' . implode(' OR ', $currentPage) . ')'); + $params->set('offset', 0); + $params->set('limit', $this->pageSize); $this->injectResponseWriter($params); $next = $this->createRecordCollection( $this->connector->search($params) @@ -321,10 +323,10 @@ public function retrieveBatch($ids, ?ParamBag $params = null) */ public function similar($id, ?ParamBag $params = null) { - $params = $params ?: new ParamBag(); + $params = NestingParamBag::from($params); $this->injectResponseWriter($params); - $params->mergeWith($this->getSimilarBuilder()->build($id)); + $params = $this->getSimilarBuilder()->build($id, $params); $response = $this->connector->similar($id, $params); $collection = $this->createRecordCollection($response); $this->injectSourceIdentifier($collection); @@ -354,29 +356,29 @@ public function terms( } // Create empty ParamBag if none provided: - $params = $params ?: new ParamBag(); + $params = NestingParamBag::from($params); $this->injectResponseWriter($params); // Always enable terms: - $params->set('terms', 'true'); + $params->setNested('params', 'terms', 'true'); // Use parameters if provided: if (null !== $field) { - $params->set('terms.fl', $field); + $params->setNested('params', 'terms.fl', $field); } if (null !== $start) { - $params->set('terms.lower', $start); + $params->setNested('params', 'terms.lower', $start); } if (null !== $limit) { - $params->set('terms.limit', $limit); + $params->setNested('params', 'terms.limit', $limit); } // Set defaults unless overridden: - if (!$params->hasParam('terms.lower.incl')) { - $params->set('terms.lower.incl', 'false'); + if (!$params->hasNestedParam('params', 'terms.lower.incl')) { + $params->setNested('params', 'terms.lower.incl', 'false'); } - if (!$params->hasParam('terms.sort')) { - $params->set('terms.sort', 'index'); + if (!$params->hasNestedParam('params', 'terms.sort')) { + $params->setNested('params', 'terms.sort', 'index'); } $response = $this->connector->terms($params); @@ -405,7 +407,8 @@ public function alphabeticBrowse( $params = null, $offsetDelta = 0 ) { - $params = $params ?: new ParamBag(); + // TODO Does alphabrowse also need to be converted? Custom request handler... + $params = NestingParamBag::from($params); $this->injectResponseWriter($params); $params->set('from', $from); @@ -607,26 +610,26 @@ protected function refineBrowseException(RemoteErrorException $e) * @throws InvalidArgumentException Response writer and named list * implementation already set to an incompatible type. */ - protected function injectResponseWriter(ParamBag $params) + protected function injectResponseWriter(NestingParamBag $params) { - if (array_diff($params->get('wt') ?: [], ['json'])) { + if (array_diff($params->getNested('params', 'wt') ?: [], ['json'])) { throw new InvalidArgumentException( sprintf( 'Invalid response writer type: %s', - implode(', ', $params->get('wt')) + implode(', ', $params->getNested('params', 'wt')) ) ); } - if (array_diff($params->get('json.nl') ?: [], ['arrarr'])) { + if (array_diff($params->getNested('params', 'json.nl') ?: [], ['arrarr'])) { throw new InvalidArgumentException( sprintf( 'Invalid named list implementation type: %s', - implode(', ', $params->get('json.nl')) + implode(', ', $params->getNested('params', 'json.nl')) ) ); } - $params->set('wt', ['json']); - $params->set('json.nl', ['arrarr']); + $params->setNested('params', 'wt', 'json'); + $params->setNested('params', 'json.nl', 'arrarr'); } /** @@ -660,12 +663,12 @@ protected function workKeysSearch( $params = $defaultParams ? clone $defaultParams : new \VuFindSearch\ParamBag(); $this->injectResponseWriter($params); - $params->set('q', "{!terms f=work_keys_str_mv separator=\"\u{001f}\"}" . implode("\u{001f}", $workKeys)); + $params->set('query', "{!terms f=work_keys_str_mv separator=\"\u{001f}\"}" . implode("\u{001f}", $workKeys)); if (!$query->getIncludeSelf()) { - $params->add('fq', sprintf('-id:"%s"', addcslashes($id, '"'))); + $params->add('filter', sprintf('-id:"%s"', addcslashes($id, '"'))); } - $params->set('rows', $limit); - $params->set('start', $offset); + $params->set('limit', $limit); + $params->set('offset', $offset); if (!$params->hasParam('sort')) { $params->add('sort', 'publishDateSort desc, title_sort asc'); } diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php index a8cc5a9e947..3dc0416f5d4 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php @@ -41,6 +41,7 @@ use VuFindSearch\Backend\Exception\RequestErrorException; use VuFindSearch\Backend\Solr\Document\DocumentInterface; use VuFindSearch\Exception\InvalidArgumentException; +use VuFindSearch\NestingParamBag; use VuFindSearch\ParamBag; use function call_user_func_array; @@ -199,7 +200,7 @@ public function retrieve($id, ?ParamBag $params = null) { $params = $params ?: new ParamBag(); $params - ->set('q', sprintf('%s:"%s"', $this->uniqueKey, addcslashes($id, '"'))); + ->set('query', sprintf('%s:"%s"', $this->uniqueKey, addcslashes($id, '"'))); $handler = $this->map->getHandler(__FUNCTION__); $this->map->prepare(__FUNCTION__, $params); @@ -307,23 +308,23 @@ public function write( */ public function query($handler, ParamBag $params, bool $cacheable = false) { + $params = NestingParamBag::from($params); $urlSuffix = '/' . $handler; - $paramString = implode('&', $params->request()); - if (strlen($paramString) > self::MAX_GET_URL_LENGTH) { - $method = Request::METHOD_POST; - $callback = function ($client) use ($paramString): void { - $client->setRawBody($paramString); - $client->setEncType(HttpClient::ENC_URLENCODED); - $client->setHeaders(['Content-Length' => strlen($paramString)]); - }; - } else { - $method = Request::METHOD_GET; - $urlSuffix .= '?' . $paramString; - $callback = null; - } + $body = json_encode($params, JSON_THROW_ON_ERROR); + $method = Request::METHOD_POST; + $callback = function ($client) use ($body): void { + $client->setRawBody($body); + $client->setEncType('application/json'); + $client->setHeaders(['Content-Length' => strlen($body)]); + }; - $this->debug(sprintf('Query %s', $paramString)); + $this->debug(sprintf('Query body %s', $body)); return $this->trySolrUrls($method, $urlSuffix, $callback, $cacheable); + // $responseBody = $this->trySolrUrls($method, $urlSuffix, $callback, $cacheable); + // if (strlen($body) > 500) { + // $this->debug(sprintf('Response body to long query %s', $responseBody)); + // } + // return $responseBody; } /** diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php index 38eb0732141..b2ee6c0013d 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php @@ -31,6 +31,7 @@ namespace VuFindSearch\Backend\Solr; +use VuFindSearch\NestingParamBag; use VuFindSearch\ParamBag; use VuFindSearch\Query\AbstractQuery; use VuFindSearch\Query\Query; @@ -129,16 +130,17 @@ public function __construct( * @param AbstractQuery $query User query * @param ?ParamBag $params Search backend parameters * - * @return ParamBag + * @return NestingParamBag */ - public function build(AbstractQuery $query, ?ParamBag $params = null) + public function build(AbstractQuery $query, ?ParamBag $params = null): NestingParamBag { - $newParams = new ParamBag(); + $newParams = new NestingParamBag(); // Add spelling query if applicable -- note that we must set this up before // we process the main query in order to avoid unwanted extra syntax: if ($this->createSpellingQuery) { - $newParams->set( + $newParams->setNested( + 'params', 'spellcheck.q', $this->getLuceneHelper()->extractSearchTerms($query->getAllTerms()) ); @@ -169,17 +171,17 @@ public function build(AbstractQuery $query, ?ParamBag $params = null) // If a boost was added, we don't want to highlight based on // the boost query, so we should use the non-boosted version: if ($highlight && $oldString != $string) { - $newParams->set('hl.q', $oldString); + $newParams->setNested('params', 'hl.q', $oldString); } } } elseif ($handler->hasDismax()) { - $newParams->set('qf', implode(' ', $handler->getDismaxFields())); - $newParams->set('qt', $handler->getDismaxHandler()); + $newParams->setNested('params', 'qf', implode(' ', $handler->getDismaxFields())); + $newParams->setNested('params', 'qt', $handler->getDismaxHandler()); foreach ($handler->getDismaxParams() as $param) { - $newParams->add(reset($param), next($param)); + $newParams->addNested('params', reset($param), next($param)); } if ($handler->hasFilterQuery()) { - $newParams->add('fq', $handler->getFilterQuery()); + $newParams->add('filter', $handler->getFilterQuery()); } } else { $string = $handler->createSimpleQueryString($string); @@ -188,9 +190,9 @@ public function build(AbstractQuery $query, ?ParamBag $params = null) // Set an appropriate highlight field list when applicable: if ($highlight) { $filter = $handler ? $handler->getAllFields() : []; - $newParams->add('hl.fl', $this->getFieldsToHighlight($filter)); + $newParams->addNested('params', 'hl.fl', $this->getFieldsToHighlight($filter)); } - $newParams->set('q', $string); + $newParams->set('query', $string); // Handle any extra parameters: foreach ($this->globalExtraParams as $extraParam) { @@ -203,7 +205,7 @@ public function build(AbstractQuery $query, ?ParamBag $params = null) continue; } foreach ((array)$extraParam['value'] as $value) { - $newParams->add($extraParam['param'], $value); + $newParams->addNested('params', $extraParam['param'], $value); } } diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php index 3829a56f6ab..8a4f7faed40 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php @@ -32,6 +32,7 @@ use VuFindSearch\Response\AbstractRecordCollection; use function array_key_exists; +use function is_array; /** * Simple JSON-based record collection. @@ -55,7 +56,7 @@ class RecordCollection extends AbstractRecordCollection 'responseHeader' => [], 'response' => ['numFound' => 0, 'start' => 0], 'spellcheck' => ['suggestions' => []], - 'facet_counts' => [], + 'facets' => [], ]; /** @@ -144,22 +145,25 @@ public function getFacets() { if (null === $this->facetFields) { $this->facetFields = []; - $facetFieldData = $this->response['facet_counts']['facet_fields'] ?? []; + $facetFieldData = $this->response['facets'] ?? []; foreach ($facetFieldData as $field => $facetData) { - $values = []; - foreach ($facetData as $value) { - $values[$value[0]] = $value[1]; + if (is_array($facetData)) { + $values = []; + foreach ($facetData['buckets'] as $bucket) { + $values[$bucket['val']] = $bucket['count']; + } + $this->facetFields[$field] = $values; } - $this->facetFields[$field] = $values; - } - $facetRangeData = $this->response['facet_counts']['facet_ranges'] ?? []; - foreach ($facetRangeData as $field => $facetData) { - $values = []; - foreach ($facetData['counts'] as $value) { - $values[$value[0]] = $value[1]; - } - $this->facetFields[$field] = $values; } + // TODO Fix this + // $facetRangeData = $this->response['facet_counts']['facet_ranges'] ?? []; + // foreach ($facetRangeData as $field => $facetData) { + // $values = []; + // foreach ($facetData['counts'] as $value) { + // $values[$value[0]] = $value[1]; + // } + // $this->facetFields[$field] = $values; + // } } return $this->facetFields; } diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SimilarBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SimilarBuilder.php index 851fcb6d81e..47d6b6d6904 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SimilarBuilder.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SimilarBuilder.php @@ -33,6 +33,7 @@ namespace VuFindSearch\Backend\Solr; +use VuFindSearch\NestingParamBag; use VuFindSearch\ParamBag; use function sprintf; @@ -113,28 +114,29 @@ public function __construct( /** * Return SOLR search parameters based on a record Id and params. * - * @param string $id Record Id + * @param string $id Record Id + * @param ?ParamBag $params Existing params * * @return ParamBag */ - public function build($id) + public function build(string $id, ?ParamBag $params = null): ParamBag { - $params = new ParamBag(); + $params = NestingParamBag::from($params); if ($this->useHandler) { $mltParams = $this->handlerParams ? $this->handlerParams : 'qf=title,title_short,callnumber-label,topic,language,author,' . 'publishDate mintf=1 mindf=1'; - $params->set('q', sprintf('{!mlt %s}%s', $mltParams, $id)); + $params->set('query', sprintf('{!mlt %s}%s', $mltParams, $id)); } else { $params->set( - 'q', + 'query', sprintf('%s:"%s"', $this->uniqueKey, addcslashes($id, '"')) ); - $params->set('qt', 'morelikethis'); + $params->setNested('params', 'qt', 'morelikethis'); } - if (null === $params->get('rows')) { - $params->set('rows', $this->count); + if (null === $params->get('limit')) { + $params->set('limit', $this->count); } return $params; } diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SimilarBuilderInterface.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SimilarBuilderInterface.php index 183015b8f40..014fb3d66d9 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SimilarBuilderInterface.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SimilarBuilderInterface.php @@ -52,9 +52,10 @@ interface SimilarBuilderInterface /** * Build SOLR query based on VuFind query object. * - * @param string $id Record id + * @param string $id Record id + * @param ?ParamBag $params Existing params * * @return ParamBag */ - public function build($id); + public function build(string $id, ?ParamBag $params): ParamBag; } diff --git a/module/VuFindSearch/src/VuFindSearch/NestingParamBag.php b/module/VuFindSearch/src/VuFindSearch/NestingParamBag.php new file mode 100644 index 00000000000..0e417f9bf39 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/NestingParamBag.php @@ -0,0 +1,320 @@ +. + * + * @category VuFind + * @package Search + * @author Maccabee Levine + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org + */ + +namespace VuFindSearch; + +use JsonSerializable; + +use function count; +use function is_array; + +/** + * Bag of (string) parameters or nested ParamBags. + * + * @category VuFind + * @package Search + * @author Maccabee Levine + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org + */ +class NestingParamBag extends ParamBag implements JsonSerializable +{ + /** + * Transform any ParamBag into a NestingParamBag. + * + * @param ?ParamBag $original The original ParamBag + * @param bool $createIfNull Create an empty ParamBag if $original is null + * + * @return ?NestingParamBag + */ + public static function from(?ParamBag $original, bool $createIfNull = true): ?NestingParamBag + { + if (!$original) { + return $createIfNull ? new NestingParamBag() : null; + } + if ($original instanceof NestingParamBag) { + return $original; + } + $bag = new NestingParamBag(); + $bag->mergeWith($original); + return $bag; + } + + /** + * Transform a potentially nested array of values into a NestingParamBag. + * + * @param array $values Source values + * + * @return NestingParamBag + */ + public static function fromArray(array $values): NestingParamBag + { + $bag = new NestingParamBag(); + foreach ($values as $name => $value) { + $bag->addMultiNested($name, $value); + } + return $bag; + } + + /** + * Return nested parameter value. + * + * @param string $name Parameter name + * @param string $nestedName Nested parameter name + * + * @return ?array Array of parameter values or NULL if not set + */ + public function getNested(string $name, string $nestedName): ?array + { + $nestedBag = $this->get($name); + if (!$nestedBag) { + return null; + } + return $nestedBag[0]->get($nestedName); + } + + /** + * Return true if the bag contains any value(s) for the specified parameters. + * + * @param string $name Parameter name + * @param string $nestedName Nested parameter name + * + * @return bool + */ + public function hasNestedParam(string $name, string $nestedName): bool + { + $nestedBag = $this->get($name); + if (!$nestedBag) { + return false; + } + return $nestedBag[0]->hasParam($nestedName); + } + + /** + * Set a nested parameter. + * + * @param string $name Parameter name + * @param string $nestedName Nested parameter name + * @param string $nestedValue Nested parameter value + * + * @return void + */ + public function setNested(string $name, string $nestedName, string $nestedValue): void + { + $nestedBag = $this->items[$name] ?? null; + if (!$nestedBag) { + $nestedBag = [new ParamBag()]; + $this->set($name, $nestedBag); + } + $nestedBag[0]->set($nestedName, $nestedValue); + } + + /** + * Add a nested parameter value. + * + * @param string $name Parameter name + * @param string $nestedName Nested parameter name + * @param string $nestedValue Nested parameter value + * + * @return void + */ + public function addNested(string $name, string $nestedName, string $nestedValue): void + { + $nestedBag = $this->items[$name] ?? null; + if (!$nestedBag) { + $nestedBag = [new ParamBag()]; + $this->set($name, $nestedBag); + } + $nestedBag[0]->add($nestedName, $nestedValue); + } + + /** + * Parse n-deep arrays to add values. + * + * @param string $name Parameter name + * @param mixed $value A scalar value, or some n-deep array of arrays into parameters + * + * @return void + */ + public function addMultiNested(string $name, mixed $value): void + { + if (is_array($value)) { + if (array_is_list($value)) { + foreach ($value as $valueItem) { + $this->add($name, $valueItem); + } + } else { + $nestedBag = $this->items[$name][0] ?? null; + if (!$nestedBag || !$nestedBag instanceof NestingParamBag) { + $nestedBag = NestingParamBag::from($nestedBag); + $this->set($name, $nestedBag); + } + foreach ($value as $nestedName => $nestedValue) { + $nestedBag->addMultiNested($nestedName, $nestedValue); + } + } + } else { + $this->add($name, $value); + } + } + + /** + * Add parameter value. + * + * @param string $name Parameter name + * @param mixed $value Parameter value + * @param bool $deduplicate Deduplicate parameter values + * + * @return void + */ + public function add($name, $value, $deduplicate = true): void + { + $existingValues = $this->items[$name] ?? []; + $this->validateCompatibleValues($existingValues, $value, $name); + + if ($value && is_array($value)) { + // Merge as needed so there is only one ParamBag for any $name + if ($value[0] instanceof ParamBag) { + $existingValues[0]->mergeWith($value[0]); + return; + } + } + + parent::add($name, $value, $deduplicate); + } + + /** + * Validate that the new values can be added to the existing values. + * + * @param array $existingValues The existing values + * @param mixed $newValues Values being added + * @param ?string $name Name associated with these values + * + * @throws \Exception If the combination of values is not valid. + * + * @return void + */ + protected function validateCompatibleValues(array $existingValues, mixed $newValues, ?string $name) + { + if (!is_array($newValues)) { + $newValues = [$newValues]; + } + foreach ([$existingValues, $newValues] as $values) { + if (!$values) { + return; + } + $this->validateValues($values, $name); + } + // Either both arrays, or neither, should contain a ParamBag + if ( + empty(array_filter($existingValues, fn ($value) => $value instanceof ParamBag)) != + empty(array_filter($newValues, fn ($value) => $value instanceof ParamBag)) + ) { + throw new \Exception('New values for name ' . ($name ?? '(unknown)') + . ' are not compatible with existing values; both or neither must be a ParamBag.'); + } + } + + /** + * Return a serializable object, for json_encode use into a POST body. + * + * @return mixed + */ + public function jsonSerialize(): mixed + { + $serializable = $this->jsonSerializeItems($this->items); + return $serializable; + } + + /** + * Parse ParamBag items into an array, recursively. + * + * @param array $items The array from a ParamBag + * + * @return array + */ + protected function jsonSerializeItems($items): array + { + $jsonObject = []; + foreach ($items as $name => $values) { + if (is_array($values)) { + $this->validateValues($values, $name); + + if (count($values) > 1) { + $jsonObject[$name] = $values; + } elseif (count($values) == 1) { + $value = $values[0]; + if ($value instanceof ParamBag) { + $nestedValues = $value->getArrayCopy(); + $jsonObject[$name] = $this->jsonSerializeItems($nestedValues); + } else { + $jsonObject[$name] = $value; + } + } + } else { + throw new \Exception('ParamBag values for ' . $name . ' is not an array.'); + } + } + return $jsonObject; + } + + /** + * Return array of params ready to be used in a HTTP request. + * + * Returns a numerical array with all request parameters as properly URL + * encoded key-value pairs. + * + * @return array + */ + public function request() + { + throw new \Exception('Simple query parameters are not supported by NestingParamBag'); + } + + /** + * Validate the values in a ParamBag. + * + * @param array $values The values + * @param ?string $name Name associated with these values + * + * @throws \Exception If the combination of values is not valid. + * + * @return void + */ + protected function validateValues(array $values, ?string $name): void + { + if ( + count($values) > 1 && + array_filter($values, fn ($value) => $value instanceof ParamBag) + ) { + throw new \Exception('More than one value for name ' . ($name ?? '(unknown)') + . 'including at least one ParamBag.'); + } + } +} diff --git a/module/VuFindSearch/src/VuFindSearch/ParamBag.php b/module/VuFindSearch/src/VuFindSearch/ParamBag.php index e5e480857b9..c7603ac536f 100644 --- a/module/VuFindSearch/src/VuFindSearch/ParamBag.php +++ b/module/VuFindSearch/src/VuFindSearch/ParamBag.php @@ -54,7 +54,7 @@ class ParamBag implements \Countable * * @var array */ - protected $params = []; + protected $items = []; /** * Constructor. @@ -79,7 +79,7 @@ public function __construct(array $initial = []) */ public function get($name) { - return $this->params[$name] ?? null; + return $this->items[$name] ?? null; } /** @@ -89,7 +89,7 @@ public function get($name) */ public function count(): int { - return count($this->params); + return count($this->items); } /** @@ -101,7 +101,7 @@ public function count(): int */ public function hasParam($name) { - return isset($this->params[$name]); + return isset($this->items[$name]); } /** @@ -128,7 +128,7 @@ public function contains($name, $value) */ public function set($name, $value) { - $this->params[$name] = is_array($value) ? $value : [$value]; + $this->items[$name] = is_array($value) ? $value : [$value]; } /** @@ -140,8 +140,8 @@ public function set($name, $value) */ public function remove($name) { - if (isset($this->params[$name])) { - unset($this->params[$name]); + if (isset($this->items[$name])) { + unset($this->items[$name]); } } @@ -156,22 +156,22 @@ public function remove($name) */ public function add($name, $value, $deduplicate = true) { - if (!isset($this->params[$name])) { - $this->params[$name] = []; + if (!isset($this->items[$name])) { + $this->items[$name] = []; } if (is_array($value)) { - $this->params[$name] = array_merge_recursive($this->params[$name], $value); + $this->items[$name] = array_merge_recursive($this->items[$name], $value); } else { - $this->params[$name][] = $value; + $this->items[$name][] = $value; } if ($deduplicate) { // Avoid deduplicating associative array params (like Primo filterList): - foreach ($this->params[$name] as $key => $current) { + foreach ($this->items[$name] as $key => $current) { if (!is_numeric($key) || is_array($current)) { return; } } - $this->params[$name] = array_values(array_unique($this->params[$name])); + $this->items[$name] = array_values(array_unique($this->items[$name])); } } @@ -184,7 +184,7 @@ public function add($name, $value, $deduplicate = true) */ public function mergeWith(ParamBag $bag) { - foreach ($bag->params as $key => $value) { + foreach ($bag->items as $key => $value) { if (!empty($value)) { $this->add($key, $value); } @@ -212,7 +212,7 @@ public function mergeWithAll(array $bags) */ public function getArrayCopy() { - return $this->params; + return $this->items; } /** @@ -224,8 +224,8 @@ public function getArrayCopy() */ public function exchangeArray(array $input) { - $current = $this->params; - $this->params = []; + $current = $this->items; + $this->items = []; foreach ($input as $key => $value) { $this->set($key, $value); } @@ -243,7 +243,7 @@ public function exchangeArray(array $input) public function request() { $request = []; - foreach ($this->params as $name => $values) { + foreach ($this->items as $name => $values) { if (!empty($values)) { $request = array_merge( $request, diff --git a/module/VuFindSearch/tests/fixtures/blender/response/solr/search-author.json b/module/VuFindSearch/tests/fixtures/blender/response/solr/search-author.json index a3d4705a94b..503ee20791c 100644 --- a/module/VuFindSearch/tests/fixtures/blender/response/solr/search-author.json +++ b/module/VuFindSearch/tests/fixtures/blender/response/solr/search-author.json @@ -181,62 +181,69 @@ "work_keys_str_mv":["AT willbbrokeafterwordbypssherman ofmoneyandslashes"], "_version_":1726988259027845120}] }, - "facet_counts":{ - "facet_queries":{}, - "facet_fields":{ - "institution": - [ - ["MyInstitution",240]], - "building": - [ - ["0/Main/", 231], - ["1/Main/Sub/", 7], - ["0/Sub/",1], - ["1/Sub/Foo/",1]], - "format": - [ - ["Book Chapter",177], - ["Book",63]], - "author_facet": - [ - ["Granda, Isiah",5], - ["Hartfield, Shelba",5], - ["Oram, Donn",5], - ["Overall, Lula",5], - ["Winrow, Sanjuana",5], - ["Abner, Shelli",4], - ["Crafts, Claudette",4], - ["Erickson, Joette",4], - ["Harrill, Mariko",4], - ["Leyva, Marie",4], - ["Mackson, Yolanda",4], - ["Oros, Cierra",4], - ["Pakele, Marina",4], - ["Saar, Danna",4], - ["Walcott, Porfirio",4], - ["Murden, Claud",3], - ["Nelms, Lanelle",3], - ["Alcina, Rashad",2], - ["Arechiga, Moon",2], - ["Beckett, Tamala",2], - ["Behringer, Anitra",2], - ["Beveridge, Albina",2], - ["Blacker, Minerva",2], - ["Bogard, Noma",2], - ["Bornstein, Dia",2], - ["Boyland, Bebe",2], - ["Bucher, Margit",2], - ["Cantrell, Cornelius",2], - ["Cornforth, Desiree",2], - ["Counter, Modesto",2]], - "language": - [ - ["English",7], - ["Italian",2], - ["Latin",2]]}, - "facet_ranges":{}, - "facet_intervals":{}, - "facet_heatmaps":{}}, + "facets": { + "count": 240, + "institution": { + "buckets": [ + { "val": "MyInstitution", "count": 240 } + ] + }, + "building": { + "buckets": [ + { "val": "0/Main/", "count": 231 }, + { "val": "1/Main/Sub/", "count": 7 }, + { "val": "0/Sub/", "count": 1 }, + { "val": "1/Sub/Foo/", "count": 1 } + ] + }, + "format": { + "buckets": [ + { "val": "Book Chapter", "count": 177 }, + { "val": "Book", "count": 63 } + ] + }, + "author_facet": { + "buckets": [ + { "val": "Granda, Isiah", "count": 5 }, + { "val": "Hartfield, Shelba", "count": 5 }, + { "val": "Oram, Donn", "count": 5 }, + { "val": "Overall, Lula", "count": 5 }, + { "val": "Winrow, Sanjuana", "count": 5 }, + { "val": "Abner, Shelli", "count": 4 }, + { "val": "Crafts, Claudette", "count": 4 }, + { "val": "Erickson, Joette", "count": 4 }, + { "val": "Harrill, Mariko", "count": 4 }, + { "val": "Leyva, Marie", "count": 4 }, + { "val": "Mackson, Yolanda", "count": 4 }, + { "val": "Oros, Cierra", "count": 4 }, + { "val": "Pakele, Marina", "count": 4 }, + { "val": "Saar, Danna", "count": 4 }, + { "val": "Walcott, Porfirio", "count": 4 }, + { "val": "Murden, Claud", "count": 3 }, + { "val": "Nelms, Lanelle", "count": 3 }, + { "val": "Alcina, Rashad", "count": 2 }, + { "val": "Arechiga, Moon", "count": 2 }, + { "val": "Beckett, Tamala", "count": 2 }, + { "val": "Behringer, Anitra", "count": 2 }, + { "val": "Beveridge, Albina", "count": 2 }, + { "val": "Blacker, Minerva", "count": 2 }, + { "val": "Bogard, Noma", "count": 2 }, + { "val": "Bornstein, Dia", "count": 2 }, + { "val": "Boyland, Bebe", "count": 2 }, + { "val": "Bucher, Margit", "count": 2 }, + { "val": "Cantrell, Cornelius", "count": 2 }, + { "val": "Cornforth, Desiree", "count": 2 }, + { "val": "Counter, Modesto", "count": 2 } + ] + }, + "language": { + "buckets": [ + { "val": "English", "count": 7 }, + { "val": "Italian", "count": 2 }, + { "val": "Latin", "count": 2 } + ] + } + }, "spellcheck":{ "suggestions": [], diff --git a/module/VuFindSearch/tests/fixtures/blender/response/solr/search-title.json b/module/VuFindSearch/tests/fixtures/blender/response/solr/search-title.json index c3d42d663e1..39695663d6a 100644 --- a/module/VuFindSearch/tests/fixtures/blender/response/solr/search-title.json +++ b/module/VuFindSearch/tests/fixtures/blender/response/solr/search-title.json @@ -409,62 +409,69 @@ "work_keys_str_mv":["AT demiankatz octothorpeswhynot"], "_version_":1726988259035185152}] }, - "facet_counts":{ - "facet_queries":{}, - "facet_fields":{ - "institution": - [ - ["MyInstitution",240]], - "building": - [ - ["0/Main/", 231], - ["1/Main/Sub/", 7], - ["0/Sub/",1], - ["1/Sub/Foo/",1]], - "format": - [ - ["Book Chapter",177], - ["Book",63]], - "author_facet": - [ - ["Granda, Isiah",5], - ["Hartfield, Shelba",5], - ["Oram, Donn",5], - ["Overall, Lula",5], - ["Winrow, Sanjuana",5], - ["Abner, Shelli",4], - ["Crafts, Claudette",4], - ["Erickson, Joette",4], - ["Harrill, Mariko",4], - ["Leyva, Marie",4], - ["Mackson, Yolanda",4], - ["Oros, Cierra",4], - ["Pakele, Marina",4], - ["Saar, Danna",4], - ["Walcott, Porfirio",4], - ["Murden, Claud",3], - ["Nelms, Lanelle",3], - ["Alcina, Rashad",2], - ["Arechiga, Moon",2], - ["Beckett, Tamala",2], - ["Behringer, Anitra",2], - ["Beveridge, Albina",2], - ["Blacker, Minerva",2], - ["Bogard, Noma",2], - ["Bornstein, Dia",2], - ["Boyland, Bebe",2], - ["Bucher, Margit",2], - ["Cantrell, Cornelius",2], - ["Cornforth, Desiree",2], - ["Counter, Modesto",2]], - "language": - [ - ["English",7], - ["Italian",2], - ["Latin",2]]}, - "facet_ranges":{}, - "facet_intervals":{}, - "facet_heatmaps":{}}, +"facets": { + "count": 240, + "institution": { + "buckets": [ + { "val": "MyInstitution", "count": 240 } + ] + }, + "building": { + "buckets": [ + { "val": "0/Main/", "count": 231 }, + { "val": "1/Main/Sub/", "count": 7 }, + { "val": "0/Sub/", "count": 1 }, + { "val": "1/Sub/Foo/", "count": 1 } + ] + }, + "format": { + "buckets": [ + { "val": "Book Chapter", "count": 177 }, + { "val": "Book", "count": 63 } + ] + }, + "author_facet": { + "buckets": [ + { "val": "Granda, Isiah", "count": 5 }, + { "val": "Hartfield, Shelba", "count": 5 }, + { "val": "Oram, Donn", "count": 5 }, + { "val": "Overall, Lula", "count": 5 }, + { "val": "Winrow, Sanjuana", "count": 5 }, + { "val": "Abner, Shelli", "count": 4 }, + { "val": "Crafts, Claudette", "count": 4 }, + { "val": "Erickson, Joette", "count": 4 }, + { "val": "Harrill, Mariko", "count": 4 }, + { "val": "Leyva, Marie", "count": 4 }, + { "val": "Mackson, Yolanda", "count": 4 }, + { "val": "Oros, Cierra", "count": 4 }, + { "val": "Pakele, Marina", "count": 4 }, + { "val": "Saar, Danna", "count": 4 }, + { "val": "Walcott, Porfirio", "count": 4 }, + { "val": "Murden, Claud", "count": 3 }, + { "val": "Nelms, Lanelle", "count": 3 }, + { "val": "Alcina, Rashad", "count": 2 }, + { "val": "Arechiga, Moon", "count": 2 }, + { "val": "Beckett, Tamala", "count": 2 }, + { "val": "Behringer, Anitra", "count": 2 }, + { "val": "Beveridge, Albina", "count": 2 }, + { "val": "Blacker, Minerva", "count": 2 }, + { "val": "Bogard, Noma", "count": 2 }, + { "val": "Bornstein, Dia", "count": 2 }, + { "val": "Boyland, Bebe", "count": 2 }, + { "val": "Bucher, Margit", "count": 2 }, + { "val": "Cantrell, Cornelius", "count": 2 }, + { "val": "Cornforth, Desiree", "count": 2 }, + { "val": "Counter, Modesto", "count": 2 } + ] + }, + "language": { + "buckets": [ + { "val": "English", "count": 7 }, + { "val": "Italian", "count": 2 }, + { "val": "Latin", "count": 2 } + ] + } + }, "spellcheck":{ "suggestions": [], diff --git a/module/VuFindSearch/tests/fixtures/blender/response/solr/search.json b/module/VuFindSearch/tests/fixtures/blender/response/solr/search.json index 0017bec6519..206d4aa72cc 100644 --- a/module/VuFindSearch/tests/fixtures/blender/response/solr/search.json +++ b/module/VuFindSearch/tests/fixtures/blender/response/solr/search.json @@ -1844,63 +1844,70 @@ "Site 324"], "_version_":1726988325127979008}] }, - "facet_counts":{ - "facet_queries":{}, - "facet_fields":{ - "institution": - [ - ["MyInstitution",240]], - "building": - [ - ["0/Main/", 231], - ["1/Main/Sub/", 7], - ["0/Sub/",1], - ["1/Sub/Foo/",1]], - "format": - [ - ["Book Chapter",177], - ["Book",63]], - "author_facet": - [ - ["Granda, Isiah",5], - ["Hartfield, Shelba",5], - ["Oram, Donn",5], - ["Overall, Lula",5], - ["Winrow, Sanjuana",5], - ["Abner, Shelli",4], - ["Crafts, Claudette",4], - ["Erickson, Joette",4], - ["Harrill, Mariko",4], - ["Leyva, Marie",4], - ["Mackson, Yolanda",4], - ["Oros, Cierra",4], - ["Pakele, Marina",4], - ["Saar, Danna",4], - ["Walcott, Porfirio",4], - ["Murden, Claud",3], - ["Nelms, Lanelle",3], - ["Alcina, Rashad",2], - ["Arechiga, Moon",2], - ["Beckett, Tamala",2], - ["Behringer, Anitra",2], - ["Beveridge, Albina",2], - ["Blacker, Minerva",2], - ["Bogard, Noma",2], - ["Bornstein, Dia",2], - ["Boyland, Bebe",2], - ["Bucher, Margit",2], - ["Cantrell, Cornelius",2], - ["Cornforth, Desiree",2], - ["Counter, Modesto",2]], - "language": - [ - ["English",7], - ["Italian",2], - ["Latin",2]]}, - "facet_ranges":{}, - "facet_intervals":{}, - "facet_heatmaps":{}}, + "facets": { + "count": 240, + "institution": { + "buckets": [ + { "val": "MyInstitution", "count": 240 } + ] + }, + "building": { + "buckets": [ + { "val": "0/Main/", "count": 231 }, + { "val": "1/Main/Sub/", "count": 7 }, + { "val": "0/Sub/", "count": 1 }, + { "val": "1/Sub/Foo/", "count": 1 } + ] + }, + "format": { + "buckets": [ + { "val": "Book Chapter", "count": 177 }, + { "val": "Book", "count": 63 } + ] + }, + "author_facet": { + "buckets": [ + { "val": "Granda, Isiah", "count": 5 }, + { "val": "Hartfield, Shelba", "count": 5 }, + { "val": "Oram, Donn", "count": 5 }, + { "val": "Overall, Lula", "count": 5 }, + { "val": "Winrow, Sanjuana", "count": 5 }, + { "val": "Abner, Shelli", "count": 4 }, + { "val": "Crafts, Claudette", "count": 4 }, + { "val": "Erickson, Joette", "count": 4 }, + { "val": "Harrill, Mariko", "count": 4 }, + { "val": "Leyva, Marie", "count": 4 }, + { "val": "Mackson, Yolanda", "count": 4 }, + { "val": "Oros, Cierra", "count": 4 }, + { "val": "Pakele, Marina", "count": 4 }, + { "val": "Saar, Danna", "count": 4 }, + { "val": "Walcott, Porfirio", "count": 4 }, + { "val": "Murden, Claud", "count": 3 }, + { "val": "Nelms, Lanelle", "count": 3 }, + { "val": "Alcina, Rashad", "count": 2 }, + { "val": "Arechiga, Moon", "count": 2 }, + { "val": "Beckett, Tamala", "count": 2 }, + { "val": "Behringer, Anitra", "count": 2 }, + { "val": "Beveridge, Albina", "count": 2 }, + { "val": "Blacker, Minerva", "count": 2 }, + { "val": "Bogard, Noma", "count": 2 }, + { "val": "Bornstein, Dia", "count": 2 }, + { "val": "Boyland, Bebe", "count": 2 }, + { "val": "Bucher, Margit", "count": 2 }, + { "val": "Cantrell, Cornelius", "count": 2 }, + { "val": "Cornforth, Desiree", "count": 2 }, + { "val": "Counter, Modesto", "count": 2 } + ] + }, + "language": { + "buckets": [ + { "val": "English", "count": 7 }, + { "val": "Italian", "count": 2 }, + { "val": "Latin", "count": 2 } + ] + } + }, "spellcheck":{ "suggestions": [], - "correctlySpelled":true}} + "correctlySpelled":true}} \ No newline at end of file diff --git a/module/VuFindSearch/tests/fixtures/solr/response/facet b/module/VuFindSearch/tests/fixtures/solr/response/facet index 6738f16783b..1b094136a81 100644 --- a/module/VuFindSearch/tests/fixtures/solr/response/facet +++ b/module/VuFindSearch/tests/fixtures/solr/response/facet @@ -1,43 +1,47 @@ -HTTP/1.1 200 OK -Content-Security-Policy: default-src 'none'; base-uri 'none'; connect-src 'self'; form-action 'self'; font-src 'self'; frame-ancestors 'none'; img-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; worker-src 'self'; -X-Content-Type-Options: nosniff -X-Frame-Options: SAMEORIGIN -X-XSS-Protection: 1; mode=block -Last-Modified: Fri, 08 Apr 2022 10:21:31 GMT -ETag: "MWMwMDAwMDAwMDAwMDAwMFNvbHI=" -Content-Type: application/json;charset=utf-8 -Vary: Accept-Encoding, User-Agent -Content-Length: 779 - +HTTP/1.1 200 OK +Content-Security-Policy: default-src 'none'; base-uri 'none'; connect-src 'self'; form-action 'self'; font-src 'self'; frame-ancestors 'none'; img-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; worker-src 'self'; +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-XSS-Protection: 1; mode=block +Last-Modified: Fri, 08 Apr 2022 10:21:31 GMT +ETag: "MWMwMDAwMDAwMDAwMDAwMFNvbHI=" +Content-Type: application/json;charset=utf-8 +Vary: Accept-Encoding, User-Agent +Content-Length: 779 + { "responseHeader":{ "status":0, - "QTime":0, + "QTime":3, "params":{ - "facet.limit":"5", - "q":"*:*", - "facet.field":"topic_facet", - "json.nl":"arrarr", - "fl":"*", - "start":"0", - "facet.mincount":"1", - "sort":"title_sort asc", - "rows":"0", - "facet":"true", - "wt":"json", - "facet.sort":"count"}}, - "response":{"numFound":317,"start":0,"numFoundExact":true,"docs":[] + "json":"{\r\n \"query\": \"*:*\",\r\n \"limit\" :0,\r\n \"facet\": {\r\n \"topic_facet\": {\r\n \"type\": \"terms\",\r\n \"field\": \"topic_facet\",\r\n \"limit\": 5\r\n }\r\n }\r\n}" + } }, - "facet_counts":{ - "facet_queries":{}, - "facet_fields":{ - "topic_facet": - [ - ["Research",16], - ["Psychotherapy",8], - ["Adult children of aging parents",7], - ["Automobile drivers' tests",7], - ["Fathers and daughters",7]]}, - "facet_ranges":{}, - "facet_intervals":{}, - "facet_heatmaps":{}}} + "response":{ + "numFound":328, + "start":0, + "numFoundExact":true, + "docs":[ ] + }, + "facets":{ + "count":328, + "topic_facet":{ + "buckets":[{ + "val":"Research", + "count":16 + },{ + "val":"Psychotherapy", + "count":8 + },{ + "val":"Adult children of aging parents", + "count":7 + },{ + "val":"Automobile drivers' tests", + "count":7 + },{ + "val":"Fathers and daughters", + "count":7 + }] + } + } +} \ No newline at end of file diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Blender/BackendTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Blender/BackendTest.php index 7374dc7181d..036341bc74a 100644 --- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Blender/BackendTest.php +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Blender/BackendTest.php @@ -350,54 +350,54 @@ public static function getSearchTestData(): array ); return [ - [ + 'limit 0' => [ 0, 0, [], ], - [ + 'limit 20' => [ 0, 20, array_slice($expectedRecords, 0, 20), ], - [ + 'offset 1, limit 20' => [ 1, 20, array_slice($expectedRecords, 1, 20), ], - [ + 'offset 2, limit 20' => [ 2, 20, array_slice($expectedRecords, 2, 20), ], - [ + 'offset 3, limit 20' => [ 3, 20, array_slice($expectedRecords, 3, 20), ], - [ + 'offset 19, limit 20' => [ 19, 20, array_slice($expectedRecords, 19, 20), ], - [ + 'offset 0, limit 40' => [ 0, 40, array_slice($expectedRecords, 0, 40), ], - [ + 'offset 0, limit 40, no boost config' => [ 0, 40, array_slice($expectedRecordsNoBoost, 0, 40), $noBoostConfig, ], - [ + 'offset 0, limit 40, adaptive config' => [ 0, 40, array_slice($expectedRecordsAdaptive, 0, 40), $adaptiveConfig, ], - [ + 'solr only' => [ 0, 20, array_slice($solrRecords, 0, 20), @@ -406,7 +406,7 @@ public static function getSearchTestData(): array 240, null, ], - [ + 'not EDS' => [ 0, 20, array_slice($solrRecords, 0, 20), @@ -415,7 +415,7 @@ public static function getSearchTestData(): array 240, 0, ], - [ + 'EDS only' => [ 0, 20, array_slice($edsRecords, 0, 20), @@ -424,7 +424,7 @@ public static function getSearchTestData(): array 0, 65924, ], - [ + 'both backends' => [ 0, 40, array_slice($expectedRecords, 0, 40), @@ -434,7 +434,7 @@ public static function getSearchTestData(): array . '(blender_backend:"Solr" OR blender_backend:"EDS")', ], ], - [ + 'no Solr or EDS backend' => [ 0, 20, [], @@ -443,7 +443,7 @@ public static function getSearchTestData(): array 0, 0, ], - [ + 'title query' => [ 0, 20, $expectedRecordsTitleSearch, @@ -453,7 +453,7 @@ public static function getSearchTestData(): array 65924, new Query('foo', 'Title'), ], - [ + 'author query' => [ 0, 20, $expectedRecordsAuthorSearch, @@ -1102,15 +1102,15 @@ protected function getSolrConnector( throw new BackendException('Simulated Solr failure'); } if (null === $fixture) { - $field = $params->get('qf')[0] ?? ''; + $field = $params->getNested('params', 'qf')[0] ?? ''; $type = ''; if (in_array($field, ['title', 'author'])) { $type = "-$field"; } $fixture = "blender/response/solr/search$type.json"; } - $start = $params->get('start')[0]; - $rows = $params->get('rows')[0]; + $start = $params->get('offset')[0]; + $rows = $params->get('limit')[0]; $results = $this->getJsonFixture($fixture, 'VuFindSearch'); $results['response']['docs'] = array_slice( $results['response']['docs'], diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php index 9a6d50f139f..ec9592c05f2 100644 --- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php @@ -40,11 +40,10 @@ use VuFindSearch\Backend\Solr\Document\CommitDocument; use VuFindSearch\Backend\Solr\HandlerMap; use VuFindSearch\Backend\Solr\Response\Json\RecordCollection; +use VuFindSearch\NestingParamBag; use VuFindSearch\ParamBag; use VuFindSearch\Query\Query; -use function count; - /** * Unit tests for SOLR backend. * @@ -430,9 +429,9 @@ public function testInjectResponseWriterThrownOnIncompabileResponseWriter(): voi $this->expectException(\VuFindSearch\Exception\InvalidArgumentException::class); $this->expectExceptionMessage('Invalid response writer type: xml'); - $conn = $this->getConnectorMock(); + $conn = $this->getConnectorMock(['retrieve']); $back = new Backend($conn); - $back->retrieve('foobar', new ParamBag(['wt' => ['xml']])); + $back->retrieve('foobar', NestingParamBag::fromArray(['params' => ['wt' => 'xml']])); } /** @@ -447,7 +446,7 @@ public function testInjectResponseWriterThrownOnIncompabileNamedListSetting(): v $conn = $this->getConnectorMock(); $back = new Backend($conn); - $back->retrieve('foobar', new ParamBag(['json.nl' => ['bad']])); + $back->retrieve('foobar', NestingParamBag::fromArray(['params' => ['json.nl' => ['bad']]])); } /** @@ -489,30 +488,27 @@ public static function getIdsProvider(): \Iterator /** * Test getting multiple IDs. * - * @param ?string $flIn Additional field list in input (null = none) - * @param string $expectedFlOut Expected field list in output + * @param ?string $fieldsIn Additional field list in input (null = none) + * @param string $expectedFieldsOut Expected field list in output * * @return void */ #[\PHPUnit\Framework\Attributes\DataProvider('getIdsProvider')] - public function testGetIds(?string $flIn, string $expectedFlOut): void + public function testGetIds(?string $fieldsIn, string $expectedFieldsOut): void { - $paramBagChecker = function (ParamBag $params) use ($expectedFlOut) { + $paramBagChecker = function (ParamBag $params) use ($expectedFieldsOut) { $expected = [ - 'wt' => ['json'], - 'json.nl' => ['arrarr'], - 'fl' => [$expectedFlOut], - 'rows' => [10], - 'start' => [0], - 'q' => ['foo'], + 'params' => [ + 'wt' => 'json', + 'json.nl' => 'arrarr', + ], + 'fields' => $expectedFieldsOut, + 'limit' => 10, + 'offset' => 0, + 'query' => 'foo', ]; - $paramsArr = $params->getArrayCopy(); - foreach ($expected as $key => $vals) { - if (count(array_diff($vals, $paramsArr[$key] ?? [])) !== 0) { - return false; - } - } - return true; + $paramsArr = NestingParamBag::from($params)->jsonSerialize(); + return $expected == $paramsArr; }; // TODO: currently this test is concerned with ensuring that the right // parameters are sent to Solr; it may be worth adding a more realistic @@ -524,8 +520,8 @@ public function testGetIds(?string $flIn, string $expectedFlOut): void $back = new Backend($conn); $query = new Query('foo'); $params = new ParamBag(); - if ($flIn) { - $params->set('fl', $flIn); + if ($fieldsIn) { + $params->set('fields', $fieldsIn); } $result = $back->getIds($query, 0, 10, $params); $this->assertInstanceOf(RecordCollection::class, $result); @@ -579,7 +575,7 @@ public function testRefineAlphaBrowseExceptionWithNonBrowseString(): void public function testRandom(): void { // Test that random sort parameter is added: - $params = $this->getMockBuilder(\VuFindSearch\ParamBag::class) + $params = $this->getMockBuilder(\VuFindSearch\NestingParamBag::class) ->onlyMethods(['set'])->getMock(); $params->expects($this->once())->method('set') ->with('sort', $this->matchesRegularExpression('/[0-9]+_random asc/')); diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php index f59214eb4ed..ccb88f52cad 100644 --- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php @@ -281,7 +281,7 @@ public function testClientCreation() $httpService = $this->createMock(\VuFindHttp\HttpService::class); $httpService->expects($this->once()) ->method('createClient') - ->with('http://localhost/select?q=id%3A%221%22') + ->with('http://localhost/select') ->willReturn($this->createClient()); $connector = new Connector( 'http://localhost', diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php index d92a099f12a..9edda0894f9 100644 --- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php @@ -105,8 +105,8 @@ public function testNormalization(string $input, string $output): void $qb = new QueryBuilder(); $q = new Query($input); $response = $qb->build($q); - $processedQ = $response->get('q'); - $this->assertEquals($output, $processedQ[0]); + $processedQuery = $response->get('query'); + $this->assertEquals($output, $processedQuery[0]); } /** @@ -172,8 +172,8 @@ protected function runBasicQuestionTest($qb, $handler, $test) $response = $qb->build($q); // Make sure the query builder had no side effects on the query object: $this->assertEquals($before, $q->getString()); - $processedQ = $response->get('q'); - $this->assertEquals($basicOutput, $processedQ[0]); + $processedQuery = $response->get('query'); + $this->assertEquals($basicOutput, $processedQuery[0]); } /** @@ -200,8 +200,8 @@ protected function runAdvancedQuestionTest($qb, $handler, $test) } $advancedQ = new QueryGroup('AND', [new Query($input, 'test')]); $advResponse = $qb->build($advancedQ); - $advProcessedQ = $advResponse->get('q'); - $this->assertEquals($advOutput, $advProcessedQ[0]); + $advProcessedQuery = $advResponse->get('query'); + $this->assertEquals($advOutput, $advProcessedQuery[0]); } /** @@ -291,13 +291,13 @@ public function testExactQueryHandler() // non-quoted search uses main DismaxFields $q = new Query('q', 'test'); $response = $qb->build($q); - $qf = $response->get('qf'); + $qf = $response->getNested('params', 'qf'); $this->assertEquals('a b', $qf[0]); // quoted search uses ExactSettings>DismaxFields $q = new Query('"q"', 'test'); $response = $qb->build($q); - $qf = $response->get('qf'); + $qf = $response->getNested('params', 'qf'); $this->assertEquals('c d', $qf[0]); } @@ -315,8 +315,8 @@ public function testQueryHandlerWithFilterQueryAndDisMax() ); $q = new Query('q', 'test'); $response = $qb->build($q); - $fq = $response->get('fq'); - $this->assertEquals('a:filter', $fq[0]); + $filter = $response->get('filter'); + $this->assertEquals('a:filter', $filter[0]); } /** @@ -333,8 +333,8 @@ public function testQueryHandlerWithFilterQueryAndNoDisMax() ); $q = new Query('q', 'test'); $response = $qb->build($q); - $q = $response->get('q'); - $this->assertEquals('((q) AND (a:filter))', $q[0]); + $query = $response->get('query'); + $this->assertEquals('((q) AND (a:filter))', $query[0]); } /** @@ -352,8 +352,8 @@ public function testMatchAllQueryWithFilterQueryAndNoDisMax() ); $q = new Query('*:*', 'test'); $response = $qb->build($q); - $q = $response->get('q'); - $this->assertEquals('a:filter', $q[0]); + $query = $response->get('query'); + $this->assertEquals('a:filter', $query[0]); } /** @@ -380,10 +380,10 @@ public function testHlQ() $qb->setFieldsToHighlight('*'); $response = $qb->build($q); - $hlq = $response->get('hl.q'); - $q = $response->get('q'); + $hlq = $response->getNested('params', 'hl.q'); + $query = $response->get('query'); $this->assertEquals('(my friend*)', $hlq[0]); - $this->assertEquals('((my friend*)) AND (*:* OR boost)', $q[0]); + $this->assertEquals('((my friend*)) AND (*:* OR boost)', $query[0]); } /** @@ -422,7 +422,7 @@ public function testSetFieldsToHighlight() foreach ($tests as $input => $output) { $qb->setFieldsToHighlight($input); $response = $qb->build($q); - $hlfl = $response->get('hl.fl'); + $hlfl = $response->getNested('params', 'hl.fl'); $this->assertEquals($output, $hlfl[0] ?? null); } } @@ -448,13 +448,13 @@ public function testSetCreateSpellingQuery() // No spellcheck.q if spellcheck query disabled: $qb->setCreateSpellingQuery(false); $response1 = $qb->build($q); - $spQ1 = $response1->get('spellcheck.q'); + $spQ1 = $response1->getNested('params', 'spellcheck.q'); $this->assertFalse(isset($spQ1[0])); // spellcheck.q if spellcheck query enabled: $qb->setCreateSpellingQuery(true); $response2 = $qb->build($q); - $spQ2 = $response2->get('spellcheck.q'); + $spQ2 = $response2->getNested('params', 'spellcheck.q'); $this->assertEquals('my friend', $spQ2[0]); } @@ -481,11 +481,11 @@ public function testQueryGroup() $q = new QueryGroup('OR', [$q1, $q2]); $response = $qb->build($q); - $processedQ = $response->get('q'); + $processedQuery = $response->get('query'); $this->assertEquals( '((_query_:"{!dismax qf=\"field_a\" mm=\\\'100%\\\'}value1") OR ' . '(_query_:"{!dismax qf=\"field_b\" mm=\\\'100%\\\'}value2"))', - $processedQ[0] + $processedQuery[0] ); } @@ -516,11 +516,11 @@ public function testQueryGroupWithAdvancedSyntax() $q = new QueryGroup('OR', [$q1, $q2]); $response = $qb->build($q); - $processedQ = $response->get('q'); + $processedQuery = $response->get('query'); $this->assertEquals( '((field_a:(value*)^100 OR field_c:(value*)^200) OR ' . '(_query_:"{!dismax qf=\"field_b\" mm=\\\'100%\\\'}value2"))', - $processedQ[0] + $processedQuery[0] ); } @@ -544,8 +544,8 @@ public function testMultipleQuotedPhrases() $q = new Query('"foo" "bar" "baz"', 'a'); $response = $qb->build($q); - $processedQ = $response->get('q'); - $this->assertEquals('(field_a:("foo" OR "bar" OR "baz"))', $processedQ[0]); + $processedQuery = $response->get('query'); + $this->assertEquals('(field_a:("foo" OR "bar" OR "baz"))', $processedQuery[0]); } /** @@ -568,8 +568,8 @@ public function testMixedQuotedPhrases() $q = new Query('708396 "708398" 708399 "708400"', 'a'); $response = $qb->build($q); - $processedQ = $response->get('q'); - $this->assertEquals('(field_a:(708396 OR "708398" OR 708399 OR "708400"))', $processedQ[0]); + $processedQuery = $response->get('query'); + $this->assertEquals('(field_a:(708396 OR "708398" OR 708399 OR "708400"))', $processedQuery[0]); } /** @@ -592,8 +592,8 @@ public function testMixedQuotedPhrasesWithEscapedQuote() $q = new Query('708396 "708398" 708399 "foo\"bar"', 'a'); $response = $qb->build($q); - $processedQ = $response->get('q'); - $this->assertEquals('(field_a:(708396 OR "708398" OR 708399 OR "foo\"bar"))', $processedQ[0]); + $processedQuery = $response->get('query'); + $this->assertEquals('(field_a:(708396 OR "708398" OR 708399 OR "foo\"bar"))', $processedQuery[0]); } /** @@ -805,7 +805,7 @@ public function testIndividualQueryHandlerWithGlobalExtraParams( $qb = new QueryBuilder($specs); $response = $qb->build($q1, $params1); foreach ($expected1 as $field => $expected) { - $values = $response->get($field); + $values = $response->getNested('params', $field); $this->assertEquals( $expected, $values, @@ -814,7 +814,7 @@ public function testIndividualQueryHandlerWithGlobalExtraParams( } $response = $qb->build($q2, $params2); foreach ($expected2 as $field => $expected) { - $values = $response->get($field); + $values = $response->getNested('params', $field); $this->assertEquals( $expected, $values, @@ -927,7 +927,7 @@ public function testGroupedQueryHandlerWithGlobalExtraParams( $qb = new QueryBuilder($specs); $response = $qb->build($group); foreach ($expectedFields as $field => $expected) { - $values = $response->get($field); + $values = $response->getNested('params', $field); $this->assertEquals( $expected, $values @@ -945,7 +945,7 @@ public function testNegatedQuery() $group = new QueryGroup('NOT', [new Query('q')]); $qb = new QueryBuilder([]); $response = $qb->build($group); - $this->assertEquals(['(*:* NOT (q))'], $response->get('q')); + $this->assertEquals(['(*:* NOT (q))'], $response->get('query')); } /** @@ -962,7 +962,7 @@ public function testNegatedAndQuery() $response = $qb->build($group); $this->assertEquals( ['((*:* NOT (q1 OR q2)) AND (q3 AND q4))'], - $response->get('q') + $response->get('query') ); } @@ -980,7 +980,7 @@ public function testNegatedOrQuery() $response = $qb->build($group); $this->assertEquals( ['((*:* NOT (q1 OR q2)) OR (q3 AND q4))'], - $response->get('q') + $response->get('query') ); } @@ -1016,8 +1016,8 @@ public function testDismaxMunge() [$input, $output] = $test; $q = new Query($input, 'test'); $response = $qb->build($q); - $processedQ = $response->get('q'); - $this->assertEquals($output, $processedQ[0]); + $processedQuery = $response->get('query'); + $this->assertEquals($output, $processedQuery[0]); } } } diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionTest.php index 99df0042642..4ff4dce96f3 100644 --- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionTest.php +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionTest.php @@ -243,12 +243,13 @@ public function testFacets() { $coll = new RecordCollection( [ - 'facet_counts' => [ - 'facet_fields' => [ - 'format' => [ - ['Book', 123], - ['Journal', 234], - ['Map', 1], + 'facets' => [ + 'count' => 358, + 'format' => [ + 'buckets' => [ + ['val' => 'Book', 'count' => 123], + ['val' => 'Journal', 'count' => 234], + ['val' => 'Map', 'count' => 1], ], ], ], diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SimilarBuilderTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SimilarBuilderTest.php index 674a5ef5b6d..e2326d22be3 100644 --- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SimilarBuilderTest.php +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SimilarBuilderTest.php @@ -54,11 +54,11 @@ public function testDefaultParams() { $sb = new SimilarBuilder(); $response = $sb->build('testrecord'); - $rows = $response->get('rows'); - $this->assertEquals(5, $rows[0]); - $q = $response->get('q'); - $this->assertEquals('id:"testrecord"', $q[0]); - $qt = $response->get('qt'); + $limit = $response->get('limit'); + $this->assertEquals(5, $limit[0]); + $query = $response->get('query'); + $this->assertEquals('id:"testrecord"', $query[0]); + $qt = $response->getNested('params', 'qt'); $this->assertEquals('morelikethis', $qt[0]); } @@ -71,8 +71,8 @@ public function testAlternativeIdField() { $sb = new SimilarBuilder(null, 'key'); $response = $sb->build('testrecord'); - $q = $response->get('q'); - $this->assertEquals('key:"testrecord"', $q[0]); + $query = $response->get('query'); + $this->assertEquals('key:"testrecord"', $query[0]); } /** @@ -89,27 +89,27 @@ public function testMltConfig() ]; $sb = new SimilarBuilder(new \VuFind\Config\Config($config)); $response = $sb->build('testrecord'); - $rows = $response->get('rows'); - $this->assertEquals(10, $rows[0]); + $limit = $response->get('limit'); + $this->assertEquals(10, $limit[0]); $config['MoreLikeThis']['useMoreLikeThisHandler'] = true; $sb = new SimilarBuilder(new \VuFind\Config\Config($config)); $response = $sb->build('testrecord'); - $rows = $response->get('rows'); - $this->assertEquals(10, $rows[0]); - $q = $response->get('q'); + $limit = $response->get('limit'); + $this->assertEquals(10, $limit[0]); + $query = $response->get('query'); $this->assertEquals( '{!mlt qf=title,title_short,callnumber-label,topic,language,author,' . 'publishDate mintf=1 mindf=1}testrecord', - $q[0] + $query[0] ); - $qt = $response->get('qt'); + $qt = $response->getNested('params', 'qt'); $this->assertEquals(null, $qt); $config['MoreLikeThis']['params'] = 'qf=title,topic'; $sb = new SimilarBuilder(new \VuFind\Config\Config($config)); $response = $sb->build('testrecord'); - $q = $response->get('q'); - $this->assertEquals('{!mlt qf=title,topic}testrecord', $q[0]); + $query = $response->get('query'); + $this->assertEquals('{!mlt qf=title,topic}testrecord', $query[0]); } } diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/NestingParamBagTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/NestingParamBagTest.php new file mode 100644 index 00000000000..0495644c6fb --- /dev/null +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/NestingParamBagTest.php @@ -0,0 +1,463 @@ +. + * + * @category VuFind + * @package Search + * @author Maccabee Levine + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org + */ + +namespace VuFindTest; + +use PHPUnit\Framework\TestCase; +use VuFindSearch\NestingParamBag; +use VuFindSearch\ParamBag; + +use function is_string; + +/** + * Unit tests for NestingParamBag. + * + * @category VuFind + * @package Search + * @author Maccabee Levine + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org + */ +class NestingParamBagTest extends TestCase +{ + /** + * Data provider for testFrom + * + * @return \Iterator + */ + public static function fromProvider(): \Iterator + { + yield 'simple' => [ + new ParamBag([ 'filter' => 'format:Book', 'debugQuery' => true ]), + [], + [ 'filter' => ['format:Book'], 'debugQuery' => [ true ], ], + ]; + yield 'null, default create' => [ + null, + [], + [], + ]; + yield 'null, create true' => [ + null, + [ true ], + [], + ]; + yield 'null, create false' => [ + null, + [ false ], + null, + ]; + } + + /** + * Test static from() function. + * + * @param ?ParamBag $original Original ParamBag + * @param array $createIfNullParam Array containing nothing or the $createIfNull bool + * @param ?array $expectedContent Expected content of the NestingParamBag + * + * @return void + */ + #[\PHPUnit\Framework\Attributes\DataProvider('fromProvider')] + public function testFrom(?ParamBag $original, array $createIfNullParam, ?array $expectedContent): void + { + $params = NestingParamBag::from($original, ...$createIfNullParam); + $this->assertSame($expectedContent, $params?->getArrayCopy()); + } + + /** + * Data provider for testFromArray + * + * @return \Iterator + */ + public static function fromArrayProvider(): \Iterator + { + yield 'simple' => [ + [ 'filter' => 'format:Book', 'rows' => 10 ], + new NestingParamBag([ 'filter' => [ 'format:Book' ], 'rows' => [ 10 ] ]), + ]; + yield 'two dimensions' => [ + [ 'filter' => 'format:Book', 'params' => [ 'sow' => false, 'timeAllowed' => -1 ] ], + new NestingParamBag([ + 'filter' => [ 'format:Book' ], + 'params' => new NestingParamBag(['sow' => false, 'timeAllowed' => -1]), + ]), + ]; + } + + /** + * Test static fromArray() function. + * + * @param array $values Input values + * @param NestingParamBag $expected Expected result + * + * @return void + */ + #[\PHPUnit\Framework\Attributes\DataProvider('fromArrayProvider')] + public function testFromArray(array $values, NestingParamBag $expected): void + { + $params = NestingParamBag::fromArray($values); + $this->assertEquals($expected, $params); + } + + /** + * Data provider for getNested method + * + * @return \Iterator + */ + public static function getNestedProvider(): \Iterator + { + yield 'present' => [ self::buildInputParams(), 'params', 'sow', [ false ] ]; + yield 'absent' => [ self::buildInputParams(), 'params', 'debugQuery', null ]; + } + + /** + * Test getNested method. + * + * @param NestingParamBag $params Bag of nested parameters + * @param string $name Parameter name + * @param string $nestedName Nested parameter name + * @param ?array $expected Expected array of parameter values or NULL if not set + * + * @return void + */ + #[\PHPUnit\Framework\Attributes\DataProvider('getNestedProvider')] + public function testGetNested(NestingParamBag $params, string $name, string $nestedName, ?array $expected): void + { + $this->assertSame($expected, $params->getNested($name, $nestedName)); + } + + /** + * Test hasNestedParam method. + * + * @param NestingParamBag $params Bag of nested parameters + * @param string $name Parameter name + * @param string $nestedName Nested parameter name + * @param ?array $expectedArray Expected array of parameter values or NULL if not set + * + * @return void + */ + #[\PHPUnit\Framework\Attributes\DataProvider('getNestedProvider')] + public function testHasNested( + NestingParamBag $params, + string $name, + string $nestedName, + ?array $expectedArray + ): void { + $expected = !empty($expectedArray); + $this->assertSame($expected, $params->hasNestedParam($name, $nestedName)); + } + + /** + * Data provider for setNested method + * + * @return \Iterator + */ + public static function setNestedProvider(): \Iterator + { + yield 'add value' => [ + self::buildInputParams(), + 'params', 'spellcheck', true, + new NestingParamBag([ + 'filter' => [ 'format:Book' ], + 'params' => new NestingParamBag(['sow' => false, 'timeAllowed' => -1, 'spellcheck' => true]), + ]), + ]; + yield 'overwrite value' => [ + self::buildInputParams(), + 'params', 'sow', true, + new NestingParamBag([ + 'filter' => [ 'format:Book' ], + 'params' => new NestingParamBag(['sow' => true, 'timeAllowed' => -1]), + ]), + ]; + yield 'new top-level name' => [ + self::buildInputParams(), + 'facet', 'topic_facet', 'foo', + new NestingParamBag([ + 'filter' => [ 'format:Book' ], + 'params' => new NestingParamBag(['sow' => false, 'timeAllowed' => -1]), + 'facet' => new ParamBag(['topic_facet' => 'foo']), + ]), + ]; + } + + /** + * Test setNestedParam method. + * + * @param NestingParamBag $params Bag of nested parameters + * @param string $name Parameter name + * @param string $nestedName Nested parameter name + * @param mixed $value Nested parameter value + * @param NestingParamBag $expected Expected resulting bag of nested parameters + * + * @return void + */ + #[\PHPUnit\Framework\Attributes\DataProvider('setNestedProvider')] + public function testSetNested( + NestingParamBag $params, + string $name, + string $nestedName, + mixed $value, + NestingParamBag $expected + ): void { + $params->setNested($name, $nestedName, $value); + $this->assertEquals($expected, $params); + } + + /** + * Data provider for addNested method + * + * @return \Iterator + */ + public static function addNestedProvider(): \Iterator + { + yield 'existing name' => [ + self::buildInputParams(), + 'params', 'sow', true, + new NestingParamBag([ + 'filter' => [ 'format:Book' ], + 'params' => new NestingParamBag(['sow' => [false, true], 'timeAllowed' => -1]), + ]), + ]; + yield 'new nested name' => [ + self::buildInputParams(), + 'params', 'spellcheck', true, + new NestingParamBag([ + 'filter' => [ 'format:Book' ], + 'params' => new NestingParamBag(['sow' => [false], 'timeAllowed' => -1, 'spellcheck' => true]), + ]), + ]; + yield 'new top-level name' => [ + self::buildInputParams(), + 'facet', 'topic_facet', 'foo', + new NestingParamBag([ + 'filter' => [ 'format:Book' ], + 'params' => new NestingParamBag(['sow' => false, 'timeAllowed' => -1]), + 'facet' => new ParamBag(['topic_facet' => 'foo']), + ]), + ]; + } + + /** + * Test addNestedParam method. + * + * @param NestingParamBag $params Bag of nested parameters + * @param string $name Parameter name + * @param string $nestedName Nested parameter name + * @param mixed $value Nested parameter value + * @param NestingParamBag $expected Expected resulting bag of nested parameters + * + * @return void + */ + #[\PHPUnit\Framework\Attributes\DataProvider('addNestedProvider')] + public function testAddNested( + NestingParamBag $params, + string $name, + string $nestedName, + mixed $value, + NestingParamBag $expected + ): void { + $params->addNested($name, $nestedName, $value); + $this->assertEquals($expected, $params); + } + + /** + * Data provider for addMultiNested method + * + * @return \Iterator + */ + public static function addMultiNestedProvider(): \Iterator + { + yield 'second level' => [ + self::buildInputParams(), + 'params', + ['spellcheck' => true], + new NestingParamBag([ + 'filter' => [ 'format:Book' ], + 'params' => new NestingParamBag(['sow' => false, 'timeAllowed' => -1, 'spellcheck' => true]), + ]), + ]; + yield 'deeper' => [ + self::buildInputParams(), + 'facet', + ['topic_facet' => ['type' => 'terms', 'field' => 'topic_facet', 'limit' => 30, + 'domain' => ['excludeTags' => 'topic_facet_filter']]], + new NestingParamBag([ + 'filter' => [ 'format:Book' ], + 'params' => new NestingParamBag(['sow' => false, 'timeAllowed' => -1]), + 'facet' => new NestingParamBag(['topic_facet' => new NestingParamBag( + ['type' => 'terms', 'field' => 'topic_facet', 'limit' => 30, 'domain' => new NestingParamBag( + ['excludeTags' => 'topic_facet_filter'] + )] + )]), + ]), + ]; + } + + /** + * Test addMultiNested method. + * + * @param NestingParamBag $params Bag of nested parameters + * @param string $name Parameter name + * @param array $value Nested array of parameter values + * @param NestingParamBag $expected Expected resulting bag of nested parameters + * + * @return void + */ + #[\PHPUnit\Framework\Attributes\DataProvider('addMultiNestedProvider')] + public function testAddMultiNested( + NestingParamBag $params, + string $name, + mixed $value, + NestingParamBag $expected + ): void { + $params->addMultiNested($name, $value); + $this->assertEquals($expected, $params); + } + + /** + * Data provider for add method + * + * @return \Iterator + */ + public static function addProvider(): \Iterator + { + yield 'new name' => [ + self::buildInputParams(), + 'sort', 'title', true, + new NestingParamBag([ + 'filter' => [ 'format:Book' ], + 'params' => new NestingParamBag(['sow' => false, 'timeAllowed' => -1]), + 'sort' => 'title', + ]), + ]; + yield 'existing name' => [ + self::buildInputParams(), + 'filter', 'location:Main Library', true, + new NestingParamBag([ + 'filter' => [ 'format:Book', 'location:Main Library' ], + 'params' => new NestingParamBag(['sow' => false, 'timeAllowed' => -1]), + ]), + ]; + yield 'adding scalar where existing is a ParamBag' => [ + self::buildInputParams(), + 'params', true, true, + 'New values for name params are not compatible with existing values; both or neither must be a ParamBag.', + ]; + yield 'adding ParamBag where existing is a scalar' => [ + self::buildInputParams(), + 'filter', new ParamBag(['foo' => 'bar']), true, + 'New values for name filter are not compatible with existing values; both or neither must be a ParamBag.', + ]; + } + + /** + * Test add method. + * + * @param NestingParamBag $params Bag of nested parameters + * @param string $name Parameter name + * @param mixed $value Parameter values + * @param bool $deduplicate Whether to de-duplicate + * @param NestingParamBag|string $expected Expected resulting bag of nested parameters or exception message + * + * @return void + */ + #[\PHPUnit\Framework\Attributes\DataProvider('addProvider')] + public function testAdd( + NestingParamBag $params, + string $name, + mixed $value, + bool $deduplicate, + NestingParamBag|string $expected + ): void { + if (is_string($expected)) { + $this->expectExceptionMessage($expected); + } + $params->add($name, $value, $deduplicate); + if ($expected instanceof NestingParamBag) { + $this->assertEquals($expected, $params); + } + } + + /** + * Data provider for testJsonSerialize method + * + * @return \Iterator + */ + public static function jsonSerializeProvider(): \Iterator + { + yield 'basic' => [ + self::buildInputParams(), + [ + 'filter' => 'format:Book', + 'params' => ['sow' => false, 'timeAllowed' => -1], + ], + ]; + yield 'multiple values for same name' => [ + new NestingParamBag([ + 'filter' => [ 'format:Book', 'location:Main Library'], + 'params' => new NestingParamBag(['sow' => false, 'timeAllowed' => -1]), + ]), + [ + 'filter' => ['format:Book', 'location:Main Library'], + 'params' => ['sow' => false, 'timeAllowed' => -1], + ], + + ]; + } + + /** + * Test add method. + * + * @param NestingParamBag $params Bag of nested parameters + * @param array $expected Expected array of JSON-serialized values + * + * @return void + */ + #[\PHPUnit\Framework\Attributes\DataProvider('jsonSerializeProvider')] + public function testJsonSerialize(NestingParamBag $params, array $expected): void + { + $serialized = $params->jsonSerialize(); + $this->assertEquals($expected, $serialized); + } + + /** + * Build a NestingParamBag for testing. + * + * @return NestingParamBag + */ + protected static function buildInputParams(): NestingParamBag + { + return new NestingParamBag([ + 'filter' => [ 'format:Book' ], + 'params' => new NestingParamBag(['sow' => false, 'timeAllowed' => -1]), + ]); + } +}