diff --git a/CHANGELOG.md b/CHANGELOG.md index 578aa6fc3..0bcf15c5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to Roadiz will be documented in this file. +## [2.7.2](https://github.com/roadiz/core-bundle-dev-app/compare/v2.7.1...v2.7.2) - 2026-02-23 + +### Bug Fixes + +- add priority to login reset confirmation route - ([a6bfc27](https://github.com/roadiz/core-bundle-dev-app/commit/a6bfc27429fa2cf04074471ed8469cc138da27a1)) + ## [2.7.1](https://github.com/roadiz/core-bundle-dev-app/compare/v2.7.0...v2.7.1) - 2026-02-03 ### Bug Fixes diff --git a/config/api_resources/nodes_sources.yml b/config/api_resources/nodes_sources.yml index 2fcc6829e..340619ea4 100644 --- a/config/api_resources/nodes_sources.yml +++ b/config/api_resources/nodes_sources.yml @@ -34,33 +34,33 @@ resources: description: | Get available NodesSources archives (years and months) based on their `publishedAt` field -# api_nodes_sources_search: -# class: ApiPlatform\Metadata\GetCollection -# method: 'GET' -# uriTemplate: '/nodes_sources/search' -# controller: RZ\Roadiz\SolrBundle\Controller\NodesSourcesSearchController -# read: false -# normalizationContext: -# groups: -# - get -# - nodes_sources_base -# - nodes_sources_default -# - urls -# - tag_base -# - translation_base -# - document_display -# openapi: -# summary: Search NodesSources resources -# description: | -# Search all website NodesSources resources using **Solr** full-text search engine -# parameters: -# - type: string -# name: search -# in: query -# required: true -# description: Search pattern -# schema: -# type: string + api_nodes_sources_search: + class: ApiPlatform\Metadata\GetCollection + method: 'GET' + uriTemplate: '/nodes_sources/search' + controller: RZ\Roadiz\SolrBundle\Controller\NodesSourcesSearchController + read: false + normalizationContext: + groups: + - get + - nodes_sources_base + - nodes_sources_default + - urls + - tag_base + - translation_base + - document_display + openapi: + summary: Search NodesSources resources + description: | + Search all website NodesSources resources using **Solr** full-text search engine + parameters: + - type: string + name: search + in: query + required: true + description: Search pattern + schema: + type: string # ApiPlatform\Metadata\Get: # method: 'GET' diff --git a/config/packages/roadiz_solr.yaml b/config/packages/roadiz_solr.yaml new file mode 100644 index 000000000..2df177f7e --- /dev/null +++ b/config/packages/roadiz_solr.yaml @@ -0,0 +1,4 @@ +roadiz_solr: + search: + fuzzy_proximity: 2 + fuzzy_min_term_length: 3 diff --git a/docs/developer/first-steps/use_apache_solr.md b/docs/developer/first-steps/use_apache_solr.md index cecb05ace..49c8903d8 100644 --- a/docs/developer/first-steps/use_apache_solr.md +++ b/docs/developer/first-steps/use_apache_solr.md @@ -53,10 +53,23 @@ nelmio_solarium: adapter_timeout: 5 ``` +Then configure fuzzy search options in `config/packages/roadiz_solr.yaml`: + +```yaml +# config/packages/roadiz_solr.yaml +roadiz_solr: + search: + fuzzy_proximity: 2 + fuzzy_min_term_length: 3 +``` + You can use Solr in 2 ways: as a core or as a collection: - If you are using Solr as a single core, you can set the `SOLR_CORE_NAME` environment variable. - If you are using _SolrCloud mode_, you can set the `SOLR_COLLECTION_NAME` +Fuzzy search options are configured in `roadiz_solr.search`. +For backward compatibility, `roadiz_core.solr.search` is still read as a fallback during migration. + ::: info When using _SolrCloud mode_ you will need to set the `SOLR_COLLECTION_NUM_SHARDS` and `SOLR_COLLECTION_REPLICATION_FACTOR` variables to configure your collection and execute diff --git a/docs/developer/optional-bundles/solr-bundle.md b/docs/developer/optional-bundles/solr-bundle.md index c8e7d30b2..e3fc488c4 100644 --- a/docs/developer/optional-bundles/solr-bundle.md +++ b/docs/developer/optional-bundles/solr-bundle.md @@ -68,10 +68,23 @@ nelmio_solarium: adapter_timeout: 5 ``` +Configure fuzzy search options in `config/packages/roadiz_solr.yaml`: + +```yaml +# config/packages/roadiz_solr.yaml +roadiz_solr: + search: + fuzzy_proximity: 2 + fuzzy_min_term_length: 3 +``` + ::: tip You can use Solr in 2 ways: - **Standalone core**: Set the `core` parameter to `SOLR_CORE_NAME` - **SolrCloud collection**: Set the `core` parameter to `SOLR_COLLECTION_NAME` and configure shards/replication + +Fuzzy search options are configured in `roadiz_solr.search`. +For backward compatibility, `roadiz_core.solr.search` is still read as a fallback during migration. ::: ### API Platform Integration diff --git a/lib/RoadizCoreBundle/config/packages/roadiz_core.yaml b/lib/RoadizCoreBundle/config/packages/roadiz_core.yaml index 0e51a79d2..a328a1f73 100644 --- a/lib/RoadizCoreBundle/config/packages/roadiz_core.yaml +++ b/lib/RoadizCoreBundle/config/packages/roadiz_core.yaml @@ -44,4 +44,3 @@ roadiz_core: domainName: '%env(string:VARNISH_DOMAIN)%' - diff --git a/lib/RoadizCoreBundle/config/services.yaml b/lib/RoadizCoreBundle/config/services.yaml index 958cdff60..a88a5ae71 100644 --- a/lib/RoadizCoreBundle/config/services.yaml +++ b/lib/RoadizCoreBundle/config/services.yaml @@ -1,6 +1,6 @@ --- parameters: - roadiz_core.cms_version: '2.7.1' + roadiz_core.cms_version: '2.7.2' roadiz_core.cms_version_prefix: 'main' env(APP_NAMESPACE): "roadiz" env(APP_VERSION): "0.1.0" diff --git a/lib/RoadizCoreBundle/src/Api/Model/WebResponse.php b/lib/RoadizCoreBundle/src/Api/Model/WebResponse.php index 73015b67f..d8558dce7 100644 --- a/lib/RoadizCoreBundle/src/Api/Model/WebResponse.php +++ b/lib/RoadizCoreBundle/src/Api/Model/WebResponse.php @@ -4,8 +4,6 @@ namespace RZ\Roadiz\CoreBundle\Api\Model; -use ApiPlatform\Metadata\ApiResource; - final class WebResponse implements WebResponseInterface, BlocksAwareWebResponseInterface, RealmsAwareWebResponseInterface { use WebResponseTrait; diff --git a/lib/RoadizCoreBundle/src/DependencyInjection/Configuration.php b/lib/RoadizCoreBundle/src/DependencyInjection/Configuration.php index 29fd9fefc..31f8e1cbb 100644 --- a/lib/RoadizCoreBundle/src/DependencyInjection/Configuration.php +++ b/lib/RoadizCoreBundle/src/DependencyInjection/Configuration.php @@ -227,6 +227,20 @@ protected function addSolrNode(): ArrayNodeDefinition|NodeDefinition $node->children() ->scalarNode('timeout')->defaultValue(3)->end() + ->arrayNode('search') + ->addDefaultsIfNotSet() + ->children() + ->integerNode('fuzzy_proximity') + ->defaultValue(2) + ->min(0) + ->max(2) + ->end() + ->integerNode('fuzzy_min_term_length') + ->defaultValue(3) + ->min(0) + ->end() + ->end() + ->end() ->arrayNode('endpoints') ->defaultValue([]) ->useAttributeAsKey('name') diff --git a/lib/RoadizCoreBundle/src/DependencyInjection/RoadizCoreExtension.php b/lib/RoadizCoreBundle/src/DependencyInjection/RoadizCoreExtension.php index be600fcaf..2712b554d 100644 --- a/lib/RoadizCoreBundle/src/DependencyInjection/RoadizCoreExtension.php +++ b/lib/RoadizCoreBundle/src/DependencyInjection/RoadizCoreExtension.php @@ -76,7 +76,6 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('roadiz_core.project_logo_url', $config['projectLogoUrl']); $container->setParameter('roadiz_core.generated_class_namespace', $config['generatedClassNamespace']); $container->setParameter('roadiz_core.generated_repository_namespace', $config['generatedRepositoryNamespace']); - /* * Assets config */ diff --git a/lib/RoadizCoreBundle/src/Serializer/Normalizer/NodesSourcesTypeNormalizer.php b/lib/RoadizCoreBundle/src/Serializer/Normalizer/NodesSourcesTypeNormalizer.php index 0ccf86af2..1a81f82a2 100644 --- a/lib/RoadizCoreBundle/src/Serializer/Normalizer/NodesSourcesTypeNormalizer.php +++ b/lib/RoadizCoreBundle/src/Serializer/Normalizer/NodesSourcesTypeNormalizer.php @@ -5,13 +5,9 @@ namespace RZ\Roadiz\CoreBundle\Serializer\Normalizer; use RZ\Roadiz\CoreBundle\Entity\NodesSources; -use RZ\Roadiz\CoreBundle\Serializer\Normalizer\AbstractPathNormalizer; final class NodesSourcesTypeNormalizer extends AbstractPathNormalizer { - /** - * @inheritDoc - */ public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { if (!$data instanceof NodesSources) { diff --git a/lib/RoadizRozierBundle/src/Controller/Login/LoginResetController.php b/lib/RoadizRozierBundle/src/Controller/Login/LoginResetController.php index 7b1f83a5d..e03869233 100644 --- a/lib/RoadizRozierBundle/src/Controller/Login/LoginResetController.php +++ b/lib/RoadizRozierBundle/src/Controller/Login/LoginResetController.php @@ -64,6 +64,7 @@ public function resetAction(Request $request, string $token): Response path: '/rz-admin/login/reset/confirm', name: 'loginResetConfirmPage', methods: ['GET'], + priority: 2, )] public function confirmAction(): Response { diff --git a/lib/RoadizSolrBundle/README.md b/lib/RoadizSolrBundle/README.md index 99f58f86a..a334d2251 100644 --- a/lib/RoadizSolrBundle/README.md +++ b/lib/RoadizSolrBundle/README.md @@ -115,10 +115,22 @@ nelmio_solarium: adapter_timeout: 5 ``` +Configure fuzzy search options in a dedicated `roadiz_solr` config file: +```yaml +# config/packages/roadiz_solr.yaml +roadiz_solr: + search: + fuzzy_proximity: 2 + fuzzy_min_term_length: 3 +``` + You can use Solr Cloud with a collection instead of a core by setting the `SOLR_COLLECTION_NAME` environment variable and commenting the `core` line. Then you will need to set the `SOLR_COLLECTION_NUM_SHARDS` and `SOLR_COLLECTION_REPLICATION_FACTOR` variables to configure your collection and execute `solr:init` command to create the collection. +Fuzzy search options should now be configured in `roadiz_solr.search`. +For backward compatibility, `roadiz_core.solr.search` is still read as a fallback during migration. + #### Extending Solr configuration If you want to add/remove fields and update filters you can add an event-subscriber to the `RZ\Roadiz\SolrBundle\Event\SolrInitializationEvent` event. diff --git a/lib/RoadizSolrBundle/config/packages/roadiz_solr.yaml b/lib/RoadizSolrBundle/config/packages/roadiz_solr.yaml new file mode 100644 index 000000000..2df177f7e --- /dev/null +++ b/lib/RoadizSolrBundle/config/packages/roadiz_solr.yaml @@ -0,0 +1,4 @@ +roadiz_solr: + search: + fuzzy_proximity: 2 + fuzzy_min_term_length: 3 diff --git a/lib/RoadizSolrBundle/config/services.yaml b/lib/RoadizSolrBundle/config/services.yaml index e8c6419bd..f141f95e1 100644 --- a/lib/RoadizSolrBundle/config/services.yaml +++ b/lib/RoadizSolrBundle/config/services.yaml @@ -21,6 +21,8 @@ services: $solrHostname: '%env(string:SOLR_HOST)%' $solrPort: '%env(int:SOLR_PORT)%' $solrSecure: '%env(bool:SOLR_SECURE)%' + $fuzzyProximity: '%roadiz_solr.search.fuzzy_proximity%' + $fuzzyMinTermLength: '%roadiz_solr.search.fuzzy_min_term_length%' RZ\Roadiz\SolrBundle\: resource: '../src/' diff --git a/lib/RoadizSolrBundle/src/AbstractSearchHandler.php b/lib/RoadizSolrBundle/src/AbstractSearchHandler.php index 38a70a290..6fa900d50 100644 --- a/lib/RoadizSolrBundle/src/AbstractSearchHandler.php +++ b/lib/RoadizSolrBundle/src/AbstractSearchHandler.php @@ -34,6 +34,8 @@ public function __construct( protected readonly ObjectManager $em, protected readonly LoggerInterface $searchEngineLogger, protected readonly EventDispatcherInterface $eventDispatcher, + protected readonly int $fuzzyProximity, + protected readonly int $fuzzyMinTermLength, ) { } @@ -249,10 +251,10 @@ protected function getFormattedQuery(string $q): array $fuzzyiedQuery = implode(' ', array_map(function (string $word) { /* * Do not fuzz short words: Solr crashes - * Proximity is set to 1 by default for single-words + * Proximity is configurable and can be disabled. */ - if (\mb_strlen($word) > 3) { - return $this->escapeQuery($word).'~2'; + if ($this->shouldFuzzify($word)) { + return $this->escapeQuery($word).$this->getFuzzySuffix(); } return $this->escapeQuery($word); @@ -264,7 +266,10 @@ protected function getFormattedQuery(string $q): array /* * Wildcard search for allowing autocomplete */ - $wildcardQuery = $this->escapeQuery($q).'*~2'; + $wildcardQuery = $this->escapeQuery($q).'*'; + if ($this->shouldFuzzify($q)) { + $wildcardQuery .= $this->getFuzzySuffix(); + } return [$exactQuery, $fuzzyiedQuery, $wildcardQuery]; } @@ -312,13 +317,27 @@ protected function buildHighlightingQuery(string $q): string { $q = trim($q); $words = preg_split('#[\s,]+#', $q, -1, PREG_SPLIT_NO_EMPTY); - if (\is_array($words) && \count($words) > 1) { + if (!\is_array($words) || \count($words) > 1) { return $this->escapeQuery($q); } - $q = $this->escapeQuery($q); + $escapedQuery = $this->escapeQuery($q); + if (!$this->shouldFuzzify($q)) { + return $escapedQuery; + } + + return $escapedQuery.$this->getFuzzySuffix(); + } + + private function shouldFuzzify(string $word): bool + { + return $this->fuzzyProximity > 0 + && \mb_strlen($word) >= $this->fuzzyMinTermLength; + } - return sprintf('%s~2', $q); + private function getFuzzySuffix(): string + { + return '~'.$this->fuzzyProximity; } protected function buildQueryFields(array &$args, bool $searchTags = true): string diff --git a/lib/RoadizSolrBundle/src/DependencyInjection/Configuration.php b/lib/RoadizSolrBundle/src/DependencyInjection/Configuration.php new file mode 100644 index 000000000..004d40736 --- /dev/null +++ b/lib/RoadizSolrBundle/src/DependencyInjection/Configuration.php @@ -0,0 +1,39 @@ +getRootNode(); + + $root + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('search') + ->addDefaultsIfNotSet() + ->children() + ->integerNode('fuzzy_proximity') + ->defaultValue(2) + ->min(0) + ->max(2) + ->end() + ->integerNode('fuzzy_min_term_length') + ->defaultValue(3) + ->min(0) + ->end() + ->end() + ->end() + ->end(); + + return $builder; + } +} diff --git a/lib/RoadizSolrBundle/src/DependencyInjection/RoadizSolrExtension.php b/lib/RoadizSolrBundle/src/DependencyInjection/RoadizSolrExtension.php index d9af6e86e..5c9e3392d 100644 --- a/lib/RoadizSolrBundle/src/DependencyInjection/RoadizSolrExtension.php +++ b/lib/RoadizSolrBundle/src/DependencyInjection/RoadizSolrExtension.php @@ -20,7 +20,48 @@ public function getAlias(): string #[\Override] public function load(array $configs, ContainerBuilder $container): void { + $config = $this->processConfiguration(new Configuration(), $configs); + $loader = new YamlFileLoader($container, new FileLocator(dirname(__DIR__).'/../config')); $loader->load('services.yaml'); + + $fuzzySearchConfig = $this->resolveFuzzySearchConfig($configs, $config, $container); + $container->setParameter('roadiz_solr.search.fuzzy_proximity', $fuzzySearchConfig['fuzzy_proximity']); + $container->setParameter('roadiz_solr.search.fuzzy_min_term_length', $fuzzySearchConfig['fuzzy_min_term_length']); + } + + /** + * @return array{fuzzy_proximity: mixed, fuzzy_min_term_length: mixed} + */ + private function resolveFuzzySearchConfig(array $configs, array $config, ContainerBuilder $container): array + { + $hasRoadizSolrFuzzyProximity = false; + $hasRoadizSolrFuzzyMinTermLength = false; + + foreach ($configs as $singleConfig) { + if (isset($singleConfig['search']['fuzzy_proximity'])) { + $hasRoadizSolrFuzzyProximity = true; + } + if (isset($singleConfig['search']['fuzzy_min_term_length'])) { + $hasRoadizSolrFuzzyMinTermLength = true; + } + } + + $fuzzyProximity = $hasRoadizSolrFuzzyProximity ? $config['search']['fuzzy_proximity'] : null; + $fuzzyMinTermLength = $hasRoadizSolrFuzzyMinTermLength ? $config['search']['fuzzy_min_term_length'] : null; + + foreach ($container->getExtensionConfig('roadiz_core') as $legacyConfig) { + if (null === $fuzzyProximity && isset($legacyConfig['solr']['search']['fuzzy_proximity'])) { + $fuzzyProximity = $legacyConfig['solr']['search']['fuzzy_proximity']; + } + if (null === $fuzzyMinTermLength && isset($legacyConfig['solr']['search']['fuzzy_min_term_length'])) { + $fuzzyMinTermLength = $legacyConfig['solr']['search']['fuzzy_min_term_length']; + } + } + + return [ + 'fuzzy_proximity' => $fuzzyProximity ?? $config['search']['fuzzy_proximity'], + 'fuzzy_min_term_length' => $fuzzyMinTermLength ?? $config['search']['fuzzy_min_term_length'], + ]; } }