Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.74% covered (warning)
73.74%
146 / 198
65.38% covered (warning)
65.38%
17 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractSolrBackendFactory
73.74% covered (warning)
73.74%
146 / 198
65.38% covered (warning)
65.38%
17 / 26
134.89
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __invoke
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 getPrioritizedConfigsForIndexSettings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMergedIndexConfig
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getFlatIndexConfig
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getIndexConfig
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 createBackend
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 createListeners
58.18% covered (warning)
58.18%
32 / 55
0.00% covered (danger)
0.00%
0 / 1
28.33
 getIndexName
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getSolrBaseUrls
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getSolrUrl
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getHiddenFilters
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
5.27
 createConnector
89.66% covered (warning)
89.66%
26 / 29
0.00% covered (danger)
0.00%
0 / 1
6.04
 getHttpOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createQueryBuilder
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 createLuceneSyntaxHelper
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 createSimilarBuilder
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 createRecordCollectionFactory
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getCreateRecordCallback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 loadSpecs
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDeduplicationListener
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getCustomFilterListener
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
5.39
 getHierarchicalFacetListener
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getInjectHighlightingListener
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getInjectConditionalFilterListener
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultParametersListener
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * Abstract factory for SOLR backends.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2013.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  Search
25 * @author   David Maus <maus@hab.de>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org Main Site
28 */
29
30namespace VuFind\Search\Factory;
31
32use Laminas\Config\Config;
33use Psr\Container\ContainerInterface;
34use VuFind\Search\Solr\CustomFilterListener;
35use VuFind\Search\Solr\DeduplicationListener;
36use VuFind\Search\Solr\DefaultParametersListener;
37use VuFind\Search\Solr\FilterFieldConversionListener;
38use VuFind\Search\Solr\HierarchicalFacetListener;
39use VuFind\Search\Solr\InjectConditionalFilterListener;
40use VuFind\Search\Solr\InjectHighlightingListener;
41use VuFind\Search\Solr\InjectSpellingListener;
42use VuFind\Search\Solr\MultiIndexListener;
43use VuFind\Search\Solr\V3\ErrorListener as LegacyErrorListener;
44use VuFind\Search\Solr\V4\ErrorListener;
45use VuFindSearch\Backend\BackendInterface;
46use VuFindSearch\Backend\Solr\Backend;
47use VuFindSearch\Backend\Solr\Connector;
48use VuFindSearch\Backend\Solr\HandlerMap;
49use VuFindSearch\Backend\Solr\LuceneSyntaxHelper;
50use VuFindSearch\Backend\Solr\QueryBuilder;
51use VuFindSearch\Backend\Solr\Response\Json\RecordCollection;
52use VuFindSearch\Backend\Solr\Response\Json\RecordCollectionFactory;
53use VuFindSearch\Backend\Solr\SimilarBuilder;
54use VuFindSearch\Response\RecordCollectionFactoryInterface;
55
56use function count;
57use function is_object;
58
59/**
60 * Abstract factory for SOLR backends.
61 *
62 * @category VuFind
63 * @package  Search
64 * @author   David Maus <maus@hab.de>
65 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
66 * @link     https://vufind.org Main Site
67 */
68abstract class AbstractSolrBackendFactory extends AbstractBackendFactory
69{
70    use SharedListenersTrait;
71
72    /**
73     * Logger.
74     *
75     * @var \Laminas\Log\LoggerInterface
76     */
77    protected $logger;
78
79    /**
80     * Primary configuration file identifier.
81     *
82     * @var string
83     */
84    protected $mainConfig = 'config';
85
86    /**
87     * Search configuration file identifier.
88     *
89     * @var string
90     */
91    protected $searchConfig;
92
93    /**
94     * Facet configuration file identifier.
95     *
96     * @var string
97     */
98    protected $facetConfig;
99
100    /**
101     * YAML searchspecs filename.
102     *
103     * @var string
104     */
105    protected $searchYaml;
106
107    /**
108     * VuFind configuration reader
109     *
110     * @var \VuFind\Config\PluginManager
111     */
112    protected $config;
113
114    /**
115     * Name of index configuration setting to use to retrieve Solr index name
116     * (core or collection).
117     *
118     * @var string
119     */
120    protected $indexNameSetting = 'default_core';
121
122    /**
123     * Solr index name (used as default if $this->indexNameSetting is unset in
124     * the config).
125     *
126     * @var string
127     */
128    protected $defaultIndexName = '';
129
130    /**
131     * When looking up the Solr index name config setting, should we allow fallback
132     * into the main configuration (true), or limit ourselves to the search
133     * config (false)?
134     *
135     * @var bool
136     */
137    protected $allowFallbackForIndexName = false;
138
139    /**
140     * Solr field used to store unique identifiers
141     *
142     * @var string
143     */
144    protected $uniqueKey = 'id';
145
146    /**
147     * Solr connector class
148     *
149     * @var string
150     */
151    protected $connectorClass = Connector::class;
152
153    /**
154     * Solr backend class
155     *
156     * @var string
157     */
158    protected $backendClass = Backend::class;
159
160    /**
161     * Record collection class for RecordCollectionFactory
162     *
163     * @var string
164     */
165    protected $recordCollectionClass = RecordCollection::class;
166
167    /**
168     * Record collection factory class
169     *
170     * @var string
171     */
172    protected $recordCollectionFactoryClass = RecordCollectionFactory::class;
173
174    /**
175     * Merged index configuration
176     *
177     * @var ?array
178     */
179    protected $mergedIndexConfig = null;
180
181    /**
182     * Constructor
183     */
184    public function __construct()
185    {
186        parent::__construct();
187    }
188
189    /**
190     * Create service
191     *
192     * @param ContainerInterface $sm      Service manager
193     * @param string             $name    Requested service name
194     * @param array              $options Extra options (unused)
195     *
196     * @return Backend
197     *
198     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
199     */
200    public function __invoke(ContainerInterface $sm, $name, array $options = null)
201    {
202        $this->setup($sm);
203        $this->config = $this->serviceLocator
204            ->get(\VuFind\Config\PluginManager::class);
205        if ($this->serviceLocator->has(\VuFind\Log\Logger::class)) {
206            $this->logger = $this->serviceLocator->get(\VuFind\Log\Logger::class);
207        }
208        $connector = $this->createConnector();
209        $backend   = $this->createBackend($connector);
210        $backend->setIdentifier($name);
211        $this->createListeners($backend);
212        return $backend;
213    }
214
215    /**
216     * Return an ordered array of configurations to check for index configurations.
217     *
218     * @return string[]
219     */
220    protected function getPrioritizedConfigsForIndexSettings(): array
221    {
222        return array_unique([$this->searchConfig, $this->mainConfig]);
223    }
224
225    /**
226     * Merge together the Index sections of all eligible configuration files and
227     * return the result as an array.
228     *
229     * @return array
230     */
231    protected function getMergedIndexConfig(): array
232    {
233        if (null === $this->mergedIndexConfig) {
234            $this->mergedIndexConfig = [];
235            foreach ($this->getPrioritizedConfigsForIndexSettings() as $configName) {
236                $config = $this->config->get($configName);
237                $this->mergedIndexConfig += isset($config->Index)
238                    ? $config->Index->toArray() : [];
239            }
240        }
241        return $this->mergedIndexConfig;
242    }
243
244    /**
245     * Get the Index section of the highest-priority configuration file (for use
246     * in cases where fallback is not desired).
247     *
248     * @return array
249     */
250    protected function getFlatIndexConfig(): array
251    {
252        $configList = $this->getPrioritizedConfigsForIndexSettings();
253        $configObj = $this->config->get($configList[0]);
254        return isset($configObj->Index)
255            ? $configObj->Index->toArray() : [];
256    }
257
258    /**
259     * Get an index-related configuration setting.
260     *
261     * @param string $setting  Name of setting
262     * @param mixed  $default  Default value if unset
263     * @param bool   $fallback Should we fall back to main config if the
264     * setting is absent from the search config file?
265     *
266     * @return mixed
267     */
268    protected function getIndexConfig(
269        string $setting,
270        $default = null,
271        bool $fallback = true
272    ) {
273        $config = $fallback
274            ? $this->getMergedIndexConfig() : $this->getFlatIndexConfig();
275        return $config[$setting] ?? $default;
276    }
277
278    /**
279     * Create the SOLR backend.
280     *
281     * @param Connector $connector Connector
282     *
283     * @return Backend
284     */
285    protected function createBackend(Connector $connector)
286    {
287        $backend = new $this->backendClass($connector);
288        $pageSize = $this->getIndexConfig('record_batch_size', 100);
289        $maxClauses = $this->getIndexConfig('maxBooleanClauses', $pageSize);
290        if ($pageSize > 0 && $maxClauses > 0) {
291            $backend->setPageSize(min($pageSize, $maxClauses));
292        }
293        $backend->setQueryBuilder($this->createQueryBuilder());
294        $backend->setSimilarBuilder($this->createSimilarBuilder());
295        if ($this->logger) {
296            $backend->setLogger($this->logger);
297        }
298        $backend->setRecordCollectionFactory($this->createRecordCollectionFactory());
299        return $backend;
300    }
301
302    /**
303     * Create listeners.
304     *
305     * @param Backend $backend Backend
306     *
307     * @return void
308     */
309    protected function createListeners(Backend $backend)
310    {
311        $events = $this->serviceLocator->get('SharedEventManager');
312
313        // Load configurations:
314        $config = $this->config->get($this->mainConfig);
315        $search = $this->config->get($this->searchConfig);
316        $facet = $this->config->get($this->facetConfig);
317
318        // Attach default parameters listener first so that any other listeners can
319        // override the parameters as necessary:
320        if (!empty($search->General->default_parameters)) {
321            $this->getDefaultParametersListener(
322                $backend,
323                $search->General->default_parameters->toArray()
324            )->attach($events);
325        }
326
327        // Highlighting
328        $this->getInjectHighlightingListener($backend, $search)->attach($events);
329
330        // Conditional Filters
331        if (
332            isset($search->ConditionalHiddenFilters)
333            && $search->ConditionalHiddenFilters->count() > 0
334        ) {
335            $this->getInjectConditionalFilterListener($backend, $search)->attach($events);
336        }
337
338        // Spellcheck
339        if ($config->Spelling->enabled ?? true) {
340            $dictionaries = $config->Spelling->dictionaries?->toArray() ?? [];
341            if (empty($dictionaries)) {
342                // Respect the deprecated 'simple' configuration setting.
343                $dictionaries = ($config->Spelling->simple ?? false)
344                    ? ['basicSpell'] : ['default', 'basicSpell'];
345            }
346            $spellingListener = new InjectSpellingListener(
347                $backend,
348                $dictionaries,
349                $this->logger
350            );
351            $spellingListener->attach($events);
352        }
353
354        // Apply field stripping if applicable:
355        if (isset($search->StripFields) && isset($search->IndexShards)) {
356            $strip = $search->StripFields->toArray();
357            foreach ($strip as $k => $v) {
358                $strip[$k] = array_map('trim', explode(',', $v));
359            }
360            $mindexListener = new MultiIndexListener(
361                $backend,
362                $search->IndexShards->toArray(),
363                $strip,
364                $this->loadSpecs()
365            );
366            $mindexListener->attach($events);
367        }
368
369        // Apply deduplication if applicable:
370        if (isset($search->Records->deduplication)) {
371            $this->getDeduplicationListener(
372                $backend,
373                $search->Records->deduplication
374            )->attach($events);
375        }
376
377        // Attach hierarchical facet listener:
378        $this->getHierarchicalFacetListener($backend)->attach($events);
379
380        // Apply legacy filter conversion if necessary:
381        $facets = $this->config->get($this->facetConfig);
382        if (!empty($facets->LegacyFields)) {
383            $filterFieldConversionListener = new FilterFieldConversionListener(
384                $facets->LegacyFields->toArray()
385            );
386            $filterFieldConversionListener->attach($events);
387        }
388
389        // Attach custom filter listener if needed:
390        if ($cfListener = $this->getCustomFilterListener($backend, $facets)) {
391            $cfListener->attach($events);
392        }
393
394        // Attach hide facet value listener:
395        if ($hfvListener = $this->getHideFacetValueListener($backend, $facet)) {
396            $hfvListener->attach($events);
397        }
398
399        // Attach error listeners for Solr 3.x and Solr 4.x (for backward
400        // compatibility with VuFind 1.x instances).
401        $legacyErrorListener = new LegacyErrorListener($backend->getIdentifier());
402        $legacyErrorListener->attach($events);
403        $errorListener = new ErrorListener($backend->getIdentifier());
404        $errorListener->attach($events);
405    }
406
407    /**
408     * Get the name of the Solr index (core or collection).
409     *
410     * @return string
411     */
412    protected function getIndexName()
413    {
414        return $this->getIndexConfig(
415            $this->indexNameSetting,
416            $this->defaultIndexName,
417            $this->allowFallbackForIndexName
418        );
419    }
420
421    /**
422     * Get the Solr base URL(s) (without the path to the specific index)
423     *
424     * @return string[]
425     */
426    protected function getSolrBaseUrls(): array
427    {
428        $urls = $this->getIndexConfig('url', []);
429        return is_object($urls) ? $urls->toArray() : (array)$urls;
430    }
431
432    /**
433     * Get the full Solr URL(s) (including index path part).
434     *
435     * @return string|array
436     */
437    protected function getSolrUrl()
438    {
439        $indexName = $this->getIndexName();
440        $urls = array_map(
441            function ($value) use ($indexName) {
442                return "$value/$indexName";
443            },
444            $this->getSolrBaseUrls()
445        );
446        return count($urls) === 1 ? $urls[0] : $urls;
447    }
448
449    /**
450     * Get all hidden filter settings.
451     *
452     * @return array
453     */
454    protected function getHiddenFilters()
455    {
456        $search = $this->config->get($this->searchConfig);
457        $hf = [];
458
459        // Hidden filters
460        if (isset($search->HiddenFilters)) {
461            foreach ($search->HiddenFilters as $field => $value) {
462                $hf[] = sprintf('%s:"%s"', $field, $value);
463            }
464        }
465
466        // Raw hidden filters
467        if (isset($search->RawHiddenFilters)) {
468            foreach ($search->RawHiddenFilters as $filter) {
469                $hf[] = $filter;
470            }
471        }
472
473        return $hf;
474    }
475
476    /**
477     * Create the SOLR connector.
478     *
479     * @return Connector
480     */
481    protected function createConnector()
482    {
483        $timeout = $this->getIndexConfig('timeout', 30);
484        $searchConfig = $this->config->get($this->searchConfig);
485        $defaultFields = $searchConfig->General->default_record_fields ?? '*';
486
487        if (($searchConfig->Explain->enabled ?? false) && !str_contains($defaultFields, 'score')) {
488            $defaultFields .= ',score';
489        }
490
491        $handlers = [
492            'select' => [
493                'fallback' => true,
494                'defaults' => ['fl' => $defaultFields],
495                'appends'  => ['fq' => []],
496            ],
497            'terms' => [
498                'functions' => ['terms'],
499            ],
500        ];
501
502        foreach ($this->getHiddenFilters() as $filter) {
503            array_push($handlers['select']['appends']['fq'], $filter);
504        }
505
506        $connector = new $this->connectorClass(
507            $this->getSolrUrl(),
508            new HandlerMap($handlers),
509            function (string $url) use ($timeout) {
510                return $this->createHttpClient(
511                    $timeout,
512                    $this->getHttpOptions($url),
513                    $url
514                );
515            },
516            $this->uniqueKey
517        );
518
519        if ($this->logger) {
520            $connector->setLogger($this->logger);
521        }
522
523        if ($cache = $this->createConnectorCache($searchConfig)) {
524            $connector->setCache($cache);
525        }
526
527        return $connector;
528    }
529
530    /**
531     * Get HTTP options for the client
532     *
533     * @param string $url URL being requested
534     *
535     * @return array
536     *
537     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
538     */
539    protected function getHttpOptions(string $url): array
540    {
541        return [];
542    }
543
544    /**
545     * Create the query builder.
546     *
547     * @return QueryBuilder
548     */
549    protected function createQueryBuilder()
550    {
551        $specs   = $this->loadSpecs();
552        $defaultDismax = $this->getIndexConfig('default_dismax_handler', 'dismax');
553        $builder = new QueryBuilder($specs, $defaultDismax);
554
555        // Configure builder:
556        $builder->setLuceneHelper($this->createLuceneSyntaxHelper());
557
558        return $builder;
559    }
560
561    /**
562     * Create Lucene syntax helper.
563     *
564     * @return LuceneSyntaxHelper
565     */
566    protected function createLuceneSyntaxHelper()
567    {
568        $search = $this->config->get($this->searchConfig);
569        $caseSensitiveBooleans = $search->General->case_sensitive_bools ?? true;
570        $caseSensitiveRanges = $search->General->case_sensitive_ranges ?? true;
571        return new LuceneSyntaxHelper($caseSensitiveBooleans, $caseSensitiveRanges);
572    }
573
574    /**
575     * Create the similar records query builder.
576     *
577     * @return SimilarBuilder
578     */
579    protected function createSimilarBuilder()
580    {
581        return new SimilarBuilder(
582            $this->config->get($this->searchConfig),
583            $this->uniqueKey
584        );
585    }
586
587    /**
588     * Create the record collection factory.
589     *
590     * @return RecordCollectionFactoryInterface
591     */
592    protected function createRecordCollectionFactory(): RecordCollectionFactoryInterface
593    {
594        return new $this->recordCollectionFactoryClass(
595            $this->getCreateRecordCallback(),
596            $this->recordCollectionClass
597        );
598    }
599
600    /**
601     * Get the callback for creating a record.
602     *
603     * Returns a callable or null to use RecordCollectionFactory's default method.
604     *
605     * @return callable|null
606     */
607    protected function getCreateRecordCallback(): ?callable
608    {
609        return null;
610    }
611
612    /**
613     * Load the search specs.
614     *
615     * @return array
616     */
617    protected function loadSpecs()
618    {
619        return $this->serviceLocator->get(\VuFind\Config\SearchSpecsReader::class)
620            ->get($this->searchYaml);
621    }
622
623    /**
624     * Get a deduplication listener for the backend
625     *
626     * @param Backend $backend Search backend
627     * @param bool    $enabled Whether deduplication is enabled
628     *
629     * @return DeduplicationListener
630     */
631    protected function getDeduplicationListener(Backend $backend, $enabled)
632    {
633        return new DeduplicationListener(
634            $backend,
635            $this->serviceLocator,
636            $this->searchConfig,
637            'datasources',
638            $enabled
639        );
640    }
641
642    /**
643     * Get a custom filter listener for the backend (or null if not needed).
644     *
645     * @param BackendInterface $backend Search backend
646     * @param Config           $facet   Configuration of facets
647     *
648     * @return mixed null|CustomFilterListener
649     */
650    protected function getCustomFilterListener(
651        BackendInterface $backend,
652        Config $facet
653    ) {
654        $customField = $facet->CustomFilters->custom_filter_field ?? 'vufind';
655        $normal = $inverted = [];
656
657        foreach ($facet->CustomFilters->translated_filters ?? [] as $key => $val) {
658            $normal[$customField . ':"' . $key . '"'] = $val;
659        }
660        foreach ($facet->CustomFilters->inverted_filters ?? [] as $key => $val) {
661            $inverted[$customField . ':"' . $key . '"'] = $val;
662        }
663        return empty($normal) && empty($inverted)
664            ? null
665            : new CustomFilterListener($backend, $normal, $inverted);
666    }
667
668    /**
669     * Get a hierarchical facet listener for the backend
670     *
671     * @param BackendInterface $backend Search backend
672     *
673     * @return HierarchicalFacetListener
674     */
675    protected function getHierarchicalFacetListener(BackendInterface $backend)
676    {
677        return new HierarchicalFacetListener(
678            $backend,
679            $this->serviceLocator,
680            $this->facetConfig
681        );
682    }
683
684    /**
685     * Get a highlighting listener for the backend
686     *
687     * @param BackendInterface $backend Search backend
688     * @param Config           $search  Search configuration
689     *
690     * @return InjectHighlightingListener
691     */
692    protected function getInjectHighlightingListener(
693        BackendInterface $backend,
694        Config $search
695    ) {
696        $fl = $search->General->highlighting_fields ?? '*';
697        $extras = $search->General->extra_hl_params ?? [];
698        return new InjectHighlightingListener($backend, $fl, $extras);
699    }
700
701    /**
702     * Get a Conditional Filter Listener
703     *
704     * @param BackendInterface $backend Search backend
705     * @param Config           $search  Search configuration
706     *
707     * @return InjectConditionalFilterListener
708     */
709    protected function getInjectConditionalFilterListener(BackendInterface $backend, Config $search)
710    {
711        $listener = new InjectConditionalFilterListener(
712            $backend,
713            $search->ConditionalHiddenFilters->toArray()
714        );
715        $listener->setAuthorizationService(
716            $this->serviceLocator
717                ->get(\LmcRbacMvc\Service\AuthorizationService::class)
718        );
719        return $listener;
720    }
721
722    /**
723     * Get a default parameters listener for the backend
724     *
725     * @param Backend $backend Search backend
726     * @param array   $params  Default parameters
727     *
728     * @return DeduplicationListener
729     */
730    protected function getDefaultParametersListener(Backend $backend, array $params)
731    {
732        return new DefaultParametersListener($backend, $params);
733    }
734}