Skip to content
Draft
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
48a8396
VUFIND-1210: Use Solr JSON request API
maccabeelevine Dec 31, 2025
caae5eb
Fix spelling listener
maccabeelevine Jan 2, 2026
8e80568
First pass at several listeners
maccabeelevine Jan 2, 2026
e82a8e9
First version of JSON Facets api, working at basic level
maccabeelevine Jan 2, 2026
27c2e6d
Fix styles
maccabeelevine Jan 2, 2026
2a56ef9
Mark some TODOs
maccabeelevine Jan 2, 2026
f6d550a
First attempt at MultiIndexListener
maccabeelevine Jan 16, 2026
e35bfe1
Support facet prefix
maccabeelevine Jan 16, 2026
6206ef4
Support OR facets
maccabeelevine Jan 16, 2026
5db7650
Simply creating a ParamBagBag from null
maccabeelevine Jan 16, 2026
09be7e1
Handle multiple values with the same name, i.e. filters.
maccabeelevine Jan 16, 2026
2f2d089
Implement JsonSerializable
maccabeelevine Jan 16, 2026
d943442
Throw any JSON encoding error
maccabeelevine Jan 16, 2026
9451ac2
Support TreeDataSource, maybe
maccabeelevine Jan 16, 2026
3a51fa7
Merge branch 'dev' into solr-json-search
maccabeelevine Jan 19, 2026
af4ee52
Remove fields not needed for Explanation
maccabeelevine Jan 19, 2026
5cc2f9f
Merge branch 'dev' into solr-json-search
maccabeelevine Jan 20, 2026
990c9ed
Break up the tests a bit
maccabeelevine Jan 27, 2026
1faec2b
Merge branch 'dev' into solr-json-search
maccabeelevine Jan 30, 2026
cf8e76d
Revert accidental change in merge
maccabeelevine Jan 30, 2026
9d45b06
Implement DefaultParametersListener
maccabeelevine Jan 30, 2026
d3f0dde
Fix json_decode call
maccabeelevine Jan 30, 2026
7d2d26b
Start building ParamBagBagTest
maccabeelevine Jan 30, 2026
6e84cf5
Add the rest of the ParamBagBag unit tests
maccabeelevine Feb 2, 2026
4ff1d6e
Fix setNested calls after stronger typing
maccabeelevine Feb 2, 2026
e285203
Simplify addMultiNested
maccabeelevine Feb 3, 2026
9828993
Fix a hundred or so tests, more to go
maccabeelevine Feb 5, 2026
db3abd5
Fix Blender behavior and tests
maccabeelevine Feb 5, 2026
31e30c8
Merge branch 'dev' into solr-json-search
maccabeelevine Feb 5, 2026
c08ae52
Rename ParamBagBag to NestingParamBag
maccabeelevine Feb 5, 2026
c16edfc
Remove facet_matches_by_field config. It is not supported by new JSO…
maccabeelevine Feb 5, 2026
0e44e67
Fix SimilarBuilder behavior
maccabeelevine Feb 13, 2026
b34c2ee
Fix TreeDataSource
maccabeelevine Feb 13, 2026
5cea405
Fix styles
maccabeelevine Feb 13, 2026
7398011
Fix SimilarItemsTest
maccabeelevine Feb 13, 2026
e2cb768
Throw better exceptions in NestingParamBag
maccabeelevine Feb 13, 2026
e73f2ef
Fix add()
maccabeelevine Feb 13, 2026
67ba9b5
Implement "facet contains" after the search
maccabeelevine Feb 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions config/vufind/searches.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
28 changes: 16 additions & 12 deletions module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Solr.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

use VuFind\Hierarchy\TreeDataFormatter\PluginManager as FormatterManager;
use VuFindSearch\Backend\Solr\Command\RawJsonSearchCommand;
use VuFindSearch\ParamBag;
use VuFindSearch\ParamBagBag;
use VuFindSearch\Query\Query;
use VuFindSearch\Service;

Expand Down Expand Up @@ -152,12 +152,14 @@ public function getXML($id, $options = [])
protected function getDefaultSearchParams(): array
Comment thread
maccabeelevine marked this conversation as resolved.
{
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'],
],
];
}

