From 4ee2d9c1536542a427f8a57b48bd7268755ddd32 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Wed, 2 Sep 2026 14:02:03 +0200 Subject: [PATCH 1/7] IBX-12271: Implemented Solr 10 support --- .github/init_solr.sh | 49 ++++++++++++++----- .github/workflows/integration-tests.yaml | 18 ++++++- README.md | 9 ++++ bin/generate-solr-config.sh | 24 ++++++--- .../DependencyInjection/Configuration.php | 4 +- .../CloudDistributionStrategy.php | 2 +- .../StandaloneDistributionStrategy.php | 2 +- .../config/solr/language-fieldtypes.xml | 2 +- .../solr.languages/en/language-fieldtypes.xml | 5 +- .../IbexaSolrExtensionExtensionTest.php | 24 ++++++++- 10 files changed, 111 insertions(+), 28 deletions(-) diff --git a/.github/init_solr.sh b/.github/init_solr.sh index d96451e0..e833eeb0 100755 --- a/.github/init_solr.sh +++ b/.github/init_solr.sh @@ -2,9 +2,9 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -SOLR_VERSION=${SOLR_VERSION:-'9.8.1'} +SOLR_VERSION=${SOLR_VERSION:-'9.10.1'} -if [[ "${SOLR_VERSION}" =~ ^9\. ]]; then +if [[ "${SOLR_VERSION}" =~ ^(9|10)\. ]]; then default_config_files[1]="${SCRIPT_DIR}/../src/lib/Resources/config/solr/managed-schema.xml" default_config_files[2]="${SCRIPT_DIR}/../src/lib/Resources/config/solr/custom-fields-types-solr9.xml" else @@ -42,7 +42,7 @@ SOLR_CLOUD=${SOLR_CLOUD:-'no'} INSTALL_DIR="${SOLR_DIR}/${SOLR_VERSION}" HOME_DIR="${INSTALL_DIR}/server/${SOLR_HOME}" -if [[ "${SOLR_VERSION}" =~ ^9\. ]]; then +if [[ "${SOLR_VERSION}" =~ ^(9|10)\. ]]; then TEMPLATE_DIR="${HOME_DIR}/template/conf" else TEMPLATE_DIR="${HOME_DIR}/template" @@ -60,7 +60,7 @@ fi download() { case ${SOLR_VERSION} in # PS!!: Append versions and don't remove old ones (except in major versions), used in integration tests from other packages! - 9.*) + 9.* | 10.*) url="https://archive.apache.org/dist/solr/solr/${SOLR_VERSION}/solr-${SOLR_VERSION}.tgz" ;; 7.7.* | 8.* ) @@ -155,7 +155,10 @@ solr_run() { echo "Running with version ${SOLR_VERSION} in standalone mode" echo "Starting solr on port ${SOLR_PORT}..." - if [[ "${SOLR_VERSION}" =~ ^9\. ]]; then + if [[ "${SOLR_VERSION}" =~ ^10\. ]]; then + # Solr 10: SolrCloud is the default mode, standalone requires --user-managed (SOLR-17467); '-s' was repurposed for --solr-url + ./${SOLR_INSTALL_DIR}/bin/solr start --user-managed -p "${SOLR_PORT}" --solr-home "${SOLR_HOME}" || exit_on_error "Can't start Solr" + elif [[ "${SOLR_VERSION}" =~ ^9\. ]]; then ./${SOLR_INSTALL_DIR}/bin/solr start -p ${SOLR_PORT} -s ${SOLR_HOME} || exit_on_error "Can't start Solr" else ./${SOLR_INSTALL_DIR}/bin/solr -p ${SOLR_PORT} -s ${SOLR_HOME} -Dsolr.disable.shardsWhitelist=true || exit_on_error "Can't start Solr" @@ -188,7 +191,12 @@ solr_create_core() { abs_conf_dir="$(pwd)/${config_dir}" - ./${SOLR_INSTALL_DIR}/bin/solr create_core ${solr_port_flag} -c ${core_name} -d "${abs_conf_dir}" || exit_on_error "Can't create core" + if [[ "${SOLR_VERSION}" =~ ^10\. ]]; then + # Solr 10: 'create_core' was removed in favour of 'create' addressed via --solr-url (SOLR-16893) + ./${SOLR_INSTALL_DIR}/bin/solr create -c "${core_name}" -d "${abs_conf_dir}" --solr-url "http://localhost:${SOLR_PORT}" || exit_on_error "Can't create core" + else + ./${SOLR_INSTALL_DIR}/bin/solr create_core ${solr_port_flag} -c ${core_name} -d "${abs_conf_dir}" || exit_on_error "Can't create core" + fi } solr_cloud_configure_nodes() { @@ -225,12 +233,19 @@ solr_cloud_start_nodes() { local IFS=':'; read node_name node_port <<< "${node}" local node_dir="${HOME_DIR}/${node_name}" + if [[ "${SOLR_VERSION}" =~ ^10\. ]]; then + # Solr 10: SolrCloud is the default mode ('-cloud' removed, SOLR-17467), '-s' was repurposed for --solr-url, '-V' for --verbose + local start_args=(start --solr-home "${node_dir}" -p "${node_port}" --verbose) + else + local start_args=(start -cloud -s "${node_dir}" -p "${node_port}" -V) + fi + if [[ ! ${ZOOKEEPER_HOST} ]] ; then - ${START_SCRIPT} start -cloud -s ${node_dir} -p ${node_port} -V || exit_on_error "Can't start node '${node_name}'" + ${START_SCRIPT} "${start_args[@]}" || exit_on_error "Can't start node '${node_name}'" # start script default ZOOKEEPER_HOST="localhost:$((node_port+1000))" else - ${START_SCRIPT} start -cloud -s ${node_dir} -p ${node_port} -z "${ZOOKEEPER_HOST}" -V || exit_on_error "Can't start node '${node_name}'" + ${START_SCRIPT} "${start_args[@]}" -z "${ZOOKEEPER_HOST}" || exit_on_error "Can't start node '${node_name}'" fi done } @@ -258,7 +273,8 @@ solr_cloud_configure_collection() { # modify solrconfig.xml to remove section that doesn't agree with our schema sed -i.bak '//d' ${TEMPLATE_DIR}/solrconfig.xml # Adapt autoSoftCommit to have a recommended value - sed -i.bak 's/${solr.autoSoftCommit.maxTime:-1}/${solr.autoSoftCommit.maxTime:20}/' "${TEMPLATE_DIR}/solrconfig.xml" || exit_on_error "Can't modify file '${TEMPLATE_DIR}/solrconfig.xml'" + # upstream default was '-1' up to Solr 8 and '3000' since Solr 9, so match any value + sed -i.bak 's/${solr.autoSoftCommit.maxTime:[^}]*}/${solr.autoSoftCommit.maxTime:20}/' "${TEMPLATE_DIR}/solrconfig.xml" || exit_on_error "Can't modify file '${TEMPLATE_DIR}/solrconfig.xml'" # Configure spellcheck component sed -i.bak 's/_text_<\/str>/meta_content__text_t<\/str>/' "${TEMPLATE_DIR}/solrconfig.xml" # Add spellcheck component to /select handler @@ -266,7 +282,12 @@ solr_cloud_configure_collection() { } solr_cloud_upload_collection_configuration() { - ${ZOOKEEPER_CLI_SCRIPT} -zkhost "${ZOOKEEPER_HOST}" -cmd upconfig -confname ${SOLR_CONFIGURATION_NAME} -confdir ${TEMPLATE_DIR} || exit_on_error "Can't upload configuration to Zookeeper" + if [[ "${SOLR_VERSION}" =~ ^10\. ]]; then + # Solr 10: zkcli.sh was removed in favour of 'bin/solr zk' (SOLR-14115) + ${START_SCRIPT} zk upconfig -z "${ZOOKEEPER_HOST}" -n ${SOLR_CONFIGURATION_NAME} -d ${TEMPLATE_DIR} || exit_on_error "Can't upload configuration to Zookeeper" + else + ${ZOOKEEPER_CLI_SCRIPT} -zkhost "${ZOOKEEPER_HOST}" -cmd upconfig -confname ${SOLR_CONFIGURATION_NAME} -confdir ${TEMPLATE_DIR} || exit_on_error "Can't upload configuration to Zookeeper" + fi echo "Uploaded configuration to Zookeeper" } @@ -290,7 +311,6 @@ solr_cloud_create_collection() { "collection.configName=${SOLR_CONFIGURATION_NAME}" "createNodeSet=${nodes}" "shards=${shards}" - "maxShardsPerNode=${SOLR_MAX_SHARDS_PER_NODE}" "replicationFactor=${SOLR_REPLICATION_FACTOR}" "router.name=compositeId" "numShards=${shards_count}" @@ -299,6 +319,11 @@ solr_cloud_create_collection() { "indent=on" ) + # maxShardsPerNode was removed from the Collections API in Solr 9 + if [[ ! "${SOLR_VERSION}" =~ ^(9|10)\. ]]; then + parameters+=("maxShardsPerNode=${SOLR_MAX_SHARDS_PER_NODE}") + fi + echo "Creating collection with parameters:" echo "$(IFS=$'\n'; echo "${parameters[*]}")" @@ -311,7 +336,7 @@ download if [ "$SOLR_CLOUD" = "no" ]; then - if [[ "${SOLR_VERSION}" =~ ^9\. ]]; then + if [[ "${SOLR_VERSION}" =~ ^(9|10)\. ]]; then TEMPLATE_CONF="template/conf" else TEMPLATE_CONF="template" diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index fa8c4481..fba046f5 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -17,14 +17,22 @@ jobs: php-version: - '8.3' solr-version: - - '7.7.3' - '8.11.2' - - '9.8.1' + - '9.10.1' + - '10.0.0' cores-setup: - 'dedicated' - 'shared' - 'single' - 'cloud' + # Solr requires Java 8+ (Solr 8), 11+ (Solr 9) and 21+ (Solr 10) + include: + - solr-version: '8.11.2' + java-version: '11' + - solr-version: '9.10.1' + java-version: '17' + - solr-version: '10.0.0' + java-version: '21' env: CORES_SETUP: ${{ matrix.cores-setup }} SOLR_VERSION: ${{ matrix.solr-version }} @@ -39,6 +47,12 @@ jobs: - uses: actions/checkout@v4 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ matrix.java-version }} + - name: Setup PHP Action uses: shivammathur/setup-php@v2 with: diff --git a/README.md b/README.md index 0854bbd4..61175fbc 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,15 @@ To use this package, [install Ibexa DXP](https://doc.ibexa.co/en/latest/install/ This package contains the Solr search engine implementation for [Ibexa DXP](https://ibexa.co). +## Supported Solr versions + +Solr 8, 9 and 10 are supported. Mind the Java requirements: Solr 10 requires Java 21, +Solr 9 requires Java 11 or higher (see [Solr system requirements](https://solr.apache.org/guide/solr/latest/deployment-guide/system-requirements.html)). + +When upgrading a SolrCloud cluster, note that starting with Solr 9.10 nodes refuse to join +a cluster containing higher-versioned nodes (rolling upgrades only go up, see +[Solr 9.10 upgrade notes](https://solr.apache.org/guide/solr/latest/upgrade-notes/major-changes-in-solr-9.html#solr-9-10)). + ### Testing locally To contribute to this bundle, make sure to run both unit and integration tests (from the `ezplatform-kernel` repository). diff --git a/bin/generate-solr-config.sh b/bin/generate-solr-config.sh index 1cf461ee..a45f3035 100755 --- a/bin/generate-solr-config.sh +++ b/bin/generate-solr-config.sh @@ -4,7 +4,7 @@ set -e # Default parameters, if not overloaded by user arguments DESTINATION_DIR=.platform/configsets/solr8/conf -SOLR_VERSION=8.11.1 +SOLR_VERSION=9.10.1 FORCE=false SOLR_INSTALL_DIR="" ALLOW_URLS_CLI="" @@ -21,7 +21,7 @@ Help (this text): Usage with Ibexa Cloud (arguments here can be skipped as they have default values): ./vendor/ibexa/solr/bin/generate-solr-config.sh \\ --destination-dir=.platform/configsets/solr8/conf \\ - --solr-version=8.11.1 + --solr-version=9.10.1 Usage with on-premise version of Solr: ./vendor/ibexa/solr/bin/generate-solr-config.sh \\ @@ -29,7 +29,7 @@ Usage with on-premise version of Solr: --solr-install-dir=/opt/solr Warning: - This script only supports Solr 7 and higher !! + This script only supports Solr 8 and higher (Solr 8, 9 and 10 are the supported versions) !! Arguments: @@ -82,6 +82,11 @@ done : "${ALLOW_URLS_CLI:=${ALLOW_URLS:-}}" +if [[ "${SOLR_VERSION}" =~ ^[0-7]\. ]]; then + echo -e "\033[1;31mError: Solr ${SOLR_VERSION} is not supported, use Solr 8, 9 or 10 \033[0m" + exit 1 +fi + if [ `whoami` == "root" ]; then echo "Error : Do not run this script as root" exit 1 @@ -102,7 +107,13 @@ if [ "$SOLR_INSTALL_DIR" == "" ]; then # If we were not provided an existing install directory we'll temporarily download a version of solr to generate config. GENERATE_SOLR_TMPDIR=`mktemp -d` echo "Downloading solr bundle:" - curl https://archive.apache.org/dist/lucene/solr/${SOLR_VERSION}/solr-${SOLR_VERSION}.tgz > $GENERATE_SOLR_TMPDIR/solr-${SOLR_VERSION}.tgz + # Solr 9+ releases moved from dist/lucene/solr to dist/solr/solr + if [[ "${SOLR_VERSION}" =~ ^(9|10)\. ]]; then + SOLR_ARCHIVE_URL="https://archive.apache.org/dist/solr/solr/${SOLR_VERSION}/solr-${SOLR_VERSION}.tgz" + else + SOLR_ARCHIVE_URL="https://archive.apache.org/dist/lucene/solr/${SOLR_VERSION}/solr-${SOLR_VERSION}.tgz" + fi + curl ${SOLR_ARCHIVE_URL} > $GENERATE_SOLR_TMPDIR/solr-${SOLR_VERSION}.tgz echo "Untaring" cd $GENERATE_SOLR_TMPDIR @@ -118,7 +129,7 @@ cp ${SOLR_INSTALL_DIR}/server/solr/configsets/_default/conf/{solrconfig.xml,stop if [[ ! $DESTINATION_DIR =~ ^\.platform ]]; then - if [[ "${SOLR_VERSION}" =~ ^9\. ]]; then + if [[ "${SOLR_VERSION}" =~ ^(9|10)\. ]]; then cp -f ${SOLR_INSTALL_DIR}/server/solr/solr.xml $DESTINATION_DIR/../.. URL_LIST="${ALLOW_URLS_CLI//,/ }" @@ -147,7 +158,8 @@ fi # Adapt autoSoftCommit to have a recommended value, and remove add-unknown-fields-to-the-schema sed -i.bak '//d' $DESTINATION_DIR/solrconfig.xml -sed -i.bak 's/${solr.autoSoftCommit.maxTime:-1}/${solr.autoSoftCommit.maxTime:20}/' $DESTINATION_DIR/solrconfig.xml +# upstream default was '-1' up to Solr 8 and '3000' since Solr 9, so match any value +sed -i.bak 's/${solr.autoSoftCommit.maxTime:[^}]*}/${solr.autoSoftCommit.maxTime:20}/' $DESTINATION_DIR/solrconfig.xml # Configure spellcheck component sed -i.bak 's/_text_<\/str>/meta_content__text_t<\/str>/' $DESTINATION_DIR/solrconfig.xml # Add spellcheck component to /select handler diff --git a/src/bundle/DependencyInjection/Configuration.php b/src/bundle/DependencyInjection/Configuration.php index ff0dd83e..bf3f7801 100644 --- a/src/bundle/DependencyInjection/Configuration.php +++ b/src/bundle/DependencyInjection/Configuration.php @@ -18,7 +18,7 @@ class Configuration implements ConfigurationInterface { public const int SOLR_HTTP_CLIENT_DEFAULT_TIMEOUT = 10; public const int SOLR_HTTP_CLIENT_DEFAULT_MAX_RETRIES = 3; - public const string SOLR_DEFAULT_VERSION = '7.7.3'; + public const string SOLR_DEFAULT_VERSION = '8.11.2'; /** * Holds default endpoint values. @@ -113,7 +113,7 @@ protected function addConnectionsSection(ArrayNodeDefinition $node): void { $node->children() ->scalarNode('version') - ->info('Version of the Solr Search Engine to use') + ->info('Version of the Solr Search Engine to use. Set it to match your Solr server, required when running Solr 9 or 10') ->defaultValue(self::SOLR_DEFAULT_VERSION) ->end() ->scalarNode('default_connection') diff --git a/src/lib/Gateway/DistributionStrategy/CloudDistributionStrategy.php b/src/lib/Gateway/DistributionStrategy/CloudDistributionStrategy.php index 606fc9b8..169c66b6 100644 --- a/src/lib/Gateway/DistributionStrategy/CloudDistributionStrategy.php +++ b/src/lib/Gateway/DistributionStrategy/CloudDistributionStrategy.php @@ -11,7 +11,7 @@ /** * Solr Cloud distributed search. * - * @see https://lucene.apache.org/solr/guide/7_7/distributed-requests.html + * @see https://solr.apache.org/guide/solr/latest/deployment-guide/solrcloud-distributed-requests.html */ final class CloudDistributionStrategy extends AbstractDistributionStrategy { diff --git a/src/lib/Gateway/DistributionStrategy/StandaloneDistributionStrategy.php b/src/lib/Gateway/DistributionStrategy/StandaloneDistributionStrategy.php index 3f2748ad..a012719b 100644 --- a/src/lib/Gateway/DistributionStrategy/StandaloneDistributionStrategy.php +++ b/src/lib/Gateway/DistributionStrategy/StandaloneDistributionStrategy.php @@ -11,7 +11,7 @@ /** * Standalone setup of distributed search. * - * @see https://lucene.apache.org/solr/guide/7_7/distributed-search-with-index-sharding.html + * @see https://solr.apache.org/guide/solr/latest/deployment-guide/user-managed-distributed-search.html */ final class StandaloneDistributionStrategy extends AbstractDistributionStrategy { diff --git a/src/lib/Resources/config/solr/language-fieldtypes.xml b/src/lib/Resources/config/solr/language-fieldtypes.xml index f578645b..7de61eb6 100644 --- a/src/lib/Resources/config/solr/language-fieldtypes.xml +++ b/src/lib/Resources/config/solr/language-fieldtypes.xml @@ -7,7 +7,7 @@ - + diff --git a/src/lib/Resources/config/solr/solr.languages/en/language-fieldtypes.xml b/src/lib/Resources/config/solr/solr.languages/en/language-fieldtypes.xml index 9bccba16..4738dd68 100644 --- a/src/lib/Resources/config/solr/solr.languages/en/language-fieldtypes.xml +++ b/src/lib/Resources/config/solr/solr.languages/en/language-fieldtypes.xml @@ -2,7 +2,8 @@ @@ -20,7 +21,7 @@ - + + */ + public function dataProviderForTestVersion(): array + { + return [ + 'Solr 8' => ['8.11.2'], + 'Solr 9' => ['9.10.1'], + 'Solr 10' => ['10.0.0'], + ]; + } + + /** + * @dataProvider dataProviderForTestVersion + */ + public function testVersion(string $version): void + { + $this->load(['version' => $version]); + + $this->assertContainerBuilderHasParameter('ibexa.solr.version', $version); + } + public function testConnectionEndpointDefaults(): void { $configurationValues = [ @@ -327,7 +349,7 @@ public function testConnectionEndpointDefaults(): void $this->assertContainerBuilderHasParameter( 'ibexa.solr.version', - '7.7.3' + Configuration::SOLR_DEFAULT_VERSION ); $this->assertContainerBuilderHasServiceDefinitionWithArgument( From aac6680edbb2351196e257bbd254794389979419 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Thu, 3 Sep 2026 14:25:35 +0200 Subject: [PATCH 2/7] IBX-12271: Fixed `deleteLocation` --- src/lib/Handler.php | 103 +++++++++++++++----------------------------- 1 file changed, 35 insertions(+), 68 deletions(-) diff --git a/src/lib/Handler.php b/src/lib/Handler.php index bf1da707..a843fc09 100644 --- a/src/lib/Handler.php +++ b/src/lib/Handler.php @@ -215,8 +215,41 @@ public function deleteContent($contentId, $versionId = null): void */ public function deleteLocation($locationId, $contentId): void { - $this->deleteAllItemsWithoutAdditionalLocation((int)$locationId); - $this->updateAllElementsWithAdditionalLocation((int)$locationId); + $query = $this->prepareQuery(self::SOLR_MAX_QUERY_LIMIT); + $query->filter = $this->allItemsWithinLocation((int)$locationId); + + $searchResult = $this->locationResultExtractor->extract( + $this->gateway->searchAllEndpoints($query) + ); + + // Whether an item survived the Location removal is decided by the persistence layer, not by a Solr + // query: the "has an additional Location" regex used before relied on the Lucene complement + // operator (~), which is unavailable as of Lucene 10 (LUCENE-10010). + $contentDocumentIds = []; + $contentItems = []; + foreach ($searchResult->searchHits as $searchHit) { + try { + $contentInfo = $this->contentHandler->loadContentInfo($searchHit->valueObject->id); + } catch (NotFoundException) { + $contentDocumentIds[] = $this->mapper->generateContentDocumentId((int)$searchHit->valueObject->id) . '*'; + continue; + } + + // Content moved to trash together with the subtree still exists, but has no Locations + // left in the tree (a surviving Location would have been reassigned as the main one) + if ($contentInfo->mainLocationId === null) { + $contentDocumentIds[] = $this->mapper->generateContentDocumentId((int)$contentInfo->id) . '*'; + continue; + } + + $contentItems[$contentInfo->id] = $this->contentHandler->load($contentInfo->id, $contentInfo->currentVersionNo); + } + + foreach (array_chunk(array_unique($contentDocumentIds), self::SOLR_BULK_REMOVE_LIMIT) as $ids) { + $this->gateway->deleteByQuery('_root_:(' . implode(' OR ', $ids) . ')'); + } + + $this->bulkIndexContent(array_values($contentItems)); } /** @@ -241,60 +274,6 @@ public function commit(bool $flush = false): void $this->gateway->commit($flush); } - protected function deleteAllItemsWithoutAdditionalLocation(int $locationId): void - { - $query = $this->prepareQuery(self::SOLR_MAX_QUERY_LIMIT); - $query->filter = new Criterion\LogicalAnd( - [ - $this->allItemsWithinLocation($locationId), - new Criterion\LogicalNot($this->allItemsWithinLocationWithAdditionalLocation($locationId)), - ] - ); - - $searchResult = $this->locationResultExtractor->extract( - $this->gateway->searchAllEndpoints($query) - ); - - $contentDocumentIds = []; - - foreach ($searchResult->searchHits as $hit) { - $contentDocumentIds[] = $this->mapper->generateContentDocumentId((int)$hit->valueObject->id) . '*'; - } - - foreach (array_chunk(array_unique($contentDocumentIds), self::SOLR_BULK_REMOVE_LIMIT) as $ids) { - $query = '_root_:(' . implode(' OR ', $ids) . ')'; - $this->gateway->deleteByQuery($query); - } - } - - protected function updateAllElementsWithAdditionalLocation(int $locationId): void - { - $query = $this->prepareQuery(self::SOLR_MAX_QUERY_LIMIT); - $query->filter = new Criterion\LogicalAnd( - [ - $this->allItemsWithinLocation($locationId), - $this->allItemsWithinLocationWithAdditionalLocation($locationId), - ] - ); - - $searchResult = $this->locationResultExtractor->extract( - $this->gateway->searchAllEndpoints($query) - ); - - $contentItems = []; - foreach ($searchResult->searchHits as $searchHit) { - try { - $contentInfo = $this->contentHandler->loadContentInfo($searchHit->valueObject->id); - } catch (NotFoundException) { - continue; - } - - $contentItems[] = $this->contentHandler->load($contentInfo->id, $contentInfo->currentVersionNo); - } - - $this->bulkIndexContent($contentItems); - } - /** * Prepare standard query for delete purpose. */ @@ -318,18 +297,6 @@ protected function allItemsWithinLocation(int $locationId): CustomField ); } - /** - * @param int $locationId - */ - protected function allItemsWithinLocationWithAdditionalLocation($locationId): CustomField - { - return new CustomField( - 'location_path_string_mid', - Criterion\Operator::EQ, - "/@&~(.*\\/{$locationId}\\/.*)/" - ); - } - /** * Generate search document for Content object to be indexed by a search engine. * From 30410555dbf2dc58fd7cbf6dd4292bd37ed9e08c Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Thu, 3 Sep 2026 20:24:03 +0200 Subject: [PATCH 3/7] IBX-12271: Fixed `deleteLocation` --- src/lib/Handler.php | 110 ++++++++++++++++++++++++++++++-------------- 1 file changed, 75 insertions(+), 35 deletions(-) diff --git a/src/lib/Handler.php b/src/lib/Handler.php index a843fc09..49805f62 100644 --- a/src/lib/Handler.php +++ b/src/lib/Handler.php @@ -215,41 +215,8 @@ public function deleteContent($contentId, $versionId = null): void */ public function deleteLocation($locationId, $contentId): void { - $query = $this->prepareQuery(self::SOLR_MAX_QUERY_LIMIT); - $query->filter = $this->allItemsWithinLocation((int)$locationId); - - $searchResult = $this->locationResultExtractor->extract( - $this->gateway->searchAllEndpoints($query) - ); - - // Whether an item survived the Location removal is decided by the persistence layer, not by a Solr - // query: the "has an additional Location" regex used before relied on the Lucene complement - // operator (~), which is unavailable as of Lucene 10 (LUCENE-10010). - $contentDocumentIds = []; - $contentItems = []; - foreach ($searchResult->searchHits as $searchHit) { - try { - $contentInfo = $this->contentHandler->loadContentInfo($searchHit->valueObject->id); - } catch (NotFoundException) { - $contentDocumentIds[] = $this->mapper->generateContentDocumentId((int)$searchHit->valueObject->id) . '*'; - continue; - } - - // Content moved to trash together with the subtree still exists, but has no Locations - // left in the tree (a surviving Location would have been reassigned as the main one) - if ($contentInfo->mainLocationId === null) { - $contentDocumentIds[] = $this->mapper->generateContentDocumentId((int)$contentInfo->id) . '*'; - continue; - } - - $contentItems[$contentInfo->id] = $this->contentHandler->load($contentInfo->id, $contentInfo->currentVersionNo); - } - - foreach (array_chunk(array_unique($contentDocumentIds), self::SOLR_BULK_REMOVE_LIMIT) as $ids) { - $this->gateway->deleteByQuery('_root_:(' . implode(' OR ', $ids) . ')'); - } - - $this->bulkIndexContent(array_values($contentItems)); + $this->deleteAllItemsWithoutAdditionalLocation((int)$locationId); + $this->updateAllElementsWithAdditionalLocation((int)$locationId); } /** @@ -274,6 +241,61 @@ public function commit(bool $flush = false): void $this->gateway->commit($flush); } + protected function deleteAllItemsWithoutAdditionalLocation(int $locationId): void + { + $query = $this->prepareQuery(self::SOLR_MAX_QUERY_LIMIT); + $query->filter = new Criterion\LogicalAnd( + [ + $this->allItemsWithinLocation($locationId), + new Criterion\LogicalNot($this->allItemsWithinLocationWithAdditionalLocation($locationId)), + ] + ); + + $searchResult = $this->locationResultExtractor->extract( + $this->gateway->searchAllEndpoints($query) + ); + + $contentDocumentIds = []; + + foreach ($searchResult->searchHits as $hit) { + $contentDocumentIds[] = $this->mapper->generateContentDocumentId((int)$hit->valueObject->id) . '*'; + } + + foreach (array_chunk(array_unique($contentDocumentIds), self::SOLR_BULK_REMOVE_LIMIT) as $ids) { + $query = '_root_:(' . implode(' OR ', $ids) . ')'; + $this->gateway->deleteByQuery($query); + } + } + + protected function updateAllElementsWithAdditionalLocation(int $locationId): void + { + $query = $this->prepareQuery(self::SOLR_MAX_QUERY_LIMIT); + $query->filter = new Criterion\LogicalAnd( + [ + $this->allItemsWithinLocation($locationId), + $this->allItemsWithinLocationWithAdditionalLocation($locationId), + ] + ); + + $searchResult = $this->locationResultExtractor->extract( + $this->gateway->searchAllEndpoints($query) + ); + + $contentItems = []; + foreach ($searchResult->searchHits as $searchHit) { + try { + $contentInfo = $this->contentHandler->loadContentInfo($searchHit->valueObject->id); + // load() may throw even though loadContentInfo() succeeded, as the persistence + // cache can serve a stale ContentInfo for content deleted a moment ago + $contentItems[] = $this->contentHandler->load($contentInfo->id, $contentInfo->currentVersionNo); + } catch (NotFoundException) { + continue; + } + } + + $this->bulkIndexContent($contentItems); + } + /** * Prepare standard query for delete purpose. */ @@ -297,6 +319,24 @@ protected function allItemsWithinLocation(int $locationId): CustomField ); } + /** + * Matches Content documents having at least one Location outside the given Location subtree. + * + * Locations are indexed as child documents of Content, so this is a boolean NOT on a block-join + * child query. A single complement regex on the multivalued Content path field was used before, + * but the Lucene complement operator (~) is unavailable as of Lucene 10 (LUCENE-10010). + */ + protected function allItemsWithinLocationWithAdditionalLocation(int $locationId): CustomField + { + // The CustomField visitor emits this value inside a quoted '_query_:"…"' clause, where the + // quoted-string parsing consumes one escaping level - hence the double backslash before slashes + return new CustomField( + '_query_', + Criterion\Operator::EQ, + "{!parent which=document_type_id:content}(+document_type_id:location -path_string_id:/.*\\\\/{$locationId}\\\\/.*/)" + ); + } + /** * Generate search document for Content object to be indexed by a search engine. * From be9d7bde3c7244cb12ef6320a3823a984b56f6ce Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Fri, 4 Sep 2026 14:25:38 +0200 Subject: [PATCH 4/7] IBX-12271: Applied review remarks --- .github/workflows/integration-tests.yaml | 4 ++-- README.md | 2 +- .../IbexaSolrExtensionExtensionTest.php | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index fba046f5..31ba1387 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -45,10 +45,10 @@ jobs: name: "Set up single core" run: echo "SOLR_CORES=collection1" >> $GITHUB_ENV - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: distribution: 'temurin' java-version: ${{ matrix.java-version }} diff --git a/README.md b/README.md index 61175fbc..19087a7b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This package contains the Solr search engine implementation for [Ibexa DXP](http ## Supported Solr versions -Solr 8, 9 and 10 are supported. Mind the Java requirements: Solr 10 requires Java 21, +Solr 8, 9, and 10 are supported. Mind the Java requirements: Solr 10 requires Java 21, Solr 9 requires Java 11 or higher (see [Solr system requirements](https://solr.apache.org/guide/solr/latest/deployment-guide/system-requirements.html)). When upgrading a SolrCloud cluster, note that starting with Solr 9.10 nodes refuse to join diff --git a/tests/bundle/DependencyInjection/IbexaSolrExtensionExtensionTest.php b/tests/bundle/DependencyInjection/IbexaSolrExtensionExtensionTest.php index f00fb07f..59bb209d 100644 --- a/tests/bundle/DependencyInjection/IbexaSolrExtensionExtensionTest.php +++ b/tests/bundle/DependencyInjection/IbexaSolrExtensionExtensionTest.php @@ -302,15 +302,15 @@ public function testConnection(): void } /** - * @phpstan-return array + * @return iterable */ - public function dataProviderForTestVersion(): array + public function dataProviderForTestVersion(): iterable { - return [ - 'Solr 8' => ['8.11.2'], - 'Solr 9' => ['9.10.1'], - 'Solr 10' => ['10.0.0'], - ]; + yield 'Solr 8' => ['8.11.2']; + + yield 'Solr 9' => ['9.10.1']; + + yield 'Solr 10' => ['10.0.0']; } /** From 3d5c283df5815a3625a4df22de3e5a5da1b18683 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Fri, 4 Sep 2026 15:23:34 +0200 Subject: [PATCH 5/7] IBX-12271: PHPStan baseline --- phpstan-baseline.neon | 6 ------ 1 file changed, 6 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index dd6dc748..6dd4520e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -596,12 +596,6 @@ parameters: count: 1 path: tests/lib/Search/ResultExtractor/AggregationResultExtractor/RangeAggregationResultExtractorTest.php - - - message: '#^Parameter \#2 \$entries of class Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Search\\AggregationResult\\RangeAggregationResult constructor expects iterable\\>, array\{Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Search\\AggregationResult\\RangeAggregationResultEntry\, Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Search\\AggregationResult\\RangeAggregationResultEntry\, Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Search\\AggregationResult\\RangeAggregationResultEntry\\} given\.$#' - identifier: argument.type - count: 1 - path: tests/lib/Search/ResultExtractor/AggregationResultExtractor/RangeAggregationResultExtractorTest.php - - message: '#^Parameter \#1 \$aggregation of method Ibexa\\Solr\\ResultExtractor\\AggregationResultExtractor\\TermAggregationKeyMapper\\ContentTypeAggregationKeyMapper\:\:map\(\) expects Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Query\\Aggregation\\ContentTypeTermAggregation, Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Query\\Aggregation&PHPUnit\\Framework\\MockObject\\MockObject given\.$#' identifier: argument.type From 09f2a2a78fbd885c55c55ed6b848cccb29a6140b Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Tue, 8 Sep 2026 13:55:17 +0200 Subject: [PATCH 6/7] IBX-12271: Removed other versions than Solr 10 --- .github/workflows/integration-tests.yaml | 13 +--- README.md | 6 +- bin/generate-solr-config.sh | 12 ++-- phpstan-baseline.neon | 2 +- .../DependencyInjection/Configuration.php | 4 +- .../MapLocation/MapLocationDistanceRange.php | 62 ------------------- .../container/solr/criterion_visitors.yml | 1 - .../IbexaSolrExtensionExtensionTest.php | 10 ++- 8 files changed, 19 insertions(+), 91 deletions(-) diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 31ba1387..cece122b 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -17,22 +17,15 @@ jobs: php-version: - '8.3' solr-version: - - '8.11.2' - - '9.10.1' - '10.0.0' cores-setup: - 'dedicated' - 'shared' - 'single' - 'cloud' - # Solr requires Java 8+ (Solr 8), 11+ (Solr 9) and 21+ (Solr 10) - include: - - solr-version: '8.11.2' - java-version: '11' - - solr-version: '9.10.1' - java-version: '17' - - solr-version: '10.0.0' - java-version: '21' + # Solr 10 requires Java 21+ + java-version: + - '21' env: CORES_SETUP: ${{ matrix.cores-setup }} SOLR_VERSION: ${{ matrix.solr-version }} diff --git a/README.md b/README.md index 19087a7b..dd6aa33e 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ This package contains the Solr search engine implementation for [Ibexa DXP](http ## Supported Solr versions -Solr 8, 9, and 10 are supported. Mind the Java requirements: Solr 10 requires Java 21, -Solr 9 requires Java 11 or higher (see [Solr system requirements](https://solr.apache.org/guide/solr/latest/deployment-guide/system-requirements.html)). +Solr 10 is the only supported version. It requires Java 21 +(see [Solr system requirements](https://solr.apache.org/guide/solr/latest/deployment-guide/system-requirements.html)). -When upgrading a SolrCloud cluster, note that starting with Solr 9.10 nodes refuse to join +When upgrading a SolrCloud cluster, note that nodes refuse to join a cluster containing higher-versioned nodes (rolling upgrades only go up, see [Solr 9.10 upgrade notes](https://solr.apache.org/guide/solr/latest/upgrade-notes/major-changes-in-solr-9.html#solr-9-10)). diff --git a/bin/generate-solr-config.sh b/bin/generate-solr-config.sh index a45f3035..5a260611 100755 --- a/bin/generate-solr-config.sh +++ b/bin/generate-solr-config.sh @@ -4,7 +4,7 @@ set -e # Default parameters, if not overloaded by user arguments DESTINATION_DIR=.platform/configsets/solr8/conf -SOLR_VERSION=9.10.1 +SOLR_VERSION=10.0.0 FORCE=false SOLR_INSTALL_DIR="" ALLOW_URLS_CLI="" @@ -21,7 +21,7 @@ Help (this text): Usage with Ibexa Cloud (arguments here can be skipped as they have default values): ./vendor/ibexa/solr/bin/generate-solr-config.sh \\ --destination-dir=.platform/configsets/solr8/conf \\ - --solr-version=9.10.1 + --solr-version=10.0.0 Usage with on-premise version of Solr: ./vendor/ibexa/solr/bin/generate-solr-config.sh \\ @@ -29,7 +29,7 @@ Usage with on-premise version of Solr: --solr-install-dir=/opt/solr Warning: - This script only supports Solr 8 and higher (Solr 8, 9 and 10 are the supported versions) !! + This script only supports Solr 10 !! Arguments: @@ -82,9 +82,9 @@ done : "${ALLOW_URLS_CLI:=${ALLOW_URLS:-}}" -if [[ "${SOLR_VERSION}" =~ ^[0-7]\. ]]; then - echo -e "\033[1;31mError: Solr ${SOLR_VERSION} is not supported, use Solr 8, 9 or 10 \033[0m" - exit 1 +# Solr 10 is the only supported version; older ones remain usable for internal CI (init_solr.sh) +if [[ ! "${SOLR_VERSION}" =~ ^10\. ]]; then + echo -e "\033[1;31mWarning: Solr ${SOLR_VERSION} is not supported, use Solr 10 \033[0m" fi if [ `whoami` == "root" ]; then diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6dd4520e..8486c4d2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -225,7 +225,7 @@ parameters: - message: '#^Parameter \#2 \$fieldDefinitionIdentifier of method Ibexa\\Solr\\Query\\Common\\CriterionVisitor\\MapLocation\:\:getSearchFields\(\) expects string, string\|null given\.$#' identifier: argument.type - count: 2 + count: 1 path: src/lib/Query/Common/CriterionVisitor/MapLocation/MapLocationDistanceRange.php - diff --git a/src/bundle/DependencyInjection/Configuration.php b/src/bundle/DependencyInjection/Configuration.php index bf3f7801..53a535eb 100644 --- a/src/bundle/DependencyInjection/Configuration.php +++ b/src/bundle/DependencyInjection/Configuration.php @@ -18,7 +18,7 @@ class Configuration implements ConfigurationInterface { public const int SOLR_HTTP_CLIENT_DEFAULT_TIMEOUT = 10; public const int SOLR_HTTP_CLIENT_DEFAULT_MAX_RETRIES = 3; - public const string SOLR_DEFAULT_VERSION = '8.11.2'; + public const string SOLR_DEFAULT_VERSION = '10.0.0'; /** * Holds default endpoint values. @@ -113,7 +113,7 @@ protected function addConnectionsSection(ArrayNodeDefinition $node): void { $node->children() ->scalarNode('version') - ->info('Version of the Solr Search Engine to use. Set it to match your Solr server, required when running Solr 9 or 10') + ->info('Version of the Solr Search Engine in use. Solr 10 is the only supported version') ->defaultValue(self::SOLR_DEFAULT_VERSION) ->end() ->scalarNode('default_connection') diff --git a/src/lib/Query/Common/CriterionVisitor/MapLocation/MapLocationDistanceRange.php b/src/lib/Query/Common/CriterionVisitor/MapLocation/MapLocationDistanceRange.php index 68547ca3..e2c01016 100644 --- a/src/lib/Query/Common/CriterionVisitor/MapLocation/MapLocationDistanceRange.php +++ b/src/lib/Query/Common/CriterionVisitor/MapLocation/MapLocationDistanceRange.php @@ -12,7 +12,6 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Query\CriterionInterface; use Ibexa\Contracts\Solr\Query\CriterionVisitor; use Ibexa\Core\Base\Exceptions\InvalidArgumentException; -use Ibexa\Core\Search\Common\FieldNameResolver; use Ibexa\Solr\Query\Common\CriterionVisitor\MapLocation; /** @@ -22,15 +21,6 @@ class MapLocationDistanceRange extends MapLocation { private const int MAX_EARTH_DISTANCE_KM = 63510; - public function __construct( - FieldNameResolver $fieldNameResolver, - string $fieldTypeIdentifier, - string $fieldName, - private string $solrVersion - ) { - parent::__construct($fieldNameResolver, $fieldTypeIdentifier, $fieldName); - } - /** * Check if visitor is applicable to current criterion. * @@ -53,53 +43,6 @@ public function canVisit(CriterionInterface $criterion): bool * @param \Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\MapLocationDistance $criterion */ public function visit(CriterionInterface $criterion, ?CriterionVisitor $subVisitor = null): string - { - if (!$this->isSolrInMaxVersion('9.3.0')) { - return $this->visitForSolr9($criterion); - } - $criterion->value = (array)$criterion->value; - - $start = $criterion->value[0]; - $end = $criterion->value[1] ?? self::MAX_EARTH_DISTANCE_KM; - - if (($criterion->operator === Operator::LT) || - ($criterion->operator === Operator::LTE)) { - $end = $start; - $start = null; - } - - $searchFields = $this->getSearchFields( - $criterion, - $criterion->target, - $this->fieldTypeIdentifier, - $this->fieldName - ); - - if (empty($searchFields)) { - throw new InvalidArgumentException( - '$criterion->target', - "No searchable Fields found for the provided Criterion target '{$criterion->target}'." - ); - } - - /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\Value\MapLocationValue $location */ - $location = $criterion->valueData; - - $queries = []; - foreach ($searchFields as $name => $fieldType) { - // @todo in future it should become possible to specify ranges directly on the filter (donut shape) - $query = sprintf('{!geofilt sfield=%s pt=%F,%F d=%s}', $name, $location->latitude, $location->longitude, $end); - if ($start !== null) { - $query = sprintf("{!frange l=%F}{$query}", $start); - } - - $queries[] = "{$query} AND {$name}_0_coordinate:[* TO *]"; - } - - return '(' . implode(' OR ', $queries) . ')'; - } - - private function visitForSolr9(Criterion $criterion): string { if (is_array($criterion->value)) { $minDistance = $criterion->value[0]; @@ -155,9 +98,4 @@ private function visitForSolr9(Criterion $criterion): string return '(' . implode(' OR ', $queries) . ')'; } - - private function isSolrInMaxVersion(string $maxVersion): bool - { - return version_compare($this->solrVersion, $maxVersion, '<'); - } } diff --git a/src/lib/Resources/config/container/solr/criterion_visitors.yml b/src/lib/Resources/config/container/solr/criterion_visitors.yml index 0acea194..7c07db87 100644 --- a/src/lib/Resources/config/container/solr/criterion_visitors.yml +++ b/src/lib/Resources/config/container/solr/criterion_visitors.yml @@ -115,7 +115,6 @@ services: - '@Ibexa\Core\Search\Common\FieldNameResolver' - 'ibexa_gmap_location' - 'value_location' - - '%ibexa.solr.version%' tags: - {name: ibexa.search.solr.query.content.criterion.visitor} - {name: ibexa.search.solr.query.location.criterion.visitor} diff --git a/tests/bundle/DependencyInjection/IbexaSolrExtensionExtensionTest.php b/tests/bundle/DependencyInjection/IbexaSolrExtensionExtensionTest.php index 59bb209d..c1f66f0f 100644 --- a/tests/bundle/DependencyInjection/IbexaSolrExtensionExtensionTest.php +++ b/tests/bundle/DependencyInjection/IbexaSolrExtensionExtensionTest.php @@ -234,7 +234,7 @@ public function testConnectionLoad(array $configurationValues): void public function testConnection(): void { $configurationValues = [ - 'version' => '9.8.1', + 'version' => '10.0.0', 'connections' => [ 'connection1' => [ 'entry_endpoints' => [ @@ -263,7 +263,7 @@ public function testConnection(): void $this->assertContainerBuilderHasParameter( 'ibexa.solr.version', - '9.8.1' + '10.0.0' ); $this->assertContainerBuilderHasServiceDefinitionWithArgument( @@ -306,11 +306,9 @@ public function testConnection(): void */ public function dataProviderForTestVersion(): iterable { - yield 'Solr 8' => ['8.11.2']; + yield 'default version' => [Configuration::SOLR_DEFAULT_VERSION]; - yield 'Solr 9' => ['9.10.1']; - - yield 'Solr 10' => ['10.0.0']; + yield 'custom version' => ['10.1.0']; } /** From 8ae7d25e95a7fefe8c70c312266580446d8d655a Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Wed, 9 Sep 2026 12:28:19 +0200 Subject: [PATCH 7/7] IBX-12271: Updated `generate-solr-config.sh` --- bin/generate-solr-config.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/generate-solr-config.sh b/bin/generate-solr-config.sh index 5a260611..d6427ed7 100755 --- a/bin/generate-solr-config.sh +++ b/bin/generate-solr-config.sh @@ -3,7 +3,7 @@ set -e # Default parameters, if not overloaded by user arguments -DESTINATION_DIR=.platform/configsets/solr8/conf +DESTINATION_DIR=.platform/configsets/solr10/conf SOLR_VERSION=10.0.0 FORCE=false SOLR_INSTALL_DIR="" @@ -20,7 +20,7 @@ Help (this text): Usage with Ibexa Cloud (arguments here can be skipped as they have default values): ./vendor/ibexa/solr/bin/generate-solr-config.sh \\ - --destination-dir=.platform/configsets/solr8/conf \\ + --destination-dir=.platform/configsets/solr10/conf \\ --solr-version=10.0.0 Usage with on-premise version of Solr: @@ -34,7 +34,7 @@ Warning: Arguments: [--destination-dir=] : Location where solr config should be stored - Default value is .platform/configsets/solr8/conf + Default value is .platform/configsets/solr10/conf [-f|--force] : Overwrite destination-dir if it already exists [--solr-install-dir] : Existing downloaded Solr install to copy base config from. [--solr-version] : Solr version to download & copy base config from, used only if --solr-install-dir is unset