Skip to content
Draft
Show file tree
Hide file tree
Changes from 14 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
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
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public function onSearchPre(EventInterface $event)
if (!isset($parts[1])) {
continue;
}
// TODO This will need some config changes to know how to nest
Comment thread
demiankatz marked this conversation as resolved.
Outdated
$params->add(urldecode($parts[0]), urldecode($parts[1]));
}
}
Expand Down
22 changes: 13 additions & 9 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,15 +250,18 @@ public function performRequest($recordId)

// prepare search params
$params = $this->getParams()->getBackendParameters();
$params->set('spellcheck', 'false');
$explainParams = new ParamBag([
'fl' => 'id,score',
'facet' => 'true',
'debug' => 'true',
'indent' => 'true',
'param' => 'q',
'echoParams' => 'all',
'explainOther' => 'id:"' . addcslashes($recordId, '"') . '"',
$params = ParamBagBag::from($params);
$params->setNested('params', 'spellcheck', 'false');
$explainParams = new ParamBagBag([
'fields' => 'id,score',
'facet' => 'true', // This field will need an update when I do the facet API upgrade
Comment thread
maccabeelevine marked this conversation as resolved.
Outdated
'params' => new ParamBag([
'debug' => 'true',
'indent' => 'true',
'param' => 'query', // Is this change correct?
Comment thread
maccabeelevine marked this conversation as resolved.
Outdated
'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
20 changes: 12 additions & 8 deletions module/VuFind/src/VuFind/Search/Solr/InjectSpellingListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
use VuFind\Log\LoggerAwareTrait;
use VuFindSearch\Backend\BackendInterface;
use VuFindSearch\Backend\Solr\Response\Json\Spellcheck;
use VuFindSearch\ParamBag;
use VuFindSearch\ParamBagBag;
use VuFindSearch\Query\Query;
use VuFindSearch\Service;

Expand Down Expand Up @@ -128,8 +128,10 @@ public function onSearchPre(EventInterface $event)
}
if ($command->getTargetIdentifier() === $this->backend->getIdentifier()) {
if ($params = $command->getSearchParameters()) {
$params = ParamBagBag::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)) {
Expand All @@ -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)
);
Expand Down Expand Up @@ -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 = ParamBagBag::from($params);
$spellcheckQuery = $params->getNested('params', 'spellcheck.q');
if (!empty($spellcheckQuery)) {
$this->aggregateSpellcheck(
$result->getSpellcheck(),
Expand All @@ -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 ParamBagBag();
$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);
Expand Down
19 changes: 15 additions & 4 deletions module/VuFind/src/VuFind/Search/Solr/MultiIndexListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
use Laminas\EventManager\EventInterface;
use Laminas\EventManager\SharedEventManagerInterface;
use VuFindSearch\Backend\BackendInterface;
use VuFindSearch\ParamBag;
use VuFindSearch\ParamBagBag;
use VuFindSearch\Service;

use function in_array;
Expand Down Expand Up @@ -126,28 +128,37 @@ public function onSearchPre(EventInterface $event)
$command = $event->getParam('command');
if ($command->getTargetIdentifier() === $this->backend->getIdentifier()) {
$params = $command->getSearchParameters();
$params = ParamBagBag::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.

// 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 : []))
);
$fields = $this->getFields($shards);
$specs = $this->getSearchSpecs($fields);
$this->backend->getQueryBuilder()->setSpecs($specs);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic below is just a first draft, I don't like how it breaks the ParamBag abstraction completely in order to use array_filter. Either have to build some sort of filtering into ParamBag itself or just create a new ParamBag on the fly instead.

Also, I will need assistance actually testing this with shards.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to test it with any two instances of Solr running the same schema. Let me know if you need more specific help on setting up a test environment; I haven't done it in a few years, but I don't recall it being too difficult.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok thanks...will try pointing it at our test and prod environments...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should work as long as they contain different record IDs (otherwise it will be hard to tell if the sharding is working).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have (mostly) overlapping record IDs, so the sharding doesn't really work. But the faceting logic that I'm implementing does seem to -- I'm able to remove a facet group with [StripFields]. I suppose I can create a few fresh schemas and do partial indexes into each, but if someone already has sharding set up that will be a plus.

Copy link
Copy Markdown
Member

@demiankatz demiankatz Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One simple approach could be to spin up a standard test instance using phing startup in a virtual machine, and then shard that with your institutional test instance.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(If you can't easily do this, I certainly can -- just have to find time, since I remain buried under a large review backlog right now).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I'm still not having any luck with testing the shards config...if/when you have time, I'd appreciate it.

$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;
Expand Down
Loading
Loading