Expand All @@ -172,7 +174,7 @@ protected function getDefaultSearchParams(): array
*/
protected function searchSolrLegacy(Query $query, $rows): array
{
$params = new ParamBag($this->getDefaultSearchParams());
$params = ParamBagBag::fromArray($this->getDefaultSearchParams());
$command = new RawJsonSearchCommand(
$this->backendId,
$query,
Expand All @@ -198,14 +200,16 @@ protected function searchSolrCursor(Query $query, $rows): array
$cursorMark = '*';
$records = [];
while ($cursorMark !== $prevCursorMark) {
$params = new ParamBag(
$params = ParamBagBag::fromArray(
$this->getDefaultSearchParams() + [
// Sort is required
'sort' => ['id asc'],
// Override any default timeAllowed since it cannot be used with
// cursorMark
'timeAllowed' => -1,
'cursorMark' => $cursorMark,
'params' => [
// Override any default timeAllowed since it cannot be used with
// cursorMark
'timeAllowed' => -1,
'cursorMark' => $cursorMark,
],
]
);
$command = new RawJsonSearchCommand(
Expand Down
4 changes: 3 additions & 1 deletion module/VuFind/src/VuFind/RecordDriver/SolrDefault.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
namespace VuFind\RecordDriver;

use VuFindSearch\Command\SearchCommand;
use VuFindSearch\ParamBag;
use VuFindSearch\ParamBagBag;

use function count;
use function in_array;
Expand Down Expand Up @@ -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 ParamBagBag(['params' => new ParamBag(['hl' => ['false']])]);
$command = new SearchCommand($this->sourceIdentifier, $query, 0, 0, $params);
return $this->searchService
->invoke($command)->getResult()->getTotal();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,16 +494,16 @@ protected function createConnector()
$handlers = [
'select' => [
'fallback' => true,
'defaults' => ['fl' => $defaultFields],
'appends' => ['fq' => []],
'defaults' => ['fields' => $defaultFields],
'appends' => ['filter' => []],
],
'terms' => [
'functions' => ['terms'],
],
];

foreach ($this->getHiddenFilters() as $filter) {
array_push($handlers['select']['appends']['fq'], $filter);
array_push($handlers['select']['appends']['filter'], $filter);
}

$connector = new $this->connectorClass(
Expand Down
8 changes: 4 additions & 4 deletions module/VuFind/src/VuFind/Search/Params/FacetLimitTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand All @@ -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;
}
Expand Down Expand Up @@ -125,7 +125,7 @@ public function getHierarchicalFacetLimit()
*
* @return void
*/
public function setHierarchicalFacetLimit($limit)
public function setHierarchicalFacetLimit(int $limit)
{
$this->hierarchicalFacetLimit = $limit;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class CustomFilterListener
*
* @var string
*/
protected $filterParam = 'fq';
protected $filterParam = 'filter';

/**
* Constructor.
Expand Down
10 changes: 5 additions & 5 deletions module/VuFind/src/VuFind/Search/Solr/DeduplicationListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

Expand Down
23 changes: 15 additions & 8 deletions module/VuFind/src/VuFind/Search/Solr/DefaultParametersListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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\ParamBagBag;
use VuFindSearch\Service;

/**
Expand Down Expand Up @@ -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 = ParamBagBag::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);
}
}
}
Expand Down
18 changes: 11 additions & 7 deletions module/VuFind/src/VuFind/Search/Solr/Explanation.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

use VuFindSearch\Backend\Solr\Command\RawJsonSearchCommand;
use VuFindSearch\ParamBag;
use VuFindSearch\ParamBagBag;

use function count;
use function floatval;
Expand Down Expand Up @@ -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 = ParamBagBag::from($params);
$params->setNested('params', 'spellcheck', 'false');
$explainParams = new ParamBagBag([
'fields' => 'id,score',
'params' => new ParamBag([
'debug' => 'true',
'indent' => 'true',
'echoParams' => 'all',
'explainOther' => 'id:"' . addcslashes($recordId, '"') . '"',
]),
]);
$params->mergeWith($explainParams);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,22 +91,22 @@ 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/",
"$newField:",
$currentFilter
);
}
$new_fq[] = $currentFilter;
$new_filters[] = $currentFilter;
}
$params->set('fq', $new_fq);
$params->set('filter', $new_filters);
}

return $event;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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('filters', $new_filters);

return $event;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use Laminas\EventManager\EventInterface;
use Laminas\EventManager\SharedEventManagerInterface;
use VuFindSearch\Backend\BackendInterface;
use VuFindSearch\ParamBagBag;
use VuFindSearch\Service;

/**
Expand Down Expand Up @@ -120,18 +121,19 @@ public function onSearchPre(EventInterface $event)
}
if ($command->getTargetIdentifier() === $this->backend->getIdentifier()) {
if ($params = $command->getSearchParameters()) {
$params = ParamBagBag::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->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()
Expand Down
Loading
Loading