This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_10x by this push:
new deeb7200f1d SOLR-18096: /admin/cores?action=UPGRADECOREINDEX (#3903)
deeb7200f1d is described below
commit deeb7200f1ddd68171c475fc338ef11504204a97
Author: Rahul Goswami <[email protected]>
AuthorDate: Sat Jan 31 22:36:53 2026 -0500
SOLR-18096: /admin/cores?action=UPGRADECOREINDEX (#3903)
Provides a /admin/cores api (action=UPGRADECOREINDEX) to upgrade older
index segments to the latest version (by targeted reindexing). Calling this
endpoint on an index created in version X-1 (assuming you are on Solr version
X) would reindex documents in any segments of the older version (X-1) creating
new segments, resulting in the old ones getting deleted. This would help
prepare the index for when Solr is upgraded to X+1 without having to recreate
the index from source (as is the r [...]
Various limitations apply.
---
...-CoreAdmin-API-to-upgrade-an-index-in-place.yml | 9 +
.../api/model/UpgradeCoreIndexRequestBody.java | 33 ++
.../client/api/model/UpgradeCoreIndexResponse.java | 38 ++
.../solr/handler/admin/CoreAdminOperation.java | 4 +-
.../solr/handler/admin/UpgradeCoreIndexOp.java | 82 ++++
.../solr/handler/admin/api/UpgradeCoreIndex.java | 434 +++++++++++++++++++++
.../org/apache/solr/update/DocumentBuilder.java | 30 ++
.../handler/admin/UpgradeCoreIndexActionTest.java | 380 ++++++++++++++++++
.../configuration-guide/pages/coreadmin-api.adoc | 88 +++++
.../apache/solr/common/params/CoreAdminParams.java | 3 +-
10 files changed, 1099 insertions(+), 2 deletions(-)
diff --git
a/changelog/unreleased/SOLR-18096-CoreAdmin-API-to-upgrade-an-index-in-place.yml
b/changelog/unreleased/SOLR-18096-CoreAdmin-API-to-upgrade-an-index-in-place.yml
new file mode 100644
index 00000000000..0fa7000bf59
--- /dev/null
+++
b/changelog/unreleased/SOLR-18096-CoreAdmin-API-to-upgrade-an-index-in-place.yml
@@ -0,0 +1,9 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: CoreAdmin API (/admin/cores?action=UPGRADECOREINDEX) to upgrade an
index in-place
+type: added
+authors:
+ - name: Rahul Goswami
+links:
+ - name: SOLR-18096
+ url: https://issues.apache.org/jira/browse/SOLR-18096
+
diff --git
a/solr/api/src/java/org/apache/solr/client/api/model/UpgradeCoreIndexRequestBody.java
b/solr/api/src/java/org/apache/solr/client/api/model/UpgradeCoreIndexRequestBody.java
new file mode 100644
index 00000000000..ecc3081014e
--- /dev/null
+++
b/solr/api/src/java/org/apache/solr/client/api/model/UpgradeCoreIndexRequestBody.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class UpgradeCoreIndexRequestBody {
+
+ @Schema(description = "Request ID to track this action which will be
processed asynchronously.")
+ @JsonProperty
+ public String async;
+
+ @Schema(
+ description =
+ "updateChain to be used for reindexing during index upgrade if you
don't want to use the one used by /update by default")
+ @JsonProperty
+ public String updateChain;
+}
diff --git
a/solr/api/src/java/org/apache/solr/client/api/model/UpgradeCoreIndexResponse.java
b/solr/api/src/java/org/apache/solr/client/api/model/UpgradeCoreIndexResponse.java
new file mode 100644
index 00000000000..09e0ba1e8b2
--- /dev/null
+++
b/solr/api/src/java/org/apache/solr/client/api/model/UpgradeCoreIndexResponse.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class UpgradeCoreIndexResponse extends SolrJerseyResponse {
+ @Schema(description = "The name of the core.")
+ @JsonProperty
+ public String core;
+
+ @Schema(description = "The total number of segments eligible for upgrade.")
+ @JsonProperty
+ public Integer numSegmentsEligibleForUpgrade;
+
+ @Schema(description = "The number of segments successfully upgraded.")
+ @JsonProperty
+ public Integer numSegmentsUpgraded;
+
+ @Schema(description = "Status of the core index upgrade operation.")
+ @JsonProperty
+ public String upgradeStatus;
+}
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminOperation.java
b/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminOperation.java
index 0a15462f13f..3cd2bb141e7 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminOperation.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminOperation.java
@@ -38,6 +38,7 @@ import static
org.apache.solr.common.params.CoreAdminParams.CoreAdminAction.SPLI
import static
org.apache.solr.common.params.CoreAdminParams.CoreAdminAction.STATUS;
import static
org.apache.solr.common.params.CoreAdminParams.CoreAdminAction.SWAP;
import static
org.apache.solr.common.params.CoreAdminParams.CoreAdminAction.UNLOAD;
+import static
org.apache.solr.common.params.CoreAdminParams.CoreAdminAction.UPGRADECOREINDEX;
import static org.apache.solr.handler.admin.CoreAdminHandler.CallInfo;
import java.lang.invoke.MethodHandles;
@@ -256,7 +257,8 @@ public enum CoreAdminOperation implements CoreAdminOp {
final ListCoreSnapshotsResponse response =
coreSnapshotAPI.listSnapshots(coreName);
V2ApiUtils.squashIntoSolrResponseWithoutHeader(it.rsp, response);
- });
+ }),
+ UPGRADECOREINDEX_OP(UPGRADECOREINDEX, new UpgradeCoreIndexOp());
final CoreAdminParams.CoreAdminAction action;
final CoreAdminOp fun;
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/UpgradeCoreIndexOp.java
b/solr/core/src/java/org/apache/solr/handler/admin/UpgradeCoreIndexOp.java
new file mode 100644
index 00000000000..8fff5c93d31
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/UpgradeCoreIndexOp.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.admin;
+
+import org.apache.solr.client.api.model.UpgradeCoreIndexRequestBody;
+import org.apache.solr.client.api.model.UpgradeCoreIndexResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CommonAdminParams;
+import org.apache.solr.common.params.CoreAdminParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.params.UpdateParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.api.UpgradeCoreIndex;
+import org.apache.solr.handler.api.V2ApiUtils;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+class UpgradeCoreIndexOp implements CoreAdminHandler.CoreAdminOp {
+ @FunctionalInterface
+ public interface UpgradeCoreIndexFactory {
+ UpgradeCoreIndex create(
+ CoreContainer coreContainer,
+ CoreAdminHandler.CoreAdminAsyncTracker coreAdminAsyncTracker,
+ SolrQueryRequest req,
+ SolrQueryResponse rsp);
+ }
+
+ static UpgradeCoreIndexFactory UPGRADE_CORE_INDEX_FACTORY =
UpgradeCoreIndex::new;
+
+ @Override
+ public boolean isExpensive() {
+ return true;
+ }
+
+ @Override
+ public void execute(CoreAdminHandler.CallInfo it) throws Exception {
+
+ assert it.handler.coreContainer != null;
+ if (it.handler.coreContainer.isZooKeeperAware()) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "action=UPGRADECOREINDEX is not supported in SolrCloud mode. As an
alternative, in order to upgrade index, configure
LatestVersionMergePolicyFactory in solrconfig.xml and reindex the data in your
collection.");
+ }
+
+ SolrParams params = it.req.getParams();
+ String cname = params.required().get(CoreAdminParams.CORE);
+ final boolean isAsync = params.get(CommonAdminParams.ASYNC) != null;
+ final var requestBody = new UpgradeCoreIndexRequestBody();
+ requestBody.updateChain = params.get(UpdateParams.UPDATE_CHAIN);
+
+ UpgradeCoreIndex upgradeCoreIndexApi =
+ UPGRADE_CORE_INDEX_FACTORY.create(
+ it.handler.coreContainer, it.handler.coreAdminAsyncTracker,
it.req, it.rsp);
+ final UpgradeCoreIndexResponse response =
+ upgradeCoreIndexApi.upgradeCoreIndex(cname, requestBody);
+ V2ApiUtils.squashIntoSolrResponseWithoutHeader(it.rsp, response);
+
+ if (isAsync) {
+ final var opResponse = new NamedList<>();
+ V2ApiUtils.squashIntoNamedListWithoutHeader(opResponse, response);
+ // REQUESTSTATUS is returning the inner response NamedList as a
positional array
+ // ([k1,v1,k2,v2...]).
+ // so converting to a map
+ it.rsp.addResponse(opResponse.asMap(1));
+ }
+ }
+}
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/api/UpgradeCoreIndex.java
b/solr/core/src/java/org/apache/solr/handler/admin/api/UpgradeCoreIndex.java
new file mode 100644
index 00000000000..a525b0f7595
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/UpgradeCoreIndex.java
@@ -0,0 +1,434 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.admin.api;
+
+import static org.apache.solr.common.SolrException.ErrorCode.BAD_REQUEST;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import java.util.Set;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.FilterLeafReader;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.MergePolicy;
+import org.apache.lucene.index.SegmentCommitInfo;
+import org.apache.lucene.index.SegmentReader;
+import org.apache.lucene.index.StoredFields;
+import org.apache.lucene.index.Terms;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.Bits;
+import org.apache.lucene.util.Version;
+import org.apache.solr.client.api.model.UpgradeCoreIndexRequestBody;
+import org.apache.solr.client.api.model.UpgradeCoreIndexResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.UpdateParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.DirectoryFactory;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.handler.RequestHandlerBase;
+import org.apache.solr.handler.admin.CoreAdminHandler;
+import org.apache.solr.index.LatestVersionMergePolicy;
+import org.apache.solr.request.LocalSolrQueryRequest;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.request.SolrRequestHandler;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.search.DocValuesIteratorCache;
+import org.apache.solr.search.SolrDocumentFetcher;
+import org.apache.solr.search.SolrIndexSearcher;
+import org.apache.solr.update.AddUpdateCommand;
+import org.apache.solr.update.CommitUpdateCommand;
+import org.apache.solr.update.DocumentBuilder;
+import org.apache.solr.update.processor.UpdateRequestProcessor;
+import org.apache.solr.update.processor.UpdateRequestProcessorChain;
+import org.apache.solr.util.RefCounted;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implements the UPGRADECOREINDEX CoreAdmin action, which upgrades an
existing core's index
+ * in-place by reindexing documents from segments belonging to older Lucene
versions, so that they
+ * get written into latest version segments.
+ *
+ * <p>The upgrade process:
+ *
+ * <ul>
+ * <li>Temporarily installs {@link LatestVersionMergePolicy} to prevent
older-version segments
+ * from participating in merges during reindexing.
+ * <li>Iterates each segment whose {@code minVersion} is older than the
current Lucene major
+ * version. For each live document, rebuilds a {@link SolrInputDocument}
from stored fields,
+ * decorates it with non-stored DocValues fields (excluding copyField
targets), and re-adds it
+ * through Solr's update pipeline.
+ * <li>Commits the changes and validates that no older-format segments
remain.
+ * <li>Restores the original merge policy.
+ * </ul>
+ *
+ * @see LatestVersionMergePolicy
+ * @see UpgradeCoreIndexRequestBody
+ * @see UpgradeCoreIndexResponse
+ */
+public class UpgradeCoreIndex extends CoreAdminAPIBase {
+ private static final Logger log =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ public enum CoreIndexUpgradeStatus {
+ UPGRADE_SUCCESSFUL,
+ ERROR,
+ NO_UPGRADE_NEEDED;
+ }
+
+ private static final int RETRY_COUNT_FOR_SEGMENT_DELETION = 5;
+
+ public UpgradeCoreIndex(
+ CoreContainer coreContainer,
+ CoreAdminHandler.CoreAdminAsyncTracker coreAdminAsyncTracker,
+ SolrQueryRequest req,
+ SolrQueryResponse rsp) {
+ super(coreContainer, coreAdminAsyncTracker, req, rsp);
+ }
+
+ @Override
+ public boolean isExpensive() {
+ return true;
+ }
+
+ public UpgradeCoreIndexResponse upgradeCoreIndex(
+ String coreName, UpgradeCoreIndexRequestBody requestBody) throws
Exception {
+ ensureRequiredParameterProvided("coreName", coreName);
+
+ final UpgradeCoreIndexResponse response =
+ instantiateJerseyResponse(UpgradeCoreIndexResponse.class);
+
+ return handlePotentiallyAsynchronousTask(
+ response,
+ coreName,
+ requestBody.async,
+ "upgrade-index",
+ () -> performUpgrade(coreName, requestBody, response));
+ }
+
+ private UpgradeCoreIndexResponse performUpgrade(
+ String coreName, UpgradeCoreIndexRequestBody requestBody,
UpgradeCoreIndexResponse response) {
+
+ try (SolrCore core = coreContainer.getCore(coreName)) {
+ return performUpgradeImpl(core, requestBody, response);
+ }
+ }
+
+ private UpgradeCoreIndexResponse performUpgradeImpl(
+ SolrCore core, UpgradeCoreIndexRequestBody requestBody,
UpgradeCoreIndexResponse response) {
+
+ RefCounted<IndexWriter> iwRef = null;
+ MergePolicy originalMergePolicy = null;
+ int numSegmentsEligibleForUpgrade = 0, numSegmentsUpgraded = 0;
+ String coreName = core.getName();
+ try {
+ iwRef = core.getSolrCoreState().getIndexWriter(core);
+ IndexWriter iw = iwRef.get();
+
+ RefCounted<SolrIndexSearcher> searcherRef = core.getSearcher();
+ try {
+ // Check for nested documents before processing - we don't support them
+ if (indexContainsNestedDocs(searcherRef.get())) {
+ throw new SolrException(
+ BAD_REQUEST,
+ "UPGRADECOREINDEX does not support indexes containing nested
documents. "
+ + " Consider reindexing your data "
+ + "from the original source.");
+ }
+
+ /* Set LatestVersionMergePolicy to prevent older segments from
+ participating in merges while we reindex. This is to prevent any older
version
+ segments from
+ merging with any newly formed segments created due to reindexing and
undoing the work
+ we are doing. */
+ originalMergePolicy = iw.getConfig().getMergePolicy();
+ iw.getConfig()
+ .setMergePolicy(
+ new LatestVersionMergePolicy(
+ iw.getConfig().getMergePolicy())); // prevent older
segments from merging
+
+ List<LeafReaderContext> leafContexts =
searcherRef.get().getIndexReader().leaves();
+ DocValuesIteratorCache dvICache = new
DocValuesIteratorCache(searcherRef.get());
+
+ UpdateRequestProcessorChain updateProcessorChain =
+ getUpdateProcessorChain(core, requestBody.updateChain);
+
+ for (LeafReaderContext lrc : leafContexts) {
+ if (!shouldUpgradeSegment(lrc)) {
+ continue;
+ }
+ numSegmentsEligibleForUpgrade++;
+ processSegment(lrc, updateProcessorChain, core, searcherRef.get(),
dvICache);
+ numSegmentsUpgraded++;
+ }
+
+ if (numSegmentsEligibleForUpgrade == 0) {
+ response.core = coreName;
+ response.upgradeStatus =
CoreIndexUpgradeStatus.NO_UPGRADE_NEEDED.toString();
+ response.numSegmentsEligibleForUpgrade = 0;
+ return response;
+ }
+ } catch (Exception e) {
+ log.error("Error while processing core: [{}}]", coreName, e);
+ throw new CoreAdminAPIBaseException(e);
+ } finally {
+ // important to decrement searcher ref count after use since we
obtained it via
+ // SolrCore.getSearcher()
+ searcherRef.decref();
+ }
+
+ try {
+ doCommit(core);
+ } catch (IOException e) {
+ throw new CoreAdminAPIBaseException(e);
+ }
+
+ boolean indexUpgraded = isIndexUpgraded(core);
+
+ if (!indexUpgraded) {
+ log.error(
+ "Validation failed for core '{}'. Some data is still present in
the older (<{}.x) Lucene index format.",
+ coreName,
+ Version.LATEST.major);
+ throw new CoreAdminAPIBaseException(
+ new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Validation failed for core '"
+ + coreName
+ + "'. Some data is still present in the older (<"
+ + Version.LATEST.major
+ + ".x) Lucene index format."));
+ }
+
+ response.core = coreName;
+ response.upgradeStatus =
CoreIndexUpgradeStatus.UPGRADE_SUCCESSFUL.toString();
+ response.numSegmentsEligibleForUpgrade = numSegmentsEligibleForUpgrade;
+ response.numSegmentsUpgraded = numSegmentsUpgraded;
+ } catch (Exception ioEx) {
+ // Avoid double-wrapping if already a CoreAdminAPIBaseException
+ if (ioEx instanceof CoreAdminAPIBaseException) {
+ throw (CoreAdminAPIBaseException) ioEx;
+ }
+ throw new CoreAdminAPIBaseException(ioEx);
+
+ } finally {
+ // Restore original merge policy
+ if (iwRef != null) {
+ IndexWriter iw = iwRef.get();
+ if (originalMergePolicy != null) {
+ iw.getConfig().setMergePolicy(originalMergePolicy);
+ }
+ iwRef.decref();
+ }
+ }
+
+ return response;
+ }
+
+ private boolean shouldUpgradeSegment(LeafReaderContext lrc) {
+ Version segmentMinVersion = null;
+
+ LeafReader leafReader = lrc.reader();
+ leafReader = FilterLeafReader.unwrap(leafReader);
+
+ SegmentCommitInfo si = ((SegmentReader) leafReader).getSegmentInfo();
+ segmentMinVersion = si.info.getMinVersion();
+
+ return (segmentMinVersion == null || segmentMinVersion.major <
Version.LATEST.major);
+ }
+
+ private boolean indexContainsNestedDocs(SolrIndexSearcher searcher) throws
IOException {
+ IndexSchema schema = searcher.getSchema();
+
+ // First check if schema supports nested docs
+ if (!schema.isUsableForChildDocs()) {
+ return false;
+ }
+
+ // Check if _root_ field has fewer unique values than documents with that
field.
+ // This indicates multiple docs share the same _root_ (i.e., child docs
exist)
+ IndexReader reader = searcher.getIndexReader();
+ for (LeafReaderContext leaf : reader.leaves()) {
+ Terms terms = leaf.reader().terms(IndexSchema.ROOT_FIELD_NAME);
+ if (terms != null) {
+ long uniqueRootValues = terms.size();
+ int docsWithRoot = terms.getDocCount();
+
+ if (uniqueRootValues == -1 || uniqueRootValues < docsWithRoot) {
+ return true; // Codec doesn't store number of terms (so a safe
fallback), or multiple docs
+ // share same _root_ (aka nested docs exist)
+ }
+ }
+ }
+ return false;
+ }
+
+ @SuppressWarnings({"rawtypes"})
+ private UpdateRequestProcessorChain getUpdateProcessorChain(
+ SolrCore core, String requestedUpdateChain) {
+
+ // Try explicitly requested chain first
+ if (requestedUpdateChain != null) {
+ UpdateRequestProcessorChain resolvedChain =
+ core.getUpdateProcessingChain(requestedUpdateChain);
+ if (resolvedChain != null) {
+ return resolvedChain;
+ }
+ throw new SolrException(
+ BAD_REQUEST,
+ "Requested update chain '"
+ + requestedUpdateChain
+ + "' not found for core "
+ + core.getName());
+ }
+
+ // Try to find chain configured in /update handler
+ String updateChainName = null;
+ SolrRequestHandler reqHandler = core.getRequestHandler("/update");
+
+ NamedList initArgs = ((RequestHandlerBase) reqHandler).getInitArgs();
+
+ if (initArgs != null) {
+ // Check invariants first
+ Object invariants = initArgs.get("invariants");
+ if (invariants instanceof NamedList) {
+ updateChainName = (String) ((NamedList)
invariants).get(UpdateParams.UPDATE_CHAIN);
+ }
+
+ // Check defaults if not found in invariants
+ if (updateChainName == null) {
+ Object defaults = initArgs.get("defaults");
+ if (defaults instanceof NamedList) {
+ updateChainName = (String) ((NamedList)
defaults).get(UpdateParams.UPDATE_CHAIN);
+ }
+ }
+ }
+
+ // default chain is returned if updateChainName is null
+ return core.getUpdateProcessingChain(updateChainName);
+ }
+
+ private boolean isIndexUpgraded(SolrCore core) throws IOException {
+
+ Directory dir =
+ core.getDirectoryFactory()
+ .get(
+ core.getIndexDir(),
+ DirectoryFactory.DirContext.DEFAULT,
+ core.getSolrConfig().indexConfig.lockType);
+
+ try (IndexReader reader = DirectoryReader.open(dir)) {
+ List<LeafReaderContext> leaves = reader.leaves();
+ if (leaves == null || leaves.isEmpty()) {
+ // no segments to process/validate
+ return true;
+ }
+
+ for (LeafReaderContext lrc : leaves) {
+ LeafReader leafReader = lrc.reader();
+ leafReader = FilterLeafReader.unwrap(leafReader);
+ if (leafReader instanceof SegmentReader) {
+ SegmentReader segmentReader = (SegmentReader) leafReader;
+ SegmentCommitInfo si = segmentReader.getSegmentInfo();
+ Version segMinVersion = si.info.getMinVersion();
+ if (segMinVersion == null || segMinVersion.major !=
Version.LATEST.major) {
+ log.warn(
+ "isIndexUpgraded(): Core: {}, Segment [{}] is still at
minVersion [{}] and is not updated to the latest version [{}]; numLiveDocs:
[{}]",
+ core.getName(),
+ si.info.name,
+ (segMinVersion == null ? 6 : segMinVersion.major),
+ Version.LATEST.major,
+ segmentReader.numDocs());
+ return false;
+ }
+ }
+ }
+ return true;
+ } catch (Exception e) {
+ log.error("Error while opening segmentInfos for core [{}]",
core.getName(), e);
+ throw e;
+ } finally {
+ if (dir != null) {
+ core.getDirectoryFactory().release(dir);
+ }
+ }
+ }
+
+ private void doCommit(SolrCore core) throws IOException {
+ try (LocalSolrQueryRequest req = new LocalSolrQueryRequest(core, new
ModifiableSolrParams())) {
+ CommitUpdateCommand cmd = new CommitUpdateCommand(req, false); //
optimize=false
+ core.getUpdateHandler().commit(cmd);
+ } catch (IOException ioEx) {
+ log.warn("Error committing on core [{}] during index upgrade",
core.getName(), ioEx);
+ throw ioEx;
+ }
+ }
+
+ private void processSegment(
+ LeafReaderContext leafReaderContext,
+ UpdateRequestProcessorChain processorChain,
+ SolrCore core,
+ SolrIndexSearcher solrIndexSearcher,
+ DocValuesIteratorCache dvICache)
+ throws Exception {
+
+ String coreName = core.getName();
+ IndexSchema indexSchema = core.getLatestSchema();
+
+ LeafReader leafReader = leafReaderContext.reader();
+ Bits liveDocs = leafReader.getLiveDocs();
+ SolrDocumentFetcher docFetcher = solrIndexSearcher.getDocFetcher();
+
+ // Exclude copy field targets to avoid duplicating values on reindex
+ Set<String> nonStoredDVFields =
docFetcher.getNonStoredDVsWithoutCopyTargets();
+
+ try (LocalSolrQueryRequest solrRequest =
+ new LocalSolrQueryRequest(core, new ModifiableSolrParams())) {
+ SolrQueryResponse rsp = new SolrQueryResponse();
+ UpdateRequestProcessor processor =
processorChain.createProcessor(solrRequest, rsp);
+ try {
+ StoredFields storedFields = leafReader.storedFields();
+ for (int luceneDocId = 0; luceneDocId < leafReader.maxDoc();
luceneDocId++) {
+ if (liveDocs != null && !liveDocs.get(luceneDocId)) {
+ continue;
+ }
+ Document doc = storedFields.document(luceneDocId);
+ SolrInputDocument solrDoc = DocumentBuilder.toSolrInputDocument(doc,
indexSchema);
+
+ docFetcher.decorateDocValueFields(
+ solrDoc, leafReaderContext.docBase + luceneDocId,
nonStoredDVFields, dvICache);
+
+ AddUpdateCommand currDocCmd = new AddUpdateCommand(solrRequest);
+ currDocCmd.solrDoc = solrDoc;
+ processor.processAdd(currDocCmd);
+ }
+ } finally {
+ // finish() must be called before close() to flush pending operations
+ processor.finish();
+ processor.close();
+ }
+ }
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/update/DocumentBuilder.java
b/solr/core/src/java/org/apache/solr/update/DocumentBuilder.java
index 61e1ae8d0b9..b43c4cfb81c 100644
--- a/solr/core/src/java/org/apache/solr/update/DocumentBuilder.java
+++ b/solr/core/src/java/org/apache/solr/update/DocumentBuilder.java
@@ -435,4 +435,34 @@ public class DocumentBuilder {
}
}
}
+
+ /** Convert a lucene Document to a SolrInputDocument */
+ public static SolrInputDocument toSolrInputDocument(
+ org.apache.lucene.document.Document doc, IndexSchema schema) {
+ SolrInputDocument out = new SolrInputDocument();
+ for (IndexableField f : doc.getFields()) {
+ String fname = f.name();
+ SchemaField sf = schema.getFieldOrNull(f.name());
+ Object val = null;
+ if (sf != null) {
+ if ((!sf.hasDocValues() && !sf.stored()) ||
schema.isCopyFieldTarget(sf)) {
+ continue;
+ }
+ val = sf.getType().toObject(f);
+ } else {
+ val = f.stringValue();
+ if (val == null) {
+ val = f.numericValue();
+ }
+ if (val == null) {
+ val = f.binaryValue();
+ }
+ if (val == null) {
+ val = f;
+ }
+ }
+ out.addField(fname, val);
+ }
+ return out;
+ }
}
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/UpgradeCoreIndexActionTest.java
b/solr/core/src/test/org/apache/solr/handler/admin/UpgradeCoreIndexActionTest.java
new file mode 100644
index 00000000000..7ae8c95b548
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/handler/admin/UpgradeCoreIndexActionTest.java
@@ -0,0 +1,380 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.admin;
+
+import static org.hamcrest.CoreMatchers.containsString;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.lucene.index.FilterLeafReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.SegmentInfo;
+import org.apache.lucene.index.SegmentReader;
+import org.apache.lucene.util.Version;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.params.CommonAdminParams;
+import org.apache.solr.common.params.CoreAdminParams;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.request.LocalSolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.update.AddUpdateCommand;
+import org.apache.solr.util.RefCounted;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class UpgradeCoreIndexActionTest extends SolrTestCaseJ4 {
+ private static final int DOCS_PER_SEGMENT = 3;
+ private static final String DV_FIELD = "dvonly_i_dvo";
+
+ private static VarHandle segmentInfoMinVersionHandle;
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ initCore("solrconfig-nomergepolicyfactory.xml", "schema.xml");
+ segmentInfoMinVersionHandle =
+ MethodHandles.privateLookupIn(SegmentInfo.class,
MethodHandles.lookup())
+ .findVarHandle(SegmentInfo.class, "minVersion", Version.class);
+ }
+
+ @Before
+ public void resetIndex() {
+ assertU(delQ("*:*"));
+ assertU(commit("openSearcher", "true"));
+ }
+
+ @Test
+ public void testUpgradeCoreIndexSelectiveReindexDeletesOldSegments() throws
Exception {
+ final SolrCore core = h.getCore();
+ final String coreName = core.getName();
+
+ final SegmentLayout layout = buildThreeSegments(coreName);
+ final Version simulatedOldMinVersion =
Version.fromBits(Version.LATEST.major - 1, 0, 0);
+
+ // Simulate:
+ // - seg1: "pure 9x" (minVersion=9)
+ // - seg2: "pure 10x" (minVersion=10)
+ // - seg3: "minVersion 9x, version 10x" (merged segment; minVersion=9)
+ setMinVersionForSegments(core, Set.of(layout.seg1, layout.seg3),
simulatedOldMinVersion);
+
+ final Set<String> segmentsBeforeUpgrade = listSegmentNames(core);
+
+ CoreAdminHandler admin = new CoreAdminHandler(h.getCoreContainer());
+ try {
+ final SolrQueryResponse resp = new SolrQueryResponse();
+ admin.handleRequestBody(
+ req(
+ CoreAdminParams.ACTION,
+ CoreAdminParams.CoreAdminAction.UPGRADECOREINDEX.toString(),
+ CoreAdminParams.CORE,
+ coreName),
+ resp);
+
+ assertNull("Unexpected exception: " + resp.getException(),
resp.getException());
+ assertEquals(coreName, resp.getValues().get("core"));
+ assertEquals(2, resp.getValues().get("numSegmentsEligibleForUpgrade"));
+ assertEquals(2, resp.getValues().get("numSegmentsUpgraded"));
+ assertEquals("UPGRADE_SUCCESSFUL",
resp.getValues().get("upgradeStatus"));
+ } finally {
+ admin.shutdown();
+ admin.close();
+ }
+
+ // The action commits internally and reopens the searcher; verify segments
on disk.
+ final Set<String> segmentsAfter = listSegmentNames(core);
+ final Set<String> newSegments = new HashSet<>(segmentsAfter);
+ newSegments.removeAll(segmentsBeforeUpgrade);
+ assertFalse(
+ "Expected at least one new segment to be created by reindexing",
newSegments.isEmpty());
+ assertTrue("Expected seg2 to remain", segmentsAfter.contains(layout.seg2));
+ assertFalse("Expected seg1 to be dropped",
segmentsAfter.contains(layout.seg1));
+ assertFalse("Expected seg3 to be dropped",
segmentsAfter.contains(layout.seg3));
+
+ // Searcher was reopened by the action's commit; verify document count and
field values.
+ assertQ(req("q", "*:*"), "//result[@numFound='" + (3 * DOCS_PER_SEGMENT) +
"']");
+
+ // Validate docValues-only (non-stored) fields were preserved for
reindexed documents.
+ // seg1 and seg3 were reindexed; seg2 was not.
+ assertDocValuesOnlyFieldPreserved();
+ }
+
+ @Test
+ @SuppressWarnings({"unchecked"})
+ public void
testUpgradeCoreIndexAsyncRequestStatusContainsOperationResponse() throws
Exception {
+ final SolrCore core = h.getCore();
+ final String coreName = core.getName();
+
+ final SegmentLayout layout = buildThreeSegments(coreName);
+ final Version simulatedOldMinVersion =
Version.fromBits(Version.LATEST.major - 1, 0, 0);
+ setMinVersionForSegments(core, Set.of(layout.seg1, layout.seg3),
simulatedOldMinVersion);
+
+ final Set<String> segmentsBeforeUpgrade = listSegmentNames(core);
+
+ final String requestId = "upgradecoreindex_async_1";
+ CoreAdminHandler admin = new CoreAdminHandler(h.getCoreContainer());
+ try {
+ SolrQueryResponse submitResp = new SolrQueryResponse();
+ admin.handleRequestBody(
+ req(
+ CoreAdminParams.ACTION,
+ CoreAdminParams.CoreAdminAction.UPGRADECOREINDEX.toString(),
+ CoreAdminParams.CORE,
+ coreName,
+ CommonAdminParams.ASYNC,
+ requestId),
+ submitResp);
+ assertNull(submitResp.getException());
+
+ SolrQueryResponse statusResp = new SolrQueryResponse();
+ int maxRetries = 60;
+ while (maxRetries-- > 0) {
+ statusResp = new SolrQueryResponse();
+ admin.handleRequestBody(
+ req(
+ CoreAdminParams.ACTION,
+ CoreAdminParams.CoreAdminAction.REQUESTSTATUS.toString(),
+ CoreAdminParams.REQUESTID,
+ requestId),
+ statusResp);
+
+ if ("completed".equals(statusResp.getValues().get("STATUS"))) {
+ break;
+ }
+ Thread.sleep(250);
+ }
+
+ assertEquals("completed", statusResp.getValues().get("STATUS"));
+ Object opResponse = statusResp.getValues().get("response");
+ assertNotNull(opResponse);
+ assertTrue("Expected map response, got: " + opResponse.getClass(),
opResponse instanceof Map);
+
+ Map<String, Object> opResponseMap = (Map<String, Object>) opResponse;
+ assertEquals(coreName, opResponseMap.get("core"));
+ assertEquals(2, ((Number)
opResponseMap.get("numSegmentsEligibleForUpgrade")).intValue());
+ assertEquals(2, ((Number)
opResponseMap.get("numSegmentsUpgraded")).intValue());
+ assertEquals("UPGRADE_SUCCESSFUL", opResponseMap.get("upgradeStatus"));
+ } finally {
+ admin.shutdown();
+ admin.close();
+ }
+
+ final Set<String> segmentsAfter = listSegmentNames(core);
+ final Set<String> newSegments = new HashSet<>(segmentsAfter);
+ newSegments.removeAll(segmentsBeforeUpgrade);
+ assertFalse(
+ "Expected at least one new segment to be created by reindexing",
newSegments.isEmpty());
+ assertTrue("Expected seg2 to remain", segmentsAfter.contains(layout.seg2));
+ assertFalse("Expected seg1 to be dropped",
segmentsAfter.contains(layout.seg1));
+ assertFalse("Expected seg3 to be dropped",
segmentsAfter.contains(layout.seg3));
+
+ // Validate docValues-only (non-stored) fields were preserved for
reindexed documents.
+ assertDocValuesOnlyFieldPreserved();
+ }
+
+ @Test
+ public void testNoUpgradeNeededWhenAllSegmentsCurrent() throws Exception {
+ final SolrCore core = h.getCore();
+ final String coreName = core.getName();
+
+ // Index documents and commit - all segments will be at the current Lucene
version
+ for (int i = 0; i < DOCS_PER_SEGMENT; i++) {
+ assertU(adoc("id", Integer.toString(i)));
+ }
+ assertU(commit("openSearcher", "true"));
+
+ final Set<String> segmentsBefore = listSegmentNames(core);
+ assertFalse("Expected at least one segment", segmentsBefore.isEmpty());
+
+ CoreAdminHandler admin = new CoreAdminHandler(h.getCoreContainer());
+ try {
+ final SolrQueryResponse resp = new SolrQueryResponse();
+ admin.handleRequestBody(
+ req(
+ CoreAdminParams.ACTION,
+ CoreAdminParams.CoreAdminAction.UPGRADECOREINDEX.toString(),
+ CoreAdminParams.CORE,
+ coreName),
+ resp);
+
+ assertNull("Unexpected exception: " + resp.getException(),
resp.getException());
+ assertEquals(coreName, resp.getValues().get("core"));
+ assertEquals(0, resp.getValues().get("numSegmentsEligibleForUpgrade"));
+ assertEquals("NO_UPGRADE_NEEDED", resp.getValues().get("upgradeStatus"));
+ } finally {
+ admin.shutdown();
+ admin.close();
+ }
+
+ // Verify no segments were modified
+ final Set<String> segmentsAfter = listSegmentNames(core);
+ assertEquals("Segments should remain unchanged", segmentsBefore,
segmentsAfter);
+
+ // Verify documents are still queryable
+ assertQ(req("q", "*:*"), "//result[@numFound='" + DOCS_PER_SEGMENT + "']");
+ }
+
+ private SegmentLayout buildThreeSegments(String coreName) throws Exception {
+ final SolrCore core = h.getCore();
+
+ Set<String> segmentsBefore = listSegmentNames(core);
+ indexDocs(0);
+ final String seg1 = commitAndGetNewSegment(core, segmentsBefore);
+ segmentsBefore = listSegmentNames(core);
+
+ indexDocs(1000);
+ final String seg2 = commitAndGetNewSegment(core, segmentsBefore);
+ segmentsBefore = listSegmentNames(core);
+
+ indexDocs(2000);
+ final String seg3 = commitAndGetNewSegment(core, segmentsBefore);
+
+ Set<String> allSegments = listSegmentNames(core);
+ assertTrue(allSegments.contains(seg1));
+ assertTrue(allSegments.contains(seg2));
+ assertTrue(allSegments.contains(seg3));
+
+ return new SegmentLayout(coreName, seg1, seg2, seg3);
+ }
+
+ private void indexDocs(int baseId) {
+ for (int i = 0; i < DOCS_PER_SEGMENT; i++) {
+ // schema.xml copies id into numeric fields; use numeric IDs to avoid
parsing errors
+ final String id = Integer.toString(baseId + i);
+ assertU(adoc("id", id, DV_FIELD, Integer.toString(baseId + i + 10_000),
"title", "t" + id));
+ }
+ }
+
+ private void assertDocValuesOnlyFieldPreserved() {
+ // Assert one doc that must have been reindexed (seg1) and one from seg3.
+ assertDocHasDvFieldValue(0, 10_000);
+ assertDocHasDvFieldValue(2000, 12_000);
+
+ // Also sanity-check a doc from the untouched segment (seg2) still has its
value.
+ assertDocHasDvFieldValue(1000, 11_000);
+ }
+
+ private void assertDocHasDvFieldValue(int id, int expected) {
+ assertQ(
+ req("q", "id:" + id, "fl", "id," + DV_FIELD),
+ "//result[@numFound='1']",
+ "//result/doc/int[@name='" + DV_FIELD + "'][.='" + expected + "']");
+ }
+
+ private String commitAndGetNewSegment(SolrCore core, Set<String>
segmentsBefore)
+ throws Exception {
+ assertU(commit("openSearcher", "true"));
+ Set<String> segmentsAfter = new HashSet<>(listSegmentNames(core));
+ segmentsAfter.removeAll(new HashSet<>(segmentsBefore));
+ assertEquals("Expected exactly one new segment", 1, segmentsAfter.size());
+ return segmentsAfter.iterator().next();
+ }
+
+ private Set<String> listSegmentNames(SolrCore core) throws Exception {
+ return core.withSearcher(
+ searcher -> {
+ final Set<String> segmentNames = new HashSet<>();
+ for (LeafReaderContext ctx :
searcher.getTopReaderContext().leaves()) {
+ SegmentReader segmentReader = (SegmentReader)
FilterLeafReader.unwrap(ctx.reader());
+ segmentNames.add(segmentReader.getSegmentName());
+ }
+ return segmentNames;
+ });
+ }
+
+ private void setMinVersionForSegments(SolrCore core, Set<String> segments,
Version minVersion)
+ throws Exception {
+ RefCounted<org.apache.solr.search.SolrIndexSearcher> searcherRef =
core.getSearcher();
+ try {
+ final List<LeafReaderContext> leaves =
searcherRef.get().getTopReaderContext().leaves();
+ for (LeafReaderContext ctx : leaves) {
+ SegmentReader segmentReader = (SegmentReader)
FilterLeafReader.unwrap(ctx.reader());
+ if (!segments.contains(segmentReader.getSegmentName())) {
+ continue;
+ }
+ final SegmentInfo segmentInfo = segmentReader.getSegmentInfo().info;
+ segmentInfoMinVersionHandle.set(segmentInfo, minVersion);
+ }
+ } finally {
+ searcherRef.decref();
+ }
+ }
+
+ private record SegmentLayout(String coreName, String seg1, String seg2,
String seg3) {}
+
+ @Test
+ public void testUpgradeCoreIndexFailsWithNestedDocuments() throws Exception {
+ final SolrCore core = h.getCore();
+ final String coreName = core.getName();
+
+ // Create a parent document with a child document (nested doc)
+ SolrInputDocument parentDoc = new SolrInputDocument();
+ parentDoc.addField("id", "100");
+ parentDoc.addField("title", "Parent Document");
+
+ SolrInputDocument childDoc = new SolrInputDocument();
+ childDoc.addField("id", "101");
+ childDoc.addField("title", "Child Document");
+
+ parentDoc.addChildDocument(childDoc);
+
+ // Index the nested document
+ LocalSolrQueryRequest req = new LocalSolrQueryRequest(core, new
ModifiableSolrParams());
+ try {
+ AddUpdateCommand cmd = new AddUpdateCommand(req);
+ cmd.solrDoc = parentDoc;
+ core.getUpdateHandler().addDoc(cmd);
+ } finally {
+ req.close();
+ }
+ assertU(commit("openSearcher", "true"));
+
+ // Verify documents were indexed (parent + child = 2 docs)
+ assertQ(req("q", "*:*"), "//result[@numFound='2']");
+
+ // Attempt to upgrade the index - should fail because of nested documents
+ CoreAdminHandler admin = new CoreAdminHandler(h.getCoreContainer());
+ try {
+ final SolrQueryResponse resp = new SolrQueryResponse();
+ SolrException thrown =
+ assertThrows(
+ SolrException.class,
+ () ->
+ admin.handleRequestBody(
+ req(
+ CoreAdminParams.ACTION,
+
CoreAdminParams.CoreAdminAction.UPGRADECOREINDEX.toString(),
+ CoreAdminParams.CORE,
+ coreName),
+ resp));
+
+ // Verify the exception message indicates nested documents are not
supported
+ assertThat(
+ thrown.getMessage(),
+ containsString("does not support indexes containing nested
documents"));
+ } finally {
+ admin.shutdown();
+ admin.close();
+ }
+ }
+}
diff --git
a/solr/solr-ref-guide/modules/configuration-guide/pages/coreadmin-api.adoc
b/solr/solr-ref-guide/modules/configuration-guide/pages/coreadmin-api.adoc
index 656310886cf..261caa7370f 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/coreadmin-api.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/coreadmin-api.adoc
@@ -780,6 +780,94 @@ This command is used as part of SolrCloud's
xref:deployment-guide:shard-manageme
When used against a core in a user-managed cluster without `split.key`
parameter, this action will split the source index and distribute its documents
alternately so that each split piece contains an equal number of documents.
If the `split.key` parameter is specified then only documents having the same
route key will be split from the source index.
+[[coreadmin-upgradecoreindex]]
+== UPGRADECOREINDEX
+
+The `UPGRADECOREINDEX` action upgrades an existing core's index in-place after
a Solr major-version upgrade by reindexing documents from older-format segments.
+If a core is upgraded by this action, it ensures index compatibility with the
next Solr major version (upon a future Solr upgrade) without having to
re-create the index from source.
+
+This action is expensive and can take a while to complete on large indexes.
Consider running with `async` option in such cases.
+
+Note:
+
+* Only stored fields and fields with docValues enabled can be preserved during
upgrade.
+Fields that are neither stored nor docValues-backed will lose their data,
unless they are `copyField` targets.
+* Not supported in SolrCloud mode. In order to achieve the same purpose in
SolrCloud mode (aka upgrade index for compatibility with next Solr version),
configure the `LatestVersionMergePolicyFactory` for the collection and reindex
all documents through a client utility.
+* Indexes containing child/nested documents are not supported.
+
+It is recommended to test on a copy and have a backup before running on
production data.
+
+=== UPGRADECOREINDEX Parameters
+
+`core`::
++
+[%autowidth,frame=none]
+|===
+s|Required |Default: none
+|===
++
+The name of the core whose index should be upgraded.
+
+`async`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|===
++
+Request ID to track this action which will be processed asynchronously.
+Use <<coreadmin-requeststatus,REQUESTSTATUS>> with the provided `requestid` to
poll for completion and retrieve the operation response.
+
+`update.chain`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|===
++
+The update processor chain to use for reindexing.
+If omitted, Solr uses the chain configured for the `/update` handler, or the
default update chain for the core (in that order).
+
+=== UPGRADECOREINDEX Response
+
+On success, the response includes:
+
+`core`::
+The core name.
+
+`numSegmentsEligibleForUpgrade`::
+The number of segments with an older Lucene format that were targeted.
+
+`numSegmentsUpgraded`::
+The number of segments successfully processed.
+
+`upgradeStatus`::
+One of `UPGRADE_SUCCESSFUL` or `NO_UPGRADE_NEEDED`.
+On failure, an exception is thrown with error details.
+
+=== UPGRADECOREINDEX Examples
+
+*Synchronous:*
+
+[source,bash]
+----
+http://localhost:8983/solr/admin/cores?action=UPGRADECOREINDEX&core=techproducts
+----
+
+*Asynchronous (recommended for large cores):*
+
+[source,bash]
+----
+http://localhost:8983/solr/admin/cores?action=UPGRADECOREINDEX&core=techproducts&async=upgrade_1
+----
+
+Then poll status:
+
+[source,bash]
+----
+http://localhost:8983/solr/admin/cores?action=REQUESTSTATUS&requestid=upgrade_1
+----
+
[[coreadmin-requeststatus]]
== REQUESTSTATUS
diff --git
a/solr/solrj/src/java/org/apache/solr/common/params/CoreAdminParams.java
b/solr/solrj/src/java/org/apache/solr/common/params/CoreAdminParams.java
index ea220642124..ad41867cc31 100644
--- a/solr/solrj/src/java/org/apache/solr/common/params/CoreAdminParams.java
+++ b/solr/solrj/src/java/org/apache/solr/common/params/CoreAdminParams.java
@@ -178,7 +178,8 @@ public abstract class CoreAdminParams {
INSTALLCOREDATA,
CREATESNAPSHOT,
DELETESNAPSHOT,
- LISTSNAPSHOTS;
+ LISTSNAPSHOTS,
+ UPGRADECOREINDEX;
public final boolean isRead;