This is an automated email from the ASF dual-hosted git repository.
epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new 0cfeeb26a6c SOLR-18450: Relocate GET /api/cluster under
/api/collections and return only the collections tree (#4952)
0cfeeb26a6c is described below
commit 0cfeeb26a6cae10d02e5ae23a7fecca47781ea1e
Author: Prithvi S <[email protected]>
AuthorDate: Tue Oct 6 22:11:55 2026 +0530
SOLR-18450: Relocate GET /api/cluster under /api/collections and return
only the collections tree (#4952)
Co-authored-by: Eric Pugh <[email protected]>
---
.../SOLR-18450-v2-collections-detailed.yml | 14 ++
.../client/api/endpoint/ListCollectionsApi.java | 36 +++-
.../client/api/model/ListCollectionsResponse.java | 100 +++++++++
.../java/org/apache/solr/handler/ClusterAPI.java | 9 -
.../apache/solr/handler/admin/ClusterStatus.java | 127 +++++++----
.../solr/handler/admin/CollectionsHandler.java | 3 +-
.../solr/handler/admin/api/ListCollections.java | 48 ++++-
.../solr/handler/V2ClusterAPIMappingTest.java | 8 -
.../api/ListCollectionsDetailedStandaloneTest.java | 45 ++++
.../admin/api/ListCollectionsDetailedTest.java | 236 +++++++++++++++++++++
.../modules/configuration-guide/pages/v2-api.adoc | 10 +-
.../pages/cluster-node-management.adoc | 25 +--
.../pages/collection-management.adoc | 200 +++++++++++++++++
.../pages/major-changes-in-solr-11.adoc | 12 ++
solr/webapp/web/js/angular/app.js | 2 +-
.../web/js/angular/controllers/alias-overview.js | 2 +-
solr/webapp/web/js/angular/controllers/cloud.js | 124 +++++------
.../js/angular/controllers/collection-overview.js | 13 +-
.../web/js/angular/controllers/collections.js | 11 +-
solr/webapp/web/js/angular/services.js | 14 +-
20 files changed, 875 insertions(+), 164 deletions(-)
diff --git a/changelog/unreleased/SOLR-18450-v2-collections-detailed.yml
b/changelog/unreleased/SOLR-18450-v2-collections-detailed.yml
new file mode 100644
index 00000000000..3406f380b72
--- /dev/null
+++ b/changelog/unreleased/SOLR-18450-v2-collections-detailed.yml
@@ -0,0 +1,14 @@
+title:
+ V2 GET /api/collections gains a `detailed=true` param returning the
collections, shards, and
+ replicas tree (in `collectionsDetail`), without live nodes, aliases, or
cluster properties. This
+ tree previously lived at GET /api/cluster in an earlier draft of this
change; it was relocated to
+ /api/collections since it's collection data, not cluster-wide data --
`/api/cluster/nodes`,
+ `/api/aliases`, and `/api/cluster/properties` already cover the cluster-wide
pieces.
+type: changed
+authors:
+ - name: Prithvi S
+ nick: iprithv
+ - name: Eric Pugh
+links:
+ - name: SOLR-18450
+ url: https://issues.apache.org/jira/browse/SOLR-18450
diff --git
a/solr/api/src/java/org/apache/solr/client/api/endpoint/ListCollectionsApi.java
b/solr/api/src/java/org/apache/solr/client/api/endpoint/ListCollectionsApi.java
index 2f20f4b4366..468b79d52bf 100644
---
a/solr/api/src/java/org/apache/solr/client/api/endpoint/ListCollectionsApi.java
+++
b/solr/api/src/java/org/apache/solr/client/api/endpoint/ListCollectionsApi.java
@@ -17,8 +17,10 @@
package org.apache.solr.client.api.endpoint;
import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
+import jakarta.ws.rs.QueryParam;
import org.apache.solr.client.api.model.ListCollectionsResponse;
@Path("/collections")
@@ -27,5 +29,37 @@ public interface ListCollectionsApi {
@Operation(
summary = "List all collections in this Solr cluster",
tags = {"collections"})
- ListCollectionsResponse listCollections();
+ ListCollectionsResponse listCollections(
+ @Parameter(
+ description =
+ "When true, return the collections, shards, and replicas
tree (in"
+ + " 'collectionsDetail') instead of the plain collection
name list.")
+ @QueryParam("detailed")
+ Boolean detailed,
+ @Parameter(
+ description =
+ "Only used when 'detailed' is true. Collection or alias to
return. Omit to"
+ + " return every collection. An alias returns the
collections it points"
+ + " at.")
+ @QueryParam("collection")
+ String collection,
+ @Parameter(
+ description =
+ "Only used when 'detailed' is true. Shard or comma-separated
shards to return."
+ + " Applied to each selected collection.")
+ @QueryParam("shard")
+ String shard,
+ @Parameter(
+ description =
+ "Only used when 'detailed' is true. Route key of a document.
Limits the tree to"
+ + " the shard that would hold that document.")
+ @QueryParam("_route_")
+ String routeKey,
+ @Parameter(
+ description =
+ "Only used when 'detailed' is true. Include per-replica
state when the"
+ + " collection uses it.")
+ @QueryParam("prs")
+ Boolean prs)
+ throws Exception;
}
diff --git
a/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionsResponse.java
b/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionsResponse.java
index 17bf44c44ec..996b6e70729 100644
---
a/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionsResponse.java
+++
b/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionsResponse.java
@@ -16,10 +16,110 @@
*/
package org.apache.solr.client.api.model;
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
public class ListCollectionsResponse extends SolrJerseyResponse {
@JsonProperty("collections")
public List<String> collections;
+
+ @Schema(description = "Collections, shards, and replicas, present only when
detailed=true.")
+ @JsonProperty("collectionsDetail")
+ public Map<String, CollectionState> collectionsDetail;
+
+ /** State of one collection, including its shards and replicas. */
+ public static class CollectionState {
+ @JsonProperty public Map<String, ShardState> shards;
+
+ @Schema(description = "Worst shard health in this collection: GREEN,
YELLOW, ORANGE, or RED.")
+ @JsonProperty
+ public String health;
+
+ @JsonProperty public String configName;
+ @JsonProperty public Integer znodeVersion;
+ @JsonProperty public Long creationTimeMillis;
+
+ @Schema(description = "Aliases that point at this collection.")
+ @JsonProperty
+ public List<String> aliases;
+
+ @Schema(description = "Document router for this collection, e.g.
name=compositeId.")
+ @JsonProperty
+ public Map<String, String> router;
+
+ @JsonProperty public Integer replicationFactor;
+
+ private final Map<String, Object> additionalProperties = new
LinkedHashMap<>();
+
+ @JsonAnyGetter
+ public Map<String, Object> unknownProperties() {
+ return additionalProperties;
+ }
+
+ @JsonAnySetter
+ public void setUnknownProperty(String field, Object value) {
+ additionalProperties.put(field, value);
+ }
+ }
+
+ /** State of one shard. */
+ public static class ShardState {
+ @JsonProperty public String state;
+ @JsonProperty public String range;
+
+ @Schema(description = "Shard health: GREEN, YELLOW, ORANGE, or RED.")
+ @JsonProperty
+ public String health;
+
+ @JsonProperty public Map<String, ReplicaState> replicas;
+
+ private final Map<String, Object> additionalProperties = new
LinkedHashMap<>();
+
+ @JsonAnyGetter
+ public Map<String, Object> unknownProperties() {
+ return additionalProperties;
+ }
+
+ @JsonAnySetter
+ public void setUnknownProperty(String field, Object value) {
+ additionalProperties.put(field, value);
+ }
+ }
+
+ /** State of one replica. */
+ public static class ReplicaState {
+ @JsonProperty public String state;
+ @JsonProperty public String core;
+
+ @JsonProperty("node_name")
+ public String nodeName;
+
+ @JsonProperty("base_url")
+ public String baseUrl;
+
+ /**
+ * {@code "true"} when this replica is the leader. Absent otherwise.
state.json stores this as a
+ * string.
+ */
+ @JsonProperty public String leader;
+
+ @JsonProperty public String type;
+
+ private final Map<String, Object> additionalProperties = new
LinkedHashMap<>();
+
+ @JsonAnyGetter
+ public Map<String, Object> unknownProperties() {
+ return additionalProperties;
+ }
+
+ @JsonAnySetter
+ public void setUnknownProperty(String field, Object value) {
+ additionalProperties.put(field, value);
+ }
+ }
}
diff --git a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
index d292cb4a05f..33b7bd41e53 100644
--- a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
@@ -38,8 +38,6 @@ import
org.apache.solr.client.solrj.request.beans.RateLimiterPayload;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.ClusterProperties;
import org.apache.solr.common.cloud.ZkStateReader;
-import org.apache.solr.common.params.CollectionParams.CollectionAction;
-import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.DefaultSolrParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.Utils;
@@ -210,13 +208,6 @@ public class ClusterAPI {
return req;
}
- @EndPoint(method = GET, path = "/cluster", permission = COLL_READ_PERM)
- public void getClusterStatus(SolrQueryRequest req, SolrQueryResponse rsp)
throws Exception {
- final Map<String, Object> v1Params =
- Map.of(CommonParams.ACTION, CollectionAction.CLUSTERSTATUS.toLower());
- collectionsHandler.handleRequestBody(wrapParams(req, v1Params), rsp);
- }
-
private CoreContainer getCoreContainer() {
return collectionsHandler.getCoreContainer();
}
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
b/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
index f7f20662506..1bd3e882a18 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
@@ -20,10 +20,12 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.function.BiConsumer;
import java.util.stream.Stream;
import org.apache.solr.client.api.util.SolrVersion;
import org.apache.solr.common.MapWriter;
@@ -112,9 +114,10 @@ public class ClusterStatus {
boolean withAliases = solrParams.getBool(ALIASES_PROP, includeAll);
List<String> liveNodes = null;
- if (withLiveNodes || collection != null) {
+ // Replica state is cross-checked against live nodes whenever the
collection tree is built,
+ // including when the caller asked for that tree without the live-node
list itself.
+ if (withLiveNodes || withCollection) {
liveNodes =
zkStateReader.getZkClient().getChildren(ZkStateReader.LIVE_NODES_ZKNODE, null);
- // add live_nodes
if (withLiveNodes) clusterStatus.add("live_nodes", liveNodes);
}
@@ -125,7 +128,7 @@ public class ClusterStatus {
if (withCollection) {
assert liveNodes != null;
- fetchClusterStatusForCollOrAlias(clusterStatus, liveNodes, aliases,
solrVersion);
+ addCollectionStatuses(clusterStatus, liveNodes, aliases, solrVersion);
}
if (withAliases) {
@@ -143,13 +146,49 @@ public class ClusterStatus {
results.add("cluster", clusterStatus);
}
- private void fetchClusterStatusForCollOrAlias(
+ /**
+ * Collections, shards, and replicas selected by this request's {@code
collection}, {@code shard},
+ * and {@code _route_} parameters.
+ *
+ * <p>Live nodes are read so replica state can be cross-checked, and aliases
are read so a
+ * collection alias in {@code collection} can be resolved. Each collection
still lists the aliases
+ * that point at it. The live-node list, the alias map, and cluster
properties are not returned.
+ */
+ public Map<String, Object> getCollectionStatuses() throws KeeperException,
InterruptedException {
+ List<String> liveNodes =
+
zkStateReader.getZkClient().getChildren(ZkStateReader.LIVE_NODES_ZKNODE, null);
+ Aliases aliases = zkStateReader.getAliases();
+ Map<String, Object> collections = new LinkedHashMap<>();
+ forEachCollectionStatus(
+ liveNodes, aliases, (name, status) -> collections.put(name,
copyForJackson(status)));
+ return collections;
+ }
+
+ private void addCollectionStatuses(
NamedList<Object> clusterStatus,
List<String> liveNodes,
Aliases aliases,
SolrVersion solrVersion) {
+ // Resolve the collection list now so a missing name fails the request,
rather than during
+ // response writing. The per-collection JSON is still built while the
response is written.
+ PreparedCollections prepared = prepareCollections(aliases);
+ if (solrVersion == null ||
solrVersion.greaterThanOrEqualTo(SolrVersion.valueOf("9.9.0"))) {
+ MapWriter collectionPropsWriter =
+ ew -> prepared.forEach(liveNodes, (name, status) -> ew.putNoEx(name,
status));
+ clusterStatus.add("collections", collectionPropsWriter);
+ } else {
+ NamedList<Object> collectionProps = new SimpleOrderedMap<>();
+ prepared.forEach(liveNodes, collectionProps::add);
+ clusterStatus.add("collections", collectionProps);
+ }
+ }
+
+ private void forEachCollectionStatus(
+ List<String> liveNodes, Aliases aliases, BiConsumer<String, Map<String,
Object>> consumer) {
+ prepareCollections(aliases).forEach(liveNodes, consumer);
+ }
- // read aliases
+ private PreparedCollections prepareCollections(Aliases aliases) {
Map<String, List<String>> collectionVsAliases = new HashMap<>();
Map<String, List<String>> aliasVsCollections =
aliases.getCollectionAliasListMap();
for (Map.Entry<String, List<String>> entry :
aliasVsCollections.entrySet()) {
@@ -164,12 +203,6 @@ public class ClusterStatus {
}
ClusterState clusterState = zkStateReader.getClusterState();
-
- String routeKey = solrParams.get(ShardParams._ROUTE_);
- String shard = solrParams.get(ZkStateReader.SHARD_ID_PROP);
-
- Set<String> requestedShards = (shard != null) ? Set.of(shard.split(",")) :
null;
-
Stream<DocCollection> collectionStream;
if (collection == null) {
collectionStream = clusterState.collectionStream();
@@ -195,33 +228,53 @@ public class ClusterStatus {
}
}
- if (solrVersion == null ||
solrVersion.greaterThanOrEqualTo(SolrVersion.valueOf("9.9.0"))) {
- MapWriter collectionPropsWriter =
- ew -> {
- collectionStream.forEach(
- (collectionState) -> {
- ew.putNoEx(
- collectionState.getName(),
- buildResponseForCollection(
- collectionState,
- collectionVsAliases,
- routeKey,
- liveNodes,
- requestedShards));
- });
- };
- clusterStatus.add("collections", collectionPropsWriter);
- } else {
- NamedList<Object> collectionProps = new SimpleOrderedMap<>();
- collectionStream.forEach(
- collectionState -> {
- collectionProps.add(
- collectionState.getName(),
- buildResponseForCollection(
- collectionState, collectionVsAliases, routeKey, liveNodes,
requestedShards));
- });
- clusterStatus.add("collections", collectionProps);
+ String routeKey = solrParams.get(ShardParams._ROUTE_);
+ String shard = solrParams.get(ZkStateReader.SHARD_ID_PROP);
+ Set<String> requestedShards = (shard != null) ? Set.of(shard.split(",")) :
null;
+ return new PreparedCollections(
+ collectionStream.toList(), collectionVsAliases, routeKey,
requestedShards);
+ }
+
+ private final class PreparedCollections {
+ private final List<DocCollection> collections;
+ private final Map<String, List<String>> collectionVsAliases;
+ private final String routeKey;
+ private final Set<String> requestedShards;
+
+ private PreparedCollections(
+ List<DocCollection> collections,
+ Map<String, List<String>> collectionVsAliases,
+ String routeKey,
+ Set<String> requestedShards) {
+ this.collections = collections;
+ this.collectionVsAliases = collectionVsAliases;
+ this.routeKey = routeKey;
+ this.requestedShards = requestedShards;
+ }
+
+ private void forEach(List<String> liveNodes, BiConsumer<String,
Map<String, Object>> consumer) {
+ for (DocCollection collectionState : collections) {
+ consumer.accept(
+ collectionState.getName(),
+ buildResponseForCollection(
+ collectionState, collectionVsAliases, routeKey, liveNodes,
requestedShards));
+ }
+ }
+ }
+
+ /**
+ * Jersey serializes the v2 response with Jackson, which does not write Solr
{@link MapWriter}s.
+ * Per-replica state is the one value still attached as a writer; turn it
into plain JSON objects.
+ * Other fields are already maps and scalars.
+ */
+ private static Map<String, Object> copyForJackson(Map<String, Object>
status) {
+ Object prs = status.get("PRS");
+ if (!(prs instanceof MapWriter)) {
+ return status;
}
+ Map<String, Object> copy = new LinkedHashMap<>(status);
+ copy.put("PRS", Utils.fromJSON(Utils.toJSON(prs)));
+ return copy;
}
private void addAliasMap(Aliases aliases, NamedList<Object> clusterStatus) {
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
index 9b8662176d6..34f4be920cf 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
@@ -782,7 +782,8 @@ public class CollectionsHandler extends RequestHandlerBase
implements Permission
LIST,
(req, rsp, h) -> {
final ListCollections listCollectionsAPI = new
ListCollections(h.coreContainer, req, rsp);
- final SolrJerseyResponse listCollectionsResponse =
listCollectionsAPI.listCollections();
+ final SolrJerseyResponse listCollectionsResponse =
+ listCollectionsAPI.listCollections(null, null, null, null, null);
V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp,
listCollectionsResponse);
return null;
}),
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollections.java
b/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollections.java
index 81c5fac32c5..831e5929b25 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollections.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollections.java
@@ -21,11 +21,18 @@ import static
org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PER
import jakarta.inject.Inject;
import java.util.List;
+import java.util.Map;
import org.apache.solr.client.api.endpoint.ListCollectionsApi;
import org.apache.solr.client.api.model.ListCollectionsResponse;
+import
org.apache.solr.client.api.model.ListCollectionsResponse.CollectionState;
import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.util.CollectionUtil;
import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.ClusterStatus;
import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJacksonMapper;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
@@ -33,6 +40,12 @@ import org.apache.solr.response.SolrQueryResponse;
* V2 API for listing collections.
*
* <p>This API (GET /v2/collections) is equivalent to the v1
/admin/collections?action=LIST command
+ *
+ * <p>With {@code detailed=true}, it instead returns the collections, shards,
and replicas tree (in
+ * {@code collectionsDetail}) -- the same tree v1 {@code CLUSTERSTATUS}
returns under {@code
+ * cluster.collections}, without the live-node list, alias map, or cluster
properties that accompany
+ * it there. Live nodes, aliases, and cluster properties have their own v2
endpoints: {@code GET
+ * /api/cluster/nodes}, {@code GET /api/aliases}, and {@code GET
/api/cluster/properties}.
*/
public class ListCollections extends AdminAPIBase implements
ListCollectionsApi {
@@ -43,11 +56,31 @@ public class ListCollections extends AdminAPIBase
implements ListCollectionsApi
@Override
@PermissionName(COLL_READ_PERM)
- public ListCollectionsResponse listCollections() {
+ public ListCollectionsResponse listCollections(
+ Boolean detailed, String collection, String shard, String routeKey,
Boolean prs)
+ throws Exception {
final ListCollectionsResponse response =
instantiateJerseyResponse(ListCollectionsResponse.class);
validateZooKeeperAwareCoreContainer(coreContainer);
+ if (Boolean.TRUE.equals(detailed)) {
+ if (collection != null) {
+ recordCollectionForLogAndTracing(collection, solrQueryRequest);
+ }
+ // Bind only the documented query params; anything else on the request
is ignored.
+ final ModifiableSolrParams params = new ModifiableSolrParams();
+ params.setNonNull("collection", collection);
+ params.setNonNull("shard", shard);
+ params.setNonNull(ShardParams._ROUTE_, routeKey);
+ if (prs != null) {
+ params.set("prs", prs);
+ }
+ final ClusterStatus clusterStatus =
+ new
ClusterStatus(coreContainer.getZkController().getZkStateReader(), params);
+ response.collectionsDetail =
typedCollections(clusterStatus.getCollectionStatuses());
+ return response;
+ }
+
// resolve each name to ensure it exists.
// TODO https://issues.apache.org/jira/browse/SOLR-16909 to go direct to
ZK?
List<String> collectionList =
@@ -63,4 +96,17 @@ public class ListCollections extends AdminAPIBase implements
ListCollectionsApi
return response;
}
+
+ /**
+ * Bind the state maps onto the response types. Fields the model knows about
are typed; every
+ * other entry stays on the object through its catch-all.
+ */
+ private static Map<String, CollectionState> typedCollections(Map<String,
Object> raw) {
+ var mapper = SolrJacksonMapper.getObjectMapper();
+ Map<String, CollectionState> collections =
CollectionUtil.newLinkedHashMap(raw.size());
+ for (Map.Entry<String, Object> entry : raw.entrySet()) {
+ collections.put(entry.getKey(), mapper.convertValue(entry.getValue(),
CollectionState.class));
+ }
+ return collections;
+ }
}
diff --git
a/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java
b/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java
index 0546551b11d..17ff7920837 100644
--- a/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java
@@ -29,7 +29,6 @@ import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.api.Api;
import org.apache.solr.api.ApiBag;
import org.apache.solr.common.params.CollectionParams;
-import org.apache.solr.common.params.CollectionParams.CollectionAction;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.CommandOperation;
@@ -75,13 +74,6 @@ public class V2ClusterAPIMappingTest extends SolrTestCaseJ4 {
assertEquals(CollectionParams.CollectionAction.OVERSEERSTATUS.lowerName,
v1Params.get(ACTION));
}
- @Test
- public void testClusterStatusAllParams() throws Exception {
- final SolrParams v1Params = captureConvertedV1Params("/cluster", "GET",
null);
-
- assertEquals(CollectionAction.CLUSTERSTATUS.lowerName,
v1Params.get(ACTION));
- }
-
private SolrParams captureConvertedV1Params(String path, String method,
String v2RequestBody)
throws Exception {
return doCaptureParams(path, method, v2RequestBody,
mockCollectionsHandler);
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedStandaloneTest.java
b/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedStandaloneTest.java
new file mode 100644
index 00000000000..48a08b7a9e3
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedStandaloneTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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 org.apache.solr.SolrTestCase;
+import org.apache.solr.client.solrj.RemoteSolrException;
+import org.apache.solr.client.solrj.request.CollectionsApi;
+import org.apache.solr.util.SolrJettyTestRule;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+/** Standalone coverage for {@code GET /api/collections?detailed=true}. */
+public class ListCollectionsDetailedStandaloneTest extends SolrTestCase {
+
+ @ClassRule public static final SolrJettyTestRule solrTestRule = new
SolrJettyTestRule();
+
+ @BeforeClass
+ public static void setupSolr() throws Exception {
+ solrTestRule.startSolr(createTempDir());
+ }
+
+ @Test
+ public void testRequiresSolrCloud() {
+ var req = new CollectionsApi.ListCollections();
+ req.setDetailed(true);
+ final RemoteSolrException ex =
+ expectThrows(RemoteSolrException.class, () ->
req.process(solrTestRule.getAdminClient()));
+ assertEquals(400, ex.code());
+ }
+}
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedTest.java
b/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedTest.java
new file mode 100644
index 00000000000..93808b8a27a
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedTest.java
@@ -0,0 +1,236 @@
+/*
+ * 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.client.solrj.SolrRequest.METHOD.GET;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.solr.client.api.model.ListCollectionsResponse;
+import
org.apache.solr.client.api.model.ListCollectionsResponse.CollectionState;
+import org.apache.solr.client.api.model.ListCollectionsResponse.ReplicaState;
+import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.CollectionsApi;
+import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.Utils;
+import org.eclipse.jetty.client.ContentResponse;
+import org.eclipse.jetty.client.HttpClient;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * HTTP tests for {@code GET /api/collections?detailed=true}.
+ *
+ * <p>The response is the collections, shards, and replicas tree. Live nodes,
the alias map, and
+ * cluster properties stay on their own endpoints (not this one). v1 {@code
CLUSTERSTATUS} still
+ * returns them.
+ */
+public class ListCollectionsDetailedTest extends SolrCloudTestCase {
+
+ private static final String COLLECTION = "clusterstatuscoll";
+ private static final String ALIAS = "clusterstatusalias";
+
+ @BeforeClass
+ public static void setupCluster() throws Exception {
+ configureCluster(1).addConfig("conf",
configset("cloud-minimal")).configure();
+ CollectionAdminRequest.createCollection(COLLECTION, "conf", 1, 1)
+ .process(cluster.getSolrClient());
+ CollectionAdminRequest.createAlias(ALIAS,
COLLECTION).process(cluster.getSolrClient());
+ }
+
+ @Test
+ public void testReturnsCollectionTreeWithoutClusterLevelExtras() throws
Exception {
+ Map<String, Object> body = getCollections("?detailed=true");
+ assertNull(body.get("cluster"));
+ assertNull(body.get("live_nodes"));
+ assertNull(body.get("aliases"));
+ assertNull(body.get("properties"));
+
+ Map<String, Object> collection = collection(body, COLLECTION);
+ assertNotNull(collection.get("health"));
+ assertEquals("conf", collection.get("configName"));
+ assertNotNull(collection.get("router"));
+ assertEquals(Set.of("shard1"), shards(collection).keySet());
+ assertNotNull(replica(collection).get("node_name"));
+ assertNotNull(replica(collection).get("state"));
+ assertTrue(aliasesOf(collection).contains(ALIAS));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testGeneratedClientReadsTheTree() throws Exception {
+ var req = new CollectionsApi.ListCollections();
+ req.setDetailed(true);
+ ListCollectionsResponse response = req.process(cluster.getSolrClient());
+ assertNull(response.error);
+ assertNull(response.collections);
+ CollectionState collection = response.collectionsDetail.get(COLLECTION);
+ assertNotNull(collection);
+ assertEquals("conf", collection.configName);
+ assertNotNull(collection.health);
+ assertTrue(collection.aliases.contains(ALIAS));
+ assertNotNull(collection.router);
+ assertEquals("compositeId", collection.router.get("name"));
+ assertEquals(Integer.valueOf(1), collection.replicationFactor);
+
+ ReplicaState replica =
collection.shards.get("shard1").replicas.values().iterator().next();
+ assertNotNull(replica.nodeName);
+ assertNotNull(replica.state);
+ assertEquals("true", replica.leader);
+ }
+
+ @Test
+ public void testWithoutDetailedReturnsPlainNameList() throws Exception {
+ var req = new CollectionsApi.ListCollections();
+ ListCollectionsResponse response = req.process(cluster.getSolrClient());
+ assertNull(response.error);
+ assertNull(response.collectionsDetail);
+ assertTrue(response.collections.contains(COLLECTION));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testPerReplicaState() throws Exception {
+ final String prsCollection = "prsclusterstatus";
+ CollectionAdminRequest.createCollection(prsCollection, "conf", 1, 1)
+ .setPerReplicaState(Boolean.TRUE)
+ .process(cluster.getSolrClient());
+
+ Map<String, Object> prs =
+ (Map<String, Object>)
+ collection(
+ getCollections("?detailed=true&collection=" +
prsCollection + "&prs=true"),
+ prsCollection)
+ .get("PRS");
+ assertNotNull(prs);
+ assertNotNull(prs.get("states"));
+
+ var request = new CollectionsApi.ListCollections();
+ request.setDetailed(true);
+ request.setCollection(prsCollection);
+ request.setPrs(true);
+ CollectionState typed =
+
request.process(cluster.getSolrClient()).collectionsDetail.get(prsCollection);
+ assertNotNull(typed.unknownProperties().get("PRS"));
+ }
+
+ @Test
+ public void testCollectionShardAndAliasFilters() throws Exception {
+ Map<String, Object> byCollection =
+ collections(getCollections("?detailed=true&collection=" + COLLECTION));
+ assertEquals(Set.of(COLLECTION), byCollection.keySet());
+
+ Map<String, Object> oneShard =
+ shards(
+ collection(
+ getCollections("?detailed=true&collection=" + COLLECTION +
"&shard=shard1"),
+ COLLECTION));
+ assertEquals(Set.of("shard1"), oneShard.keySet());
+
+ ContentResponse missingShard =
+ httpGet("?detailed=true&collection=" + COLLECTION +
"&shard=nosuchshard");
+ assertEquals(400, missingShard.getStatus());
+
+ assertNotNull(
+ collections(getCollections("?detailed=true&collection=" +
ALIAS)).get(COLLECTION));
+ }
+
+ @Test
+ public void testUnknownCollectionIsRejected() throws Exception {
+ ContentResponse response =
httpGet("?detailed=true&collection=not-a-collection");
+ assertEquals(400, response.getStatus());
+ assertTrue(response.getContentAsString().contains("not found"));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testV1ClusterStatusStillReturnsLiveNodes() throws Exception {
+ NamedList<Object> response =
+
CollectionAdminRequest.getClusterStatus().process(cluster.getSolrClient()).getResponse();
+ Map<String, Object> clusterState = (Map<String, Object>)
response.get("cluster");
+ assertNotNull(clusterState.get("live_nodes"));
+ assertNotNull(clusterState.get("collections"));
+
+ ModifiableSolrParams params = new ModifiableSolrParams();
+ params.set("action", "CLUSTERSTATUS");
+ params.set("liveNodes", false);
+ params.set("aliases", false);
+ params.set("clusterProperties", false);
+ NamedList<Object> collectionsOnly =
+ cluster
+ .getSolrClient()
+ .request(
+ new GenericSolrRequest(GET, "/admin/collections",
SolrRequestType.ADMIN, params));
+ Map<String, Object> filtered = (Map<String, Object>)
collectionsOnly.get("cluster");
+ assertNull(filtered.get("live_nodes"));
+ assertNull(filtered.get("aliases"));
+ assertNull(filtered.get("properties"));
+ assertNotNull(((Map<String, Object>)
filtered.get("collections")).get(COLLECTION));
+ }
+
+ private static Map<String, Object> getCollections(String query) throws
Exception {
+ ContentResponse response = httpGet(query);
+ assertEquals(response.getContentAsString(), 200, response.getStatus());
+ return parsed(response);
+ }
+
+ private static ContentResponse httpGet(String query) throws Exception {
+ HttpClient httpClient =
cluster.getJettySolrRunner(0).getSolrClient().getHttpClient();
+ String url = cluster.getJettySolrRunner(0).getBaseURLV2().toString() +
"/collections" + query;
+ return httpClient.GET(url);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map<String, Object> parsed(ContentResponse response) {
+ return (Map<String, Object>)
Utils.fromJSONString(response.getContentAsString());
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map<String, Object> collections(Map<String, Object> body) {
+ return (Map<String, Object>) body.get("collectionsDetail");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map<String, Object> collection(Map<String, Object> body,
String name) {
+ Map<String, Object> collection = (Map<String, Object>)
collections(body).get(name);
+ assertNotNull(collection);
+ return collection;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map<String, Object> shards(Map<String, Object> collection) {
+ return (Map<String, Object>) collection.get("shards");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List<String> aliasesOf(Map<String, Object> collection) {
+ return (List<String>) collection.get("aliases");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map<String, Object> replica(Map<String, Object> collection) {
+ Map<String, Object> shard = (Map<String, Object>)
shards(collection).get("shard1");
+ Map<String, Object> replicas = (Map<String, Object>) shard.get("replicas");
+ assertFalse(replicas.isEmpty());
+ return (Map<String, Object>) replicas.values().iterator().next();
+ }
+}
diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/v2-api.adoc
b/solr/solr-ref-guide/modules/configuration-guide/pages/v2-api.adoc
index 26390f38c30..6d0e6c0e60d 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/v2-api.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/v2-api.adoc
@@ -38,7 +38,7 @@ Following are some v2 API URL paths and path prefixes, along
with some of the op
[width="100%",options="header",]
|===
|Path prefix |Some Supported Operations
-|`/api/collections` |Create, alias, backup, and restore a collection.
+|`/api/collections` |Create, alias, backup, and restore a collection. Fetch
the collections, shards, and replicas tree (`?detailed=true`).
|`/api/c/\{collectionName}/update` |Update requests.
|`/api/c/\{collectionName}/config` |Configuration requests.
|`/api/c/\{collectionName}/schema` |Schema requests.
@@ -49,7 +49,7 @@ Following are some v2 API URL paths and path prefixes, along
with some of the op
|`/api/cores` |Create a core.
|`/api/cores/\{coreName}` |Reload, rename, delete, and unload a core.
|`/api/node` |Perform overseer operation, rejoin leader election.
-|`/api/cluster` |List live nodes, add role, remove role, set cluster property.
+|`/api/cluster` |List live nodes, manage node roles, set cluster property.
`POST` sets the cluster rate limiter.
|===
== Introspect
@@ -149,13 +149,13 @@ curl http://localhost:8983/api/c/gettingstarted -H
'Content-type:application/jso
{"responseHeader":{"status":0,"QTime":842}}
----
-See the state of the cluster:
+See the collections, shards, and replicas in the cluster:
[source,bash]
----
-curl http://localhost:8983/api/cluster
+curl http://localhost:8983/api/collections?detailed=true
-{"responseHeader":{"status":0,"QTime":0},"collections":["gettingstarted"]}
+{"responseHeader":{"status":0,"QTime":1},"collectionsDetail":{"gettingstarted":{"health":"GREEN","shards":{"shard1":{"state":"active"}}}}}
----
Set a cluster property:
diff --git
a/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
b/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
index 5301f9dd02c..f42f6d1f930 100644
---
a/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
+++
b/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
@@ -27,6 +27,10 @@ These API commands work with a SolrCloud cluster at the
entire cluster level, or
Fetch the cluster status including collections, shards, replicas,
configuration name as well as collection aliases and cluster properties.
+NOTE: This is a v1-only command.
+The v2 API splits this information up: see
xref:collection-management.adoc#collectionsdetail[Detailed Collections Tree]
for the collections, shards, and replicas tree (`detailed=true`), and `GET
/api/cluster/nodes`, `GET /api/aliases`, and `GET /api/cluster/properties` for
live nodes, the alias map, and cluster properties, respectively.
+The two collection-tree responses overlap in content -- same `health` states,
same shard/replica fields -- because they cover the same underlying data;
they're documented separately because the parameters and response shape
otherwise differ between v1 and v2 here.
+
Additionally, this command reports a `health` status of each collection and
shard, in
order to make it easier to monitor the operational state of the collections.
The
@@ -44,28 +48,10 @@ No active replicas *OR* there's no shard leader.
The collection health state is reported as the worst state of any shard, e.g.,
for a collection with all shards GREEN except for one YELLOW the collection
health will be reported as YELLOW.
-[tabs#clusterstatus-request]
-======
-V1 API::
-+
-====
[source,bash]
----
http://localhost:8983/solr/admin/collections?action=CLUSTERSTATUS
-
----
-====
-
-V2 API::
-+
-====
-[source,bash]
-----
-curl http://localhost:8983/api/cluster
-
-----
-====
-======
=== CLUSTERSTATUS Parameters
@@ -106,6 +92,7 @@ This can be used if you need the details of the shard where
a particular documen
|Optional |Default: will default to the default value of `includeAll`
parameter specified below
|===
+
+If set to true, returns the cluster alias map.
`liveNodes`::
+
@@ -114,7 +101,7 @@ This can be used if you need the details of the shard where
a particular documen
|Optional |Default: will default to the default value of `includeAll`
parameter specified below
|===
+
-If set to true, returns the status of live nodes in the cluster.
+If set to true, returns the live nodes in the cluster.
`clusterProperties`::
+
diff --git
a/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
b/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
index c985801021b..c5303b26a8c 100644
---
a/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
+++
b/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
@@ -510,6 +510,206 @@ curl http://localhost:8983/api/collections
"example2"]}
----
+[[collectionsdetail]]
+=== Detailed Collections Tree (V2 Only)
+
+With `detailed=true`, the V2 API returns the collections, shards, and replicas
tree, in `collectionsDetail`, including each collection's configuration name
and health, instead of the plain collection name list `LIST` returns by default.
+
+NOTE: This is a v2-only variant of `LIST`.
+See xref:cluster-node-management.adoc#clusterstatus[CLUSTERSTATUS] for the v1
command that returns similar information (plus live nodes, the alias map, and
cluster properties).
+The two overlap in content -- same `health` states, same shard/replica fields
-- because they cover the same underlying data; they're documented separately
because the parameters and response shape otherwise differ between v1 and v2
here.
+
+This command reports a `health` status of each collection and shard, in order
to make
+it easier to monitor the operational state of the collections.
+The following health
+state values are defined, ordered from the best to worst, based on the
percentage of
+active replicas (`active`):
+
+`GREEN`::
+`active == 100%`, all replicas are active and there's a shard leader.
+`YELLOW`::
+`100% > active > 50%`, AND there's a shard leader.
+`ORANGE`::
+`50% >= active > 0%`, AND there's a shard leader.
+`RED`::
+No active replicas *OR* there's no shard leader.
+
+The collection health state is reported as the worst state of any shard, e.g.,
for a collection with all shards GREEN except for one YELLOW the collection
health will be reported as YELLOW.
+
+[source,bash]
+----
+curl -X GET "http://localhost:8983/api/collections?detailed=true"
+----
+
+==== Detailed Collections Tree Parameters
+
+`detailed`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `false`
+|===
++
+Return the collections, shards, and replicas tree in `collectionsDetail`
instead of the plain collection name list in `collections`.
+
+`collection`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|===
++
+Only used when `detailed=true`.
+The collection or alias name for which information is requested.
+If omitted, information on all collections in the cluster will be returned.
+If an alias is supplied, information on the collections in the alias will be
returned.
+
+`shard`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|===
++
+Only used when `detailed=true`.
+The shard(s) for which information is requested.
+Multiple shard names can be specified as a comma-separated list.
+
+`\_route_`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|===
++
+Only used when `detailed=true`.
+This can be used if you need the details of the shard where a particular
document belongs to and you don't know which shard it falls under.
+
+`prs`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `false`
+|===
++
+Only used when `detailed=true`.
+Include per-replica state when the collection uses it.
+
+==== Detailed Collections Tree Response
+
+The response will include the status of the request and `collectionsDetail`, a
map of collection name to collection state.
+
+==== Example
+
+*Input*
+
+[source,text]
+----
+http://localhost:8983/api/collections?detailed=true
+----
+
+*Output*
+
+[source,json]
+----
+{
+ "responseHeader":{
+ "status":0,
+ "QTime":333},
+ "collectionsDetail":{
+ "collection1":{
+ "shards":{
+ "shard1":{
+ "range":"80000000-ffffffff",
+ "state":"active",
+ "health": "GREEN",
+ "replicas":{
+ "core_node1":{
+ "state":"active",
+ "core":"collection1",
+ "node_name":"127.0.1.1:8983_solr",
+ "base_url":"http://127.0.1.1:8983/solr",
+ "leader":"true"},
+ "core_node3":{
+ "state":"active",
+ "core":"collection1",
+ "node_name":"127.0.1.1:8900_solr",
+ "base_url":"http://127.0.1.1:8900/solr"}}},
+ "shard2":{
+ "range":"0-7fffffff",
+ "state":"active",
+ "health": "GREEN",
+ "replicas":{
+ "core_node2":{
+ "state":"active",
+ "core":"collection1",
+ "node_name":"127.0.1.1:7574_solr",
+ "base_url":"http://127.0.1.1:7574/solr",
+ "leader":"true"},
+ "core_node4":{
+ "state":"active",
+ "core":"collection1",
+ "node_name":"127.0.1.1:7500_solr",
+ "base_url":"http://127.0.1.1:7500/solr"}}}},
+ "router":{"name":"compositeId"},
+ "replicationFactor":1,
+ "znodeVersion": 11,
+ "configName" : "my_config",
+ "health": "GREEN",
+ "aliases":["both_collections"]
+ },
+ "collection2":{"key": "value"}
+ }
+}
+----
+
+Adding `collection` and `shard` narrows the tree to just that shard of that
collection:
+
+*Input*
+
+[source,text]
+----
+http://localhost:8983/api/collections?detailed=true&collection=collection1&shard=shard1
+----
+
+*Output*
+
+[source,json]
+----
+{
+ "responseHeader":{
+ "status":0,
+ "QTime":12},
+ "collectionsDetail":{
+ "collection1":{
+ "shards":{
+ "shard1":{
+ "range":"80000000-ffffffff",
+ "state":"active",
+ "health": "GREEN",
+ "replicas":{
+ "core_node1":{
+ "state":"active",
+ "core":"collection1",
+ "node_name":"127.0.1.1:8983_solr",
+ "base_url":"http://127.0.1.1:8983/solr",
+ "leader":"true"},
+ "core_node3":{
+ "state":"active",
+ "core":"collection1",
+ "node_name":"127.0.1.1:8900_solr",
+ "base_url":"http://127.0.1.1:8900/solr"}}}},
+ "router":{"name":"compositeId"},
+ "replicationFactor":1,
+ "znodeVersion": 11,
+ "configName" : "my_config",
+ "health": "GREEN",
+ "aliases":["both_collections"]
+ }
+ }
+}
+----
+
[[rename]]
== RENAME: Rename a Collection
diff --git
a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-11.adoc
b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-11.adoc
index ac2de0f8137..0372e4b820b 100644
---
a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-11.adoc
+++
b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-11.adoc
@@ -41,3 +41,15 @@ bin/solr start -Dsolr.node.roles=data:on,overseer:preferred
A node started this way asks the Overseer to re-run its node prioritization,
so a preferred node takes over without waiting for the current Overseer to
restart.
Note that node roles are fixed for the lifetime of a node: unlike `ADDROLE`,
they cannot be changed on a running node.
+
+=== V2 collections tree
+
+`GET /api/collections?detailed=true` returns the collections, shards, and
replicas tree under `collectionsDetail`.
+It does not include `live_nodes`, the alias map, or cluster properties.
+Use `GET /api/cluster/nodes`, `GET /api/aliases`, and `GET
/api/cluster/properties` for those.
+
+There is no `includeAll` parameter.
+A request with no other parameters returns every collection.
+`collection`, `shard`, and `_route_` narrow which part of the tree is returned.
+
+The v1 `CLUSTERSTATUS` command is unchanged, and `includeAll` still defaults
to true.
diff --git a/solr/webapp/web/js/angular/app.js
b/solr/webapp/web/js/angular/app.js
index f61b4e8d51c..1558918d09e 100644
--- a/solr/webapp/web/js/angular/app.js
+++ b/solr/webapp/web/js/angular/app.js
@@ -576,7 +576,7 @@ solrAdminApp.controller('MainController', function($scope,
$route, $rootScope, $
var currentCollectionName = $route.current.params.core;
delete $scope.currentCollection;
if ($scope.isCloudEnabled) {
- CollectionsV2.listCollections(function (error, cdata, response) {
+ CollectionsV2.listCollections({}, function (error, cdata, response) {
$timeout(function() {
if (error) { ApiErrorHandler.handle(response); return; }
AliasesV2.getAliases(function (error, adata, response) {
diff --git a/solr/webapp/web/js/angular/controllers/alias-overview.js
b/solr/webapp/web/js/angular/controllers/alias-overview.js
index 6fbca386fe2..212493e8444 100644
--- a/solr/webapp/web/js/angular/controllers/alias-overview.js
+++ b/solr/webapp/web/js/angular/controllers/alias-overview.js
@@ -16,7 +16,7 @@
*/
solrAdminApp.controller('AliasOverviewController',
-function($scope, $routeParams, Collections, Constants) {
+function($scope, $routeParams, Constants) {
$scope.resetMenu("collection-overview", Constants.IS_COLLECTION_PAGE);
$scope.refresh = function() {
diff --git a/solr/webapp/web/js/angular/controllers/cloud.js
b/solr/webapp/web/js/angular/controllers/cloud.js
index 2ce9a392d1c..87ae9eecf31 100644
--- a/solr/webapp/web/js/angular/controllers/cloud.js
+++ b/solr/webapp/web/js/angular/controllers/cloud.js
@@ -16,7 +16,7 @@
*/
solrAdminApp.controller('CloudController',
- function($scope, $location, $timeout, $q, Zookeeper, ZookeeperReadV2,
Constants, Collections, ClusterV2, SystemV2, Metrics, MetricsExtractor,
ZookeeperStatus, ApiErrorHandler) {
+ function($scope, $location, $timeout, $q, Zookeeper, ZookeeperReadV2,
Constants, ClusterV2, CollectionsV2, SystemV2, Metrics, MetricsExtractor,
ZookeeperStatus, ApiErrorHandler) {
$scope.showDebug = false;
@@ -37,7 +37,7 @@ solrAdminApp.controller('CloudController',
graphSubController($scope, $timeout, Zookeeper, ClusterV2,
ApiErrorHandler);
} else if (view === "nodes") {
$scope.resetMenu("cloud-nodes", Constants.IS_ROOT_PAGE);
- nodesSubController($scope, $timeout, Collections, ClusterV2,
SystemV2, Metrics, MetricsExtractor, ApiErrorHandler);
+ nodesSubController($scope, $timeout, ClusterV2, CollectionsV2,
SystemV2, Metrics, MetricsExtractor, ApiErrorHandler);
} else if (view === "zkstatus") {
$scope.resetMenu("cloud-zkstatus", Constants.IS_ROOT_PAGE);
zkStatusSubController($scope, ZookeeperStatus, false);
@@ -107,7 +107,7 @@ function isNumeric(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
-var nodesSubController = function($scope, $timeout, Collections, ClusterV2,
SystemV2, Metrics, MetricsExtractor, ApiErrorHandler) {
+var nodesSubController = function($scope, $timeout, ClusterV2, CollectionsV2,
SystemV2, Metrics, MetricsExtractor, ApiErrorHandler) {
$scope.pageSize = 10;
$scope.showNodes = true;
$scope.showTree = false;
@@ -178,70 +178,74 @@ var nodesSubController = function($scope, $timeout,
Collections, ClusterV2, Syst
var live_nodes = [];
// We build a node-centric view of the cluster state which we can easily
consume to render the table
- Collections.status(function (data) {
- // Fetch cluster state from collections API and invert to a nodes
structure
- for (var name in data.cluster.collections) {
- var collection = data.cluster.collections[name];
- collection.name = name;
- var shards = collection.shards;
- collection.shards = [];
- for (var shardName in shards) {
- var shard = shards[shardName];
- shard.name = shardName;
- shard.collection = collection.name;
- var replicas = shard.replicas;
- shard.replicas = [];
- for (var replicaName in replicas) {
- var core = replicas[replicaName];
- core.name = replicaName;
- core.replica = core['core'].replace(/.*_(replica_.*)$/, '\$1');
- core.collection = collection.name;
- core.shard = shard.name;
- core.shard_state = shard.state;
- core.label = core['collection'] + "_"
- + (core['shard'] + "_").replace(/shard(\d+)_/, 's\$1')
- + core['replica'].replace(/replica_?[ntp]?(\d+)/, 'r\$1');
-
- var node_name = core['node_name'];
- var node = getOrCreateObj(node_name, nodes);
- var cores = getOrCreateList("cores", node);
- cores.push(core);
- node['base_url'] = core.base_url;
- node['id'] = core.base_url.replace(/[^\w\d]/g, '');
- node['host'] = node_name.split(":")[0];
- var collections = getOrCreateList("collections", node);
- ensureInList(core.collection, collections);
- ensureNodeInHosts(node_name, hosts);
+ CollectionsV2.listCollections({detailed: true}, function (error, data,
response) {
+ $timeout(function() {
+ if (error) { ApiErrorHandler.handle(response); return; }
+
+ // Fetch cluster state from CollectionsApi and invert to a nodes
structure
+ for (var name in data.collectionsDetail) {
+ var collection = data.collectionsDetail[name];
+ collection.name = name;
+ var shards = collection.shards;
+ collection.shards = [];
+ for (var shardName in shards) {
+ var shard = shards[shardName];
+ shard.name = shardName;
+ shard.collection = collection.name;
+ var replicas = shard.replicas;
+ shard.replicas = [];
+ for (var replicaName in replicas) {
+ var core = replicas[replicaName];
+ core.name = replicaName;
+ core.replica = core['core'].replace(/.*_(replica_.*)$/, '\$1');
+ core.collection = collection.name;
+ core.shard = shard.name;
+ core.shard_state = shard.state;
+ core.label = core['collection'] + "_"
+ + (core['shard'] + "_").replace(/shard(\d+)_/, 's\$1')
+ + core['replica'].replace(/replica_?[ntp]?(\d+)/, 'r\$1');
+
+ var node_name = core['node_name'];
+ var node = getOrCreateObj(node_name, nodes);
+ var cores = getOrCreateList("cores", node);
+ cores.push(core);
+ node['base_url'] = core.base_url;
+ node['id'] = core.base_url.replace(/[^\w\d]/g, '');
+ node['host'] = node_name.split(":")[0];
+ var collections = getOrCreateList("collections", node);
+ ensureInList(core.collection, collections);
+ ensureNodeInHosts(node_name, hosts);
+ }
}
}
- }
- ClusterV2.listClusterNodes(function (error, nodesData, response) {
- $timeout(function() {
- if (error) { ApiErrorHandler.handle(response); return; }
-
- live_nodes = nodesData.nodes;
- for (n in live_nodes) {
- node = live_nodes[n];
- if (!(node in nodes)) {
- var hostName = node.split(":")[0];
- nodes[node] = {};
- nodes[node]['host'] = hostName;
+ ClusterV2.listClusterNodes(function (error, nodesData, response) {
+ $timeout(function() {
+ if (error) { ApiErrorHandler.handle(response); return; }
+
+ live_nodes = nodesData.nodes;
+ for (n in live_nodes) {
+ node = live_nodes[n];
+ if (!(node in nodes)) {
+ var hostName = node.split(":")[0];
+ nodes[node] = {};
+ nodes[node]['host'] = hostName;
+ }
+ ensureNodeInHosts(node, hosts);
}
- ensureNodeInHosts(node, hosts);
- }
- // Make sure nodes are sorted alphabetically to align with rowspan
in table
- for (var host in hosts) {
- hosts[host].nodes.sort();
- }
+ // Make sure nodes are sorted alphabetically to align with rowspan
in table
+ for (var host in hosts) {
+ hosts[host].nodes.sort();
+ }
- $scope.nodes = nodes;
- $scope.hosts = hosts;
- $scope.live_nodes = live_nodes;
+ $scope.nodes = nodes;
+ $scope.hosts = hosts;
+ $scope.live_nodes = live_nodes;
- $scope.Math = window.Math;
- $scope.reload();
+ $scope.Math = window.Math;
+ $scope.reload();
+ });
});
});
});
diff --git a/solr/webapp/web/js/angular/controllers/collection-overview.js
b/solr/webapp/web/js/angular/controllers/collection-overview.js
index d1834b2bd1e..9292da4f7c6 100644
--- a/solr/webapp/web/js/angular/controllers/collection-overview.js
+++ b/solr/webapp/web/js/angular/controllers/collection-overview.js
@@ -16,14 +16,17 @@
*/
solrAdminApp.controller('CollectionOverviewController',
-function($scope, $routeParams, Collections, Constants) {
+function($scope, $routeParams, $timeout, CollectionsV2, Constants,
ApiErrorHandler) {
$scope.resetMenu("collection-overview", Constants.IS_COLLECTION_PAGE);
$scope.refresh = function() {
- Collections.status({}, function(data) {
- $scope.selectedCollection = data.cluster.collections[$routeParams.core];
- $scope.selectedCollection.name = $routeParams.core;
- $scope.rootUrl = Constants.ROOT_URL;
+ CollectionsV2.listCollections({detailed: true, collection:
$routeParams.core}, function(error, data, response) {
+ $timeout(function() {
+ if (error) { ApiErrorHandler.handle(response); return; }
+ $scope.selectedCollection = data.collectionsDetail[$routeParams.core];
+ $scope.selectedCollection.name = $routeParams.core;
+ $scope.rootUrl = Constants.ROOT_URL;
+ });
});
};
diff --git a/solr/webapp/web/js/angular/controllers/collections.js
b/solr/webapp/web/js/angular/controllers/collections.js
index 1c93b088b5e..17707f34243 100644
--- a/solr/webapp/web/js/angular/controllers/collections.js
+++ b/solr/webapp/web/js/angular/controllers/collections.js
@@ -16,7 +16,7 @@
*/
solrAdminApp.controller('CollectionsController',
- function($scope, $routeParams, $location, $timeout, Collections,
CollectionsV2, AliasesV2, ShardsV2, ReplicasV2, ConfigSetsV2, ClusterV2,
Constants, ApiErrorHandler){
+ function($scope, $routeParams, $location, $timeout, CollectionsV2,
AliasesV2, ShardsV2, ReplicasV2, ConfigSetsV2, ClusterV2, Constants,
ApiErrorHandler){
$scope.resetMenu("collections", Constants.IS_ROOT_PAGE);
$scope.refresh = function() {
@@ -30,13 +30,15 @@ solrAdminApp.controller('CollectionsController',
});
});
- Collections.status(function (data) {
+ CollectionsV2.listCollections({detailed: true}, function (error,
data, response) {
+ $timeout(function() {
+ if (error) { ApiErrorHandler.handle(response); return; }
$scope.collections = [];
- for (var name in data.cluster.collections) {
+ for (var name in data.collectionsDetail) {
if (name.startsWith("._designer_")) {
continue;
}
- var collection = data.cluster.collections[name];
+ var collection = data.collectionsDetail[name];
collection.name = name;
collection.type = 'collection';
var shards = collection.shards;
@@ -85,6 +87,7 @@ solrAdminApp.controller('CollectionsController',
}
});
});
+ });
});
ConfigSetsV2.listConfigSet(function(error, data, response) {
$timeout(function() {
diff --git a/solr/webapp/web/js/angular/services.js
b/solr/webapp/web/js/angular/services.js
index c0b525b1698..462d684121e 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -166,16 +166,6 @@ solrAdminServices.factory('Metrics',
delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"];
return new solrApi.ConfigApi();
})
-.factory('Collections',
- ['$resource', function ($resource) {
- // v2 ClusterAPI (/api/cluster) delegates straight through to the same v1
CollectionsHandler
- // that v1's CLUSTERSTATUS action used, so the response shape is
byte-identical -- no
- // generated solrApi client class exists for it (old-style @EndPoint API,
predates the
- // OpenAPI-based v2 framework), so this stays a plain $resource, like
ParamSet.
- return $resource('/api/cluster', {'wt':'json', '_':Date.now()}, {
- "status": {}
- });
- }])
.factory('ConfigSetFiles',
['$http', function ($http) {
// Fetches a single file from a configset via V2
/api/configsets/{name}/files/{path}.
@@ -276,7 +266,7 @@ solrAdminServices.factory('Metrics',
// v2 GetConfigAPI/ModifyParamSetAPI
(/api/(cores|collections)/:core/config/params) still
// delegate straight through to the same v1 SolrConfigHandler, so the
response shape is
// byte-identical -- no generated solrApi client class exists for it
(old-style @EndPoint API,
- // predates the OpenAPI-based v2 framework), so this stays a plain
$resource, like Collections.
+ // predates the OpenAPI-based v2 framework), so this stays a plain
$resource.
// NB: unlike v1's flexible routing, the v2 API requires knowing up front
whether ":core" is a
// collection name (SolrCloud) or an actual core name
(standalone/user-managed) --
// /api/collections/... 500s in standalone mode (it tries to resolve
aliases, which needs ZK),
@@ -395,7 +385,7 @@ solrAdminServices.factory('Metrics',
// body (the server deliberately reads the raw content stream, dispatched
by Content-Type,
// rather than a formal parameter) and query() takes no query params at
all (the server
// forwards arbitrary SolrParams straight through). Both stay on this
plain $resource, like
- // Collections/ParamSet. Every other Schema Designer endpoint uses
SchemaDesignerV2.
+ // ParamSet. Every other Schema Designer endpoint uses SchemaDesignerV2.
return $resource('/api/schema-designer/:configSet/:path', {wt: 'json',
path: '@path', configSet: '@configSet', filePath: '@filePath', _:Date.now()}, {
get: {method: "GET"},
post: {method: "POST", timeout: 90000},