diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/CollectionStatusApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/CollectionStatusApi.java new file mode 100644 index 00000000000..d07982cab76 --- /dev/null +++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/CollectionStatusApi.java @@ -0,0 +1,75 @@ +/* + * 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.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.PathParam; +import jakarta.ws.rs.QueryParam; +import org.apache.solr.client.api.model.CollectionStatusResponse; + +/** + * V2 API definition for fetching collection metadata + * + *

This API (GET /v2/collections/collectionName) is analogous to the v1 + * /admin/collections?action=COLSTATUS command. + */ +@Path("/collections/{collectionName}") +public interface CollectionStatusApi { + + // TODO Query parameters currently match those offered by the v1 + // /admin/collections?action=COLSTATUS. Should param names be updated/clarified? + @GET + @Operation( + summary = "Fetches metadata about the specified collection", + tags = {"collections"}) + CollectionStatusResponse getCollectionStatus( + @Parameter(description = "The name of the collection return metadata for", required = true) + @PathParam("collectionName") + String collectionName, + @Parameter(description = SegmentsApi.CORE_INFO_PARAM_DESC) @QueryParam("coreInfo") + Boolean coreInfo, + @Parameter( + description = + "Boolean flag to include metadata and statistics about the segments used by each shard leader. Implicitly set to true by 'fieldInfo' and 'sizeInfo'") + @QueryParam("segments") + Boolean segments, + @Parameter( + description = + SegmentsApi.FIELD_INFO_PARAM_DESC + + " Implicitly sets the 'segments' flag to 'true'") + @QueryParam("fieldInfo") + Boolean fieldInfo, + @Parameter(description = SegmentsApi.RAW_SIZE_PARAM_DESC) @QueryParam("rawSize") + Boolean rawSize, + @Parameter(description = SegmentsApi.RAW_SIZE_SUMMARY_DESC) @QueryParam("rawSizeSummary") + Boolean rawSizeSummary, + @Parameter(description = SegmentsApi.RAW_SIZE_DETAILS_DESC) @QueryParam("rawSizeDetails") + Boolean rawSizeDetails, + @Parameter(description = SegmentsApi.RAW_SIZE_SAMPLING_PERCENT_DESC) + @QueryParam("rawSizeSamplingPercent") + Float rawSizeSamplingPercent, + @Parameter( + description = + SegmentsApi.SIZE_INFO_PARAM_DESC + + ". Implicitly sets the 'segment' flag to 'true'") + @QueryParam("sizeInfo") + Boolean sizeInfo) + throws Exception; +} diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/SegmentsApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/SegmentsApi.java new file mode 100644 index 00000000000..1f6f089642e --- /dev/null +++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/SegmentsApi.java @@ -0,0 +1,68 @@ +/* + * 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.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.GetSegmentDataResponse; +import org.apache.solr.client.api.util.CoreApiParameters; + +/** + * V2 API definition for fetching metadata about a core's segments + * + *

This API (GET /v2/cores/coreName/segments) is analogous to the v1 + * /solr/coreName/admin/segments API + */ +@Path("/cores/{coreName}/segments") +public interface SegmentsApi { + + String CORE_INFO_PARAM_DESC = + "Boolean flag to include metadata (e.g. index an data directories, IndexWriter configuration, etc.) about each shard leader's core"; + String FIELD_INFO_PARAM_DESC = + "Boolean flag to include statistics about the indexed fields present on each shard leader."; + String RAW_SIZE_PARAM_DESC = + "Boolean flag to include simple estimates of the disk size taken up by each field (e.g. \"id\", \"_version_\") and by each index data structure (e.g. 'storedFields', 'docValues_numeric')."; + String RAW_SIZE_SUMMARY_DESC = + "Boolean flag to include more involved estimates of the disk size taken up by index data structures, on a per-field basis (e.g. how much data does the \"id\" field contribute to 'storedField' index files). More detail than 'rawSize', less detail than 'rawSizeDetails'."; + String RAW_SIZE_DETAILS_DESC = + "Boolean flag to include detailed statistics about the disk size taken up by various fields and data structures. More detail than 'rawSize' and 'rawSizeSummary'."; + String RAW_SIZE_SAMPLING_PERCENT_DESC = + "Percentage (between 0 and 100) of data to read when estimating index size and statistics. Defaults to 5.0 (i.e. 5%)."; + String SIZE_INFO_PARAM_DESC = + "Boolean flag to include information about the largest index files for each Lucene segment."; + + @GET + @CoreApiParameters + @Operation( + summary = "Fetches metadata about the segments in use by the specified core", + tags = {"segments"}) + GetSegmentDataResponse getSegmentData( + @Parameter(description = CORE_INFO_PARAM_DESC) @QueryParam("coreInfo") Boolean coreInfo, + @Parameter(description = FIELD_INFO_PARAM_DESC) @QueryParam("fieldInfo") Boolean fieldInfo, + @Parameter(description = RAW_SIZE_PARAM_DESC) @QueryParam("rawSize") Boolean rawSize, + @Parameter(description = RAW_SIZE_SUMMARY_DESC) @QueryParam("rawSizeSummary") + Boolean rawSizeSummary, + @Parameter(description = RAW_SIZE_DETAILS_DESC) @QueryParam("rawSizeDetails") + Boolean rawSizeDetails, + @Parameter(description = RAW_SIZE_SAMPLING_PERCENT_DESC) @QueryParam("rawSizeSamplingPercent") + Float rawSizeSamplingPercent, + @Parameter(description = SIZE_INFO_PARAM_DESC) @QueryParam("sizeInfo") Boolean sizeInfo) + throws Exception; +} diff --git a/solr/api/src/java/org/apache/solr/client/api/model/CollectionStatusResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/CollectionStatusResponse.java new file mode 100644 index 00000000000..82109edb915 --- /dev/null +++ b/solr/api/src/java/org/apache/solr/client/api/model/CollectionStatusResponse.java @@ -0,0 +1,147 @@ +/* + * 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.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Response of the CollectionStatusApi.getCollectionStatus() API + * + *

Note that the corresponding v1 API has a slightly different response format. Users should not + * attempt to convert a v1 response into this type. + */ +public class CollectionStatusResponse extends SolrJerseyResponse { + + @JsonProperty public String name; + @JsonProperty public Integer znodeVersion; + + // TODO - consider 'Instant' once SOLR-17608 is finished + @JsonProperty + @JsonFormat(shape = JsonFormat.Shape.NUMBER) + public Date creationTimeMillis; + + @JsonProperty public CollectionMetadata properties; + @JsonProperty public Integer activeShards; + @JsonProperty public Integer inactiveShards; + @JsonProperty public List schemaNonCompliant; + + @JsonProperty public Map shards; + + // Always present in response + public static class CollectionMetadata { + @JsonProperty public String configName; + @JsonProperty public Integer nrtReplicas; + @JsonProperty public Integer pullReplicas; + @JsonProperty public Integer tlogReplicas; + @JsonProperty public Map router; + @JsonProperty public Integer replicationFactor; + + private Map unknownFields = new HashMap<>(); + + @JsonAnyGetter + public Map unknownProperties() { + return unknownFields; + } + + @JsonAnySetter + public void setUnknownProperty(String field, Object value) { + unknownFields.put(field, value); + } + } + + // Always present in response + public static class ShardMetadata { + @JsonProperty public String state; // TODO Make this an enum? + @JsonProperty public String range; + @JsonProperty public ReplicaSummary replicas; + @JsonProperty public LeaderSummary leader; + } + + // Always present in response + public static class ReplicaSummary { + @JsonProperty public Integer total; + @JsonProperty public Integer active; + @JsonProperty public Integer down; + @JsonProperty public Integer recovering; + + @JsonProperty("recovery_failed") + public Integer recoveryFailed; + } + + // Always present in response unless otherwise specified + public static class LeaderSummary { + @JsonProperty public String coreNode; + @JsonProperty public String core; + @JsonProperty public Boolean leader; + + @JsonProperty("node_name") + public String nodeName; + + @JsonProperty("base_url") + public String baseUrl; + + @JsonProperty public String state; // TODO Make this an enum? + @JsonProperty public String type; // TODO Make this an enum? + + @JsonProperty("force_set_state") + public Boolean forceSetState; + + // Present with coreInfo=true || sizeInfo=true unless otherwise specified + @JsonProperty public SegmentInfo segInfos; + + private Map unknownFields = new HashMap<>(); + + @JsonAnyGetter + public Map unknownProperties() { + return unknownFields; + } + + @JsonAnySetter + public void setUnknownProperty(String field, Object value) { + unknownFields.put(field, value); + } + } + + // Present with segments=true || coreInfo=true || sizeInfo=true || fieldInfo=true unless otherwise + // specified + + /** + * Same properties as {@link GetSegmentDataResponse}, but uses a different class to avoid + * inheriting "responseHeader", etc. + */ + public static class SegmentInfo { + @JsonProperty public GetSegmentDataResponse.SegmentSummary info; + + @JsonProperty public Map runningMerges; + + // Present with segments=true || sizeInfo=true || fieldInfo=true + @JsonProperty public Map segments; + + // Present with rawSize=true + @JsonProperty public GetSegmentDataResponse.RawSize rawSize; + + // Present only with fieldInfo=true + @JsonProperty public List fieldInfoLegend; + } +} diff --git a/solr/api/src/java/org/apache/solr/client/api/model/GetSegmentDataResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/GetSegmentDataResponse.java new file mode 100644 index 00000000000..b5e3714bfd3 --- /dev/null +++ b/solr/api/src/java/org/apache/solr/client/api/model/GetSegmentDataResponse.java @@ -0,0 +1,191 @@ +/* + * 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.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Response for {@link org.apache.solr.client.api.endpoint.SegmentsApi#getSegmentData(Boolean, + * Boolean, Boolean, Boolean, Boolean, Float, Boolean)} API + */ +public class GetSegmentDataResponse extends SolrJerseyResponse { + @JsonProperty public SegmentSummary info; + + @JsonProperty public Map runningMerges; + + @JsonProperty public Map segments; + + // Present only with fieldInfo=true + @JsonProperty public List fieldInfoLegend; + + // Present with rawSize=true + @JsonProperty public RawSize rawSize; + + // Always present in response + public static class SegmentSummary { + @JsonProperty public String minSegmentLuceneVersion; + @JsonProperty public String commitLuceneVersion; + @JsonProperty public Integer numSegments; + @JsonProperty public String segmentsFileName; + @JsonProperty public Integer totalMaxDoc; + // Typically keys are 'commitCommandVer' and 'commitTimeMSec' + @JsonProperty public Map userData; + + // Present for coreInfo=true only + @JsonProperty public CoreSummary core; + } + + // Always present in response, provided that the specified core has segments + public static class SingleSegmentData { + @JsonProperty public String name; + @JsonProperty public Integer delCount; + @JsonProperty public Integer softDelCount; + @JsonProperty public Boolean hasFieldUpdates; + @JsonProperty public Long sizeInBytes; + @JsonProperty public Integer size; + + // TODO - consider 'Instant' once SOLR-17608 is finished + @JsonProperty + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "YYYY-MM-DD'T'hh:mm:ss.S'Z'") + public Date age; + + @JsonProperty public String source; + @JsonProperty public String version; + @JsonProperty public Integer createdVersionMajor; + @JsonProperty public String minVersion; + @JsonProperty public SegmentDiagnosticInfo diagnostics; + @JsonProperty public Map attributes; + // Only present when index-sorting is in use + @JsonProperty public String sort; + @JsonProperty public Boolean mergeCandidate; + + // Present only when fieldInfo=true + @JsonProperty public Map fields; + + // Present only when sizeInfo=true + @JsonProperty("largestFiles") + public Map largestFilesByName; + } + + // Always present in response, provided that the specified core has segments + public static class SegmentSingleFieldInfo { + @JsonProperty public String flags; + @JsonProperty public Integer docCount; + @JsonProperty public Long termCount; + @JsonProperty public Long sumDocFreq; + @JsonProperty public Long sumTotalTermFreq; + @JsonProperty public String schemaType; + @JsonProperty public Map nonCompliant; + } + + // Always present in response + public static class SegmentDiagnosticInfo { + @JsonProperty("os.version") + public String osVersion; + + @JsonProperty("lucene.version") + public String luceneVersion; + + @JsonProperty public String source; + + // TODO - consider 'Instant' once SOLR-17608 is finished + @JsonProperty + @JsonFormat(shape = JsonFormat.Shape.NUMBER) + public Date timestamp; + + @JsonProperty("java.runtime.version") + public String javaRuntimeVersion; + + @JsonProperty public String os; + + @JsonProperty("java.vendor") + public String javaVendor; + + @JsonProperty("os.arch") + public String osArchitecture; + + private Map additionalDiagnostics = new HashMap<>(); + + @JsonAnyGetter + public Map getAdditionalDiagnostics() { + return additionalDiagnostics; + } + + @JsonAnySetter + public void getAdditionalDiagnostics(String field, Object value) { + additionalDiagnostics.put(field, value); + } + } + + // Present with coreInfo=true unless otherwise specified + public static class CoreSummary { + @JsonProperty public String startTime; + @JsonProperty public String dataDir; + @JsonProperty public String indexDir; + @JsonProperty public Double sizeInGB; + @JsonProperty public IndexWriterConfigSummary indexWriterConfig; + } + + // Present with coreInfo=true unless otherwise specified + + /** A serializable representation of Lucene's "LiveIndexWriterConfig" */ + public static class IndexWriterConfigSummary { + @JsonProperty public String analyzer; + @JsonProperty public Double ramBufferSizeMB; + @JsonProperty public Integer maxBufferedDocs; + @JsonProperty public String mergedSegmentWarmer; + @JsonProperty public String delPolicy; + @JsonProperty public String commit; + @JsonProperty public String openMode; + @JsonProperty public String similarity; + @JsonProperty public String mergeScheduler; + @JsonProperty public String codec; + @JsonProperty public String infoStream; + @JsonProperty public String mergePolicy; + @JsonProperty public Boolean readerPooling; + @JsonProperty public Integer perThreadHardLimitMB; + @JsonProperty public Boolean useCompoundFile; + @JsonProperty public Boolean commitOnClose; + @JsonProperty public String indexSort; + @JsonProperty public Boolean checkPendingFlushOnUpdate; + @JsonProperty public String softDeletesField; + @JsonProperty public Long maxFullFlushMergeWaitMillis; + @JsonProperty public String leafSorter; + @JsonProperty public String eventListener; + @JsonProperty public String parentField; + @JsonProperty public String writer; + } + + // Present with rawSize=true unless otherwise specified + public static class RawSize { + @JsonProperty public Map fieldsBySize; + @JsonProperty public Map typesBySize; + + // Present with rawSizeDetails=true + @JsonProperty public Object details; + + // Present with rawSizeSummary=true + @JsonProperty public Map summary; + } +} diff --git a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java index d156710a675..1ecb290fa0e 100644 --- a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java +++ b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java @@ -188,6 +188,8 @@ public void call(SolrQueryRequest req, SolrQueryResponse rsp) { Thread.currentThread().setContextClassLoader(core.getResourceLoader().getClassLoader()); this.path = path = path.substring(prefix.length() + pathSegments.get(1).length() + 2); + // Core-level API, so populate "collection" template val + parts.put(COLLECTION_PROP, origCorename); Api apiInfo = getApiInfo(core.getRequestHandlers(), path, req.getMethod(), fullPath, parts); if (isCompositeApi && apiInfo instanceof CompositeApi) { ((CompositeApi) this.api).add(apiInfo); diff --git a/solr/core/src/java/org/apache/solr/handler/admin/ColStatus.java b/solr/core/src/java/org/apache/solr/handler/admin/ColStatus.java index e7fe44e0fe6..234682e8473 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/ColStatus.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/ColStatus.java @@ -25,6 +25,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; +import org.apache.solr.client.api.model.GetSegmentDataResponse; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.io.SolrClientCache; @@ -41,6 +42,8 @@ import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.SimpleOrderedMap; +import org.apache.solr.common.util.Utils; +import org.apache.solr.jersey.SolrJacksonMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -80,12 +83,16 @@ public void getColStatus(NamedList results) { collections = Collections.singleton(col); } boolean withFieldInfo = props.getBool(FIELD_INFO_PROP, false); - boolean withSegments = props.getBool(SEGMENTS_PROP, false); boolean withCoreInfo = props.getBool(CORE_INFO_PROP, false); boolean withSizeInfo = props.getBool(SIZE_INFO_PROP, false); boolean withRawSizeInfo = props.getBool(RAW_SIZE_PROP, false); boolean withRawSizeSummary = props.getBool(RAW_SIZE_SUMMARY_PROP, false); boolean withRawSizeDetails = props.getBool(RAW_SIZE_DETAILS_PROP, false); + // FieldInfo and SizeInfo imply segments=true, since they add to the data reported about each + // segment + boolean withSegments = props.getBool(SEGMENTS_PROP, false); + withSegments |= withFieldInfo || withSizeInfo; + Object samplingPercentVal = props.get(RAW_SIZE_SAMPLING_PERCENT_PROP); Float samplingPercent = samplingPercentVal != null ? Float.parseFloat(String.valueOf(samplingPercentVal)) : null; @@ -94,6 +101,7 @@ public void getColStatus(NamedList results) { } boolean getSegments = false; if (withFieldInfo + || withSegments || withSizeInfo || withCoreInfo || withRawSizeInfo @@ -196,32 +204,35 @@ public void getColStatus(NamedList results) { } QueryRequest req = new QueryRequest(params); NamedList rsp = client.request(req); - rsp.remove("responseHeader"); - leaderMap.add("segInfos", rsp); - NamedList segs = (NamedList) rsp.get("segments"); + final var segmentResponse = + SolrJacksonMapper.getObjectMapper().convertValue(rsp, GetSegmentDataResponse.class); + segmentResponse.responseHeader = null; + + final var segs = segmentResponse.segments; if (segs != null) { - for (Map.Entry entry : segs) { - NamedList fields = - (NamedList) ((NamedList) entry.getValue()).get("fields"); - if (fields != null) { - for (Map.Entry fEntry : fields) { - Object nc = ((NamedList) fEntry.getValue()).get("nonCompliant"); - if (nc != null) { + for (Map.Entry entry : + segs.entrySet()) { + final var fieldInfoByName = entry.getValue().fields; + if (fieldInfoByName != null) { + for (Map.Entry fEntry : + fieldInfoByName.entrySet()) { + if (fEntry.getValue().nonCompliant != null) { nonCompliant.add(fEntry.getKey()); } } } if (!withFieldInfo) { - ((NamedList) entry.getValue()).remove("fields"); + entry.getValue().fields = null; } } } if (!withSegments) { - rsp.remove("segments"); + segmentResponse.segments = null; } if (!withFieldInfo) { - rsp.remove("fieldInfoLegend"); + segmentResponse.fieldInfoLegend = null; } + leaderMap.add("segInfos", Utils.reflectToMap(segmentResponse)); } catch (SolrServerException | IOException e) { log.warn("Error getting details of replica segments from {}", url, e); } 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 df6ba086d06..aefc1033d5e 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 @@ -174,7 +174,7 @@ import org.apache.solr.handler.admin.api.BalanceShardUnique; import org.apache.solr.handler.admin.api.ClusterProperty; import org.apache.solr.handler.admin.api.CollectionProperty; -import org.apache.solr.handler.admin.api.CollectionStatusAPI; +import org.apache.solr.handler.admin.api.CollectionStatus; import org.apache.solr.handler.admin.api.CreateAlias; import org.apache.solr.handler.admin.api.CreateCollection; import org.apache.solr.handler.admin.api.CreateCollectionBackup; @@ -539,11 +539,8 @@ public enum CollectionOperation implements CollectionOp { ColStatus.RAW_SIZE_SAMPLING_PERCENT_PROP, ColStatus.SIZE_INFO_PROP); - new ColStatus( - h.coreContainer.getSolrClientCache(), - h.coreContainer.getZkController().getZkStateReader().getClusterState(), - new ZkNodeProps(props)) - .getColStatus(rsp.getValues()); + CollectionStatus.populateColStatusData( + h.coreContainer, new ZkNodeProps(props), rsp.getValues()); return null; }), DELETE_OP( @@ -1360,6 +1357,7 @@ public Collection> getJerseyResources() { CreateReplica.class, AddReplicaProperty.class, BalanceShardUnique.class, + CollectionStatus.class, CreateAlias.class, CreateCollection.class, CreateCollectionBackup.class, @@ -1399,7 +1397,6 @@ public Collection getApis() { apis.addAll(AnnotatedApi.getApis(new ModifyCollectionAPI(this))); apis.addAll(AnnotatedApi.getApis(new MoveReplicaAPI(this))); apis.addAll(AnnotatedApi.getApis(new RebalanceLeadersAPI(this))); - apis.addAll(AnnotatedApi.getApis(new CollectionStatusAPI(this))); return apis; } diff --git a/solr/core/src/java/org/apache/solr/handler/admin/SegmentsInfoRequestHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/SegmentsInfoRequestHandler.java index 93cdf071a1c..fd1378b6597 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/SegmentsInfoRequestHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/SegmentsInfoRequestHandler.java @@ -16,57 +16,20 @@ */ package org.apache.solr.handler.admin; -import static org.apache.lucene.index.IndexOptions.DOCS; -import static org.apache.lucene.index.IndexOptions.DOCS_AND_FREQS; -import static org.apache.lucene.index.IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS; -import static org.apache.solr.common.params.CommonParams.NAME; - -import java.io.IOException; -import java.lang.invoke.MethodHandles; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; +import java.util.Collection; import java.util.List; -import java.util.stream.Collectors; -import org.apache.lucene.index.DocValuesType; -import org.apache.lucene.index.FieldInfo; -import org.apache.lucene.index.FieldInfos; -import org.apache.lucene.index.FilterLeafReader; -import org.apache.lucene.index.IndexOptions; -import org.apache.lucene.index.IndexWriter; -import org.apache.lucene.index.LeafMetaData; -import org.apache.lucene.index.LeafReader; -import org.apache.lucene.index.LeafReaderContext; -import org.apache.lucene.index.MergePolicy; -import org.apache.lucene.index.MergePolicy.MergeSpecification; -import org.apache.lucene.index.MergePolicy.OneMerge; -import org.apache.lucene.index.MergeTrigger; -import org.apache.lucene.index.SegmentCommitInfo; -import org.apache.lucene.index.SegmentInfos; -import org.apache.lucene.index.SegmentReader; -import org.apache.lucene.index.Terms; -import org.apache.lucene.store.Directory; -import org.apache.lucene.util.RamUsageEstimator; -import org.apache.lucene.util.Version; -import org.apache.solr.common.luke.FieldFlag; -import org.apache.solr.common.util.Pair; -import org.apache.solr.common.util.SimpleOrderedMap; -import org.apache.solr.core.SolrCore; +import org.apache.solr.api.JerseyResource; +import org.apache.solr.client.api.model.SolrJerseyResponse; +import org.apache.solr.common.params.SolrParams; import org.apache.solr.handler.RequestHandlerBase; +import org.apache.solr.handler.admin.api.GetSegmentData; +import org.apache.solr.handler.api.V2ApiUtils; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; -import org.apache.solr.schema.IndexSchema; -import org.apache.solr.schema.SchemaField; -import org.apache.solr.search.SolrIndexSearcher; import org.apache.solr.security.AuthorizationContext; -import org.apache.solr.update.SolrIndexWriter; -import org.apache.solr.util.RefCounted; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** This handler exposes information about last commit generation segments */ public class SegmentsInfoRequestHandler extends RequestHandlerBase { - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); public static final String FIELD_INFO_PARAM = "fieldInfo"; public static final String CORE_INFO_PARAM = "coreInfo"; @@ -76,385 +39,22 @@ public class SegmentsInfoRequestHandler extends RequestHandlerBase { public static final String RAW_SIZE_DETAILS_PARAM = "rawSizeDetails"; public static final String RAW_SIZE_SAMPLING_PERCENT_PARAM = "rawSizeSamplingPercent"; - private static final List FI_LEGEND; - - static { - FI_LEGEND = - Arrays.asList( - FieldFlag.INDEXED.toString(), - FieldFlag.DOC_VALUES.toString(), - "xxx - DocValues type", - FieldFlag.TERM_VECTOR_STORED.toString(), - FieldFlag.OMIT_NORMS.toString(), - FieldFlag.OMIT_TF.toString(), - FieldFlag.OMIT_POSITIONS.toString(), - FieldFlag.STORE_OFFSETS_WITH_POSITIONS.toString(), - "p - field has payloads", - "s - field uses soft deletes", - ":x:x:x - point data dim : index dim : num bytes"); - } - @Override public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception { - getSegmentsInfo(req, rsp); - rsp.setHttpCaching(false); - } - - private static final double GB = 1024.0 * 1024.0 * 1024.0; - - private void getSegmentsInfo(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception { - boolean withFieldInfo = req.getParams().getBool(FIELD_INFO_PARAM, false); - boolean withCoreInfo = req.getParams().getBool(CORE_INFO_PARAM, false); - boolean withSizeInfo = req.getParams().getBool(SIZE_INFO_PARAM, false); - boolean withRawSizeInfo = req.getParams().getBool(RAW_SIZE_PARAM, false); - boolean withRawSizeSummary = req.getParams().getBool(RAW_SIZE_SUMMARY_PARAM, false); - boolean withRawSizeDetails = req.getParams().getBool(RAW_SIZE_DETAILS_PARAM, false); - if (withRawSizeSummary || withRawSizeDetails) { - withRawSizeInfo = true; - } - SolrIndexSearcher searcher = req.getSearcher(); - - SegmentInfos infos = SegmentInfos.readLatestCommit(searcher.getIndexReader().directory()); - - SimpleOrderedMap segmentInfos = new SimpleOrderedMap<>(); - - SolrCore core = req.getCore(); - SimpleOrderedMap infosInfo = new SimpleOrderedMap<>(); - Version minVersion = infos.getMinSegmentLuceneVersion(); - if (minVersion != null) { - infosInfo.add("minSegmentLuceneVersion", minVersion.toString()); - } - Version commitVersion = infos.getCommitLuceneVersion(); - if (commitVersion != null) { - infosInfo.add("commitLuceneVersion", commitVersion.toString()); - } - infosInfo.add("numSegments", infos.size()); - infosInfo.add("segmentsFileName", infos.getSegmentsFileName()); - infosInfo.add("totalMaxDoc", infos.totalMaxDoc()); - infosInfo.add("userData", infos.userData); - if (withCoreInfo) { - SimpleOrderedMap coreInfo = new SimpleOrderedMap<>(); - infosInfo.add("core", coreInfo); - coreInfo.add( - "startTime", core.getStartTimeStamp().getTime() + "(" + core.getStartTimeStamp() + ")"); - coreInfo.add("dataDir", core.getDataDir()); - coreInfo.add("indexDir", core.getIndexDir()); - coreInfo.add("sizeInGB", (double) core.getIndexSize() / GB); - - RefCounted iwRef = core.getSolrCoreState().getIndexWriter(core); - if (iwRef != null) { - try { - IndexWriter iw = iwRef.get(); - String iwConfigStr = iw.getConfig().toString(); - SimpleOrderedMap iwConfig = new SimpleOrderedMap<>(); - // meh ... - String[] lines = iwConfigStr.split("\\n"); - for (String line : lines) { - String[] parts = line.split("="); - if (parts.length < 2) { - continue; - } - iwConfig.add(parts[0], parts[1]); - } - coreInfo.add("indexWriterConfig", iwConfig); - } finally { - iwRef.decref(); - } - } - } - SimpleOrderedMap segmentInfo; - List sortable = new ArrayList<>(infos.asList()); - // Order by the number of live docs. The display is logarithmic so it is a little jumbled - // visually - sortable.sort( - (s1, s2) -> (s2.info.maxDoc() - s2.getDelCount()) - (s1.info.maxDoc() - s1.getDelCount())); - - List mergeCandidates = new ArrayList<>(); - SimpleOrderedMap runningMerges = getMergeInformation(req, infos, mergeCandidates); - List leafContexts = searcher.getIndexReader().leaves(); - IndexSchema schema = req.getSchema(); - for (SegmentCommitInfo segmentCommitInfo : sortable) { - segmentInfo = - getSegmentInfo(segmentCommitInfo, withSizeInfo, withFieldInfo, leafContexts, schema); - if (mergeCandidates.contains(segmentCommitInfo.info.name)) { - segmentInfo.add("mergeCandidate", true); - } - segmentInfos.add((String) segmentInfo.get(NAME), segmentInfo); - } - - rsp.add("info", infosInfo); - if (runningMerges.size() > 0) { - rsp.add("runningMerges", runningMerges); - } - if (withFieldInfo) { - rsp.add("fieldInfoLegend", FI_LEGEND); - } - rsp.add("segments", segmentInfos); - if (withRawSizeInfo) { - IndexSizeEstimator estimator = - new IndexSizeEstimator( - searcher.getRawReader(), 20, 100, withRawSizeSummary, withRawSizeDetails); - Object samplingPercentVal = req.getParams().get(RAW_SIZE_SAMPLING_PERCENT_PARAM); - if (samplingPercentVal != null) { - estimator.setSamplingPercent(Float.parseFloat(String.valueOf(samplingPercentVal))); - } - IndexSizeEstimator.Estimate estimate = estimator.estimate(); - SimpleOrderedMap estimateMap = new SimpleOrderedMap<>(); - // make the units more user-friendly - estimateMap.add(IndexSizeEstimator.FIELDS_BY_SIZE, estimate.getHumanReadableFieldsBySize()); - estimateMap.add(IndexSizeEstimator.TYPES_BY_SIZE, estimate.getHumanReadableTypesBySize()); - if (estimate.getSummary() != null) { - estimateMap.add(IndexSizeEstimator.SUMMARY, estimate.getSummary()); - } - if (estimate.getDetails() != null) { - estimateMap.add(IndexSizeEstimator.DETAILS, estimate.getDetails()); - } - rsp.add("rawSize", estimateMap); - } - } - - private SimpleOrderedMap getSegmentInfo( - SegmentCommitInfo segmentCommitInfo, - boolean withSizeInfo, - boolean withFieldInfos, - List leafContexts, - IndexSchema schema) - throws IOException { - SimpleOrderedMap segmentInfoMap = new SimpleOrderedMap<>(); - - segmentInfoMap.add(NAME, segmentCommitInfo.info.name); - segmentInfoMap.add("delCount", segmentCommitInfo.getDelCount()); - segmentInfoMap.add("softDelCount", segmentCommitInfo.getSoftDelCount()); - segmentInfoMap.add("hasFieldUpdates", segmentCommitInfo.hasFieldUpdates()); - segmentInfoMap.add("sizeInBytes", segmentCommitInfo.sizeInBytes()); - segmentInfoMap.add("size", segmentCommitInfo.info.maxDoc()); - Long timestamp = Long.parseLong(segmentCommitInfo.info.getDiagnostics().get("timestamp")); - segmentInfoMap.add("age", new Date(timestamp)); - segmentInfoMap.add("source", segmentCommitInfo.info.getDiagnostics().get("source")); - segmentInfoMap.add("version", segmentCommitInfo.info.getVersion().toString()); - // don't open a new SegmentReader - try to find the right one from the leaf contexts - SegmentReader seg = null; - for (LeafReaderContext lrc : leafContexts) { - LeafReader leafReader = lrc.reader(); - leafReader = FilterLeafReader.unwrap(leafReader); - if (leafReader instanceof SegmentReader sr) { - if (sr.getSegmentInfo().info.equals(segmentCommitInfo.info)) { - seg = sr; - break; - } - } - } - if (seg != null) { - LeafMetaData metaData = seg.getMetaData(); - if (metaData != null) { - segmentInfoMap.add("createdVersionMajor", metaData.getCreatedVersionMajor()); - segmentInfoMap.add("minVersion", metaData.getMinVersion().toString()); - if (metaData.getSort() != null) { - segmentInfoMap.add("sort", metaData.getSort().toString()); - } - } - } - if (!segmentCommitInfo.info.getDiagnostics().isEmpty()) { - segmentInfoMap.add("diagnostics", segmentCommitInfo.info.getDiagnostics()); - } - if (!segmentCommitInfo.info.getAttributes().isEmpty()) { - segmentInfoMap.add("attributes", segmentCommitInfo.info.getAttributes()); - } - if (withSizeInfo) { - Directory dir = segmentCommitInfo.info.dir; - List> files = - segmentCommitInfo.files().stream() - .map( - f -> { - long size = -1; - try { - size = dir.fileLength(f); - } catch (IOException e) { - } - return new Pair(f, size); - }) - .sorted( - (p1, p2) -> { - if (p1.second() > p2.second()) { - return -1; - } else if (p1.second() < p2.second()) { - return 1; - } else { - return 0; - } - }) - .collect(Collectors.toList()); - if (!files.isEmpty()) { - SimpleOrderedMap topFiles = new SimpleOrderedMap<>(); - for (int i = 0; i < Math.min(files.size(), 5); i++) { - Pair p = files.get(i); - topFiles.add(p.first(), RamUsageEstimator.humanReadableUnits(p.second())); - } - segmentInfoMap.add("largestFiles", topFiles); - } - } - if (withFieldInfos) { - if (seg == null) { - log.debug( - "Skipping segment info - not available as a SegmentReader: {}", segmentCommitInfo); - } else { - FieldInfos fis = seg.getFieldInfos(); - SimpleOrderedMap fields = new SimpleOrderedMap<>(); - for (FieldInfo fi : fis) { - fields.add(fi.name, getFieldInfo(seg, fi, schema)); - } - segmentInfoMap.add("fields", fields); - } - } - - return segmentInfoMap; - } + final SolrParams params = req.getParams(); + final GetSegmentData segmentDataApi = new GetSegmentData(req.getCore(), req, rsp); + final SolrJerseyResponse response = + segmentDataApi.getSegmentData( + params.getBool(CORE_INFO_PARAM), + params.getBool(FIELD_INFO_PARAM), + params.getBool(RAW_SIZE_PARAM), + params.getBool(RAW_SIZE_SUMMARY_PARAM), + params.getBool(RAW_SIZE_DETAILS_PARAM), + params.getFloat(RAW_SIZE_SAMPLING_PERCENT_PARAM), + params.getBool(SIZE_INFO_PARAM)); + V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, response); - private SimpleOrderedMap getFieldInfo( - SegmentReader reader, FieldInfo fi, IndexSchema schema) { - SimpleOrderedMap fieldFlags = new SimpleOrderedMap<>(); - StringBuilder flags = new StringBuilder(); - IndexOptions opts = fi.getIndexOptions(); - flags.append((opts != IndexOptions.NONE) ? FieldFlag.INDEXED.getAbbreviation() : '-'); - DocValuesType dvt = fi.getDocValuesType(); - if (dvt != DocValuesType.NONE) { - flags.append(FieldFlag.DOC_VALUES.getAbbreviation()); - switch (dvt) { - case NUMERIC: - flags.append("num"); - break; - case BINARY: - flags.append("bin"); - break; - case SORTED: - flags.append("srt"); - break; - case SORTED_NUMERIC: - flags.append("srn"); - break; - case SORTED_SET: - flags.append("srs"); - break; - default: - flags.append("???"); // should not happen - } - } else { - flags.append("----"); - } - flags.append((fi.hasVectors()) ? FieldFlag.TERM_VECTOR_STORED.getAbbreviation() : '-'); - flags.append((fi.omitsNorms()) ? FieldFlag.OMIT_NORMS.getAbbreviation() : '-'); - - flags.append((DOCS == opts) ? FieldFlag.OMIT_TF.getAbbreviation() : '-'); - - flags.append((DOCS_AND_FREQS == opts) ? FieldFlag.OMIT_POSITIONS.getAbbreviation() : '-'); - - flags.append( - (DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS == opts) - ? FieldFlag.STORE_OFFSETS_WITH_POSITIONS.getAbbreviation() - : '-'); - - flags.append((fi.hasPayloads() ? "p" : "-")); - flags.append((fi.isSoftDeletesField() ? "s" : "-")); - if (fi.getPointDimensionCount() > 0 || fi.getPointIndexDimensionCount() > 0) { - flags.append(":"); - flags.append(fi.getPointDimensionCount()).append(':'); - flags.append(fi.getPointIndexDimensionCount()).append(':'); - flags.append(fi.getPointNumBytes()); - } - - fieldFlags.add("flags", flags.toString()); - try { - Terms terms = reader.terms(fi.name); - if (terms != null) { - fieldFlags.add("docCount", terms.getDocCount()); - fieldFlags.add("termCount", terms.size()); - fieldFlags.add("sumDocFreq", terms.getSumDocFreq()); - fieldFlags.add("sumTotalTermFreq", terms.getSumTotalTermFreq()); - } - } catch (Exception e) { - log.debug("Exception retrieving term stats for field {}", fi.name, e); - } - - // probably too much detail? - // Map attributes = fi.attributes(); - // if (!attributes.isEmpty()) { - // fieldFlags.add("attributes", attributes); - // } - - // check compliance of the index with the current schema - SchemaField sf = schema.getFieldOrNull(fi.name); - boolean hasPoints = fi.getPointDimensionCount() > 0 || fi.getPointIndexDimensionCount() > 0; - - if (sf != null) { - fieldFlags.add("schemaType", sf.getType().getTypeName()); - SimpleOrderedMap nonCompliant = new SimpleOrderedMap<>(); - if (sf.hasDocValues() - && fi.getDocValuesType() == DocValuesType.NONE - && fi.getIndexOptions() != IndexOptions.NONE) { - nonCompliant.add( - "docValues", "schema=" + sf.getType().getUninversionType(sf) + ", segment=false"); - } - if (!sf.hasDocValues() && fi.getDocValuesType() != DocValuesType.NONE) { - nonCompliant.add("docValues", "schema=false, segment=" + fi.getDocValuesType().toString()); - } - if (!sf.isPolyField()) { // difficult to find all sub-fields in a general way - if (sf.indexed() != ((fi.getIndexOptions() != IndexOptions.NONE) || hasPoints)) { - nonCompliant.add( - "indexed", "schema=" + sf.indexed() + ", segment=" + fi.getIndexOptions()); - } - } - if (!hasPoints && (sf.omitNorms() != fi.omitsNorms())) { - nonCompliant.add("omitNorms", "schema=" + sf.omitNorms() + ", segment=" + fi.omitsNorms()); - } - if (sf.storeTermVector() != fi.hasVectors()) { - nonCompliant.add( - "termVectors", "schema=" + sf.storeTermVector() + ", segment=" + fi.hasVectors()); - } - if (sf.storeOffsetsWithPositions() - != (fi.getIndexOptions() == IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS)) { - nonCompliant.add( - "storeOffsetsWithPositions", - "schema=" + sf.storeOffsetsWithPositions() + ", segment=" + fi.getIndexOptions()); - } - - if (nonCompliant.size() > 0) { - nonCompliant.add("schemaField", sf.toString()); - fieldFlags.add("nonCompliant", nonCompliant); - } - } else { - fieldFlags.add("schemaType", "(UNKNOWN)"); - } - return fieldFlags; - } - - // returns a map of currently running merges, and populates a list of candidate segments for merge - private SimpleOrderedMap getMergeInformation( - SolrQueryRequest req, SegmentInfos infos, List mergeCandidates) throws IOException { - SimpleOrderedMap result = new SimpleOrderedMap<>(); - RefCounted refCounted = - req.getCore().getSolrCoreState().getIndexWriter(req.getCore()); - try { - IndexWriter indexWriter = refCounted.get(); - if (indexWriter instanceof SolrIndexWriter) { - result.addAll(((SolrIndexWriter) indexWriter).getRunningMerges()); - } - // get chosen merge policy - MergePolicy mp = indexWriter.getConfig().getMergePolicy(); - // Find merges - MergeSpecification findMerges = mp.findMerges(MergeTrigger.EXPLICIT, infos, indexWriter); - if (findMerges != null && findMerges.merges != null && findMerges.merges.size() > 0) { - for (OneMerge merge : findMerges.merges) { - // TODO: add merge grouping - for (SegmentCommitInfo mergeSegmentInfo : merge.segments) { - mergeCandidates.add(mergeSegmentInfo.info.name); - } - } - } - - return result; - } finally { - refCounted.decref(); - } + rsp.setHttpCaching(false); } @Override @@ -471,4 +71,14 @@ public Category getCategory() { public Name getPermissionName(AuthorizationContext request) { return Name.METRICS_READ_PERM; } + + @Override + public Boolean registerV2() { + return Boolean.TRUE; + } + + @Override + public Collection> getJerseyResources() { + return List.of(GetSegmentData.class); + } } diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionStatus.java b/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionStatus.java new file mode 100644 index 00000000000..f80b6363071 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionStatus.java @@ -0,0 +1,97 @@ +/* + * 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.cloud.ZkStateReader.COLLECTION_PROP; + +import jakarta.inject.Inject; +import org.apache.solr.client.api.endpoint.CollectionStatusApi; +import org.apache.solr.client.api.model.CollectionStatusResponse; +import org.apache.solr.common.cloud.ZkNodeProps; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.core.CoreContainer; +import org.apache.solr.handler.admin.ColStatus; +import org.apache.solr.jersey.PermissionName; +import org.apache.solr.jersey.SolrJacksonMapper; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.response.SolrQueryResponse; +import org.apache.solr.security.PermissionNameProvider; + +/** V2 API implementation for {@link CollectionStatusApi}. */ +public class CollectionStatus extends AdminAPIBase implements CollectionStatusApi { + + @Inject + public CollectionStatus( + CoreContainer coreContainer, + SolrQueryRequest solrQueryRequest, + SolrQueryResponse solrQueryResponse) { + super(coreContainer, solrQueryRequest, solrQueryResponse); + } + + @Override + @PermissionName(PermissionNameProvider.Name.COLL_READ_PERM) + public CollectionStatusResponse getCollectionStatus( + String collectionName, + Boolean coreInfo, + Boolean segments, + Boolean fieldInfo, + Boolean rawSize, + Boolean rawSizeSummary, + Boolean rawSizeDetails, + Float rawSizeSamplingPercent, + Boolean sizeInfo) + throws Exception { + recordCollectionForLogAndTracing(collectionName, solrQueryRequest); + + final var params = new ModifiableSolrParams(); + params.set(COLLECTION_PROP, collectionName); + params.setNonNull(ColStatus.CORE_INFO_PROP, coreInfo); + params.setNonNull(ColStatus.SEGMENTS_PROP, segments); + params.setNonNull(ColStatus.FIELD_INFO_PROP, fieldInfo); + params.setNonNull(ColStatus.RAW_SIZE_PROP, rawSize); + params.setNonNull(ColStatus.RAW_SIZE_SUMMARY_PROP, rawSizeSummary); + params.setNonNull(ColStatus.RAW_SIZE_DETAILS_PROP, rawSizeDetails); + params.setNonNull(ColStatus.RAW_SIZE_SAMPLING_PERCENT_PROP, rawSizeSamplingPercent); + params.setNonNull(ColStatus.SIZE_INFO_PROP, sizeInfo); + + final var nlResponse = new NamedList<>(); + populateColStatusData(coreContainer, new ZkNodeProps(params), nlResponse); + + // v2 API does not support requesting the status of multiple collections simultaneously as its + // counterpart does, and its response looks slightly different as a result. Primarily, the + // v2 response eschews a level of nesting that necessitated by the multi-collection nature of + // v1. These tweaks are made below before returning. + final var colStatusResponse = + SolrJacksonMapper.getObjectMapper() + .convertValue(nlResponse.get(collectionName), CollectionStatusResponse.class); + colStatusResponse.name = collectionName; + return colStatusResponse; + } + + // TODO Modify ColStatus to produce a CollectionStatusResponse instead of a NL + public static void populateColStatusData( + CoreContainer coreContainer, ZkNodeProps params, NamedList colStatusSink) { + final var colStatusAssembler = + new ColStatus( + coreContainer.getSolrClientCache(), + coreContainer.getZkController().getZkStateReader().getClusterState(), + params); + colStatusAssembler.getColStatus(colStatusSink); + } +} diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionStatusAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionStatusAPI.java deleted file mode 100644 index 4b7eabe7226..00000000000 --- a/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionStatusAPI.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 static org.apache.solr.common.params.CommonParams.ACTION; -import static org.apache.solr.common.params.CoreAdminParams.COLLECTION; -import static org.apache.solr.handler.ClusterAPI.wrapParams; -import static org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PERM; - -import java.lang.invoke.MethodHandles; -import org.apache.solr.api.EndPoint; -import org.apache.solr.common.cloud.ZkStateReader; -import org.apache.solr.common.params.CollectionParams; -import org.apache.solr.handler.admin.CollectionsHandler; -import org.apache.solr.request.SolrQueryRequest; -import org.apache.solr.response.SolrQueryResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * V2 API for displaying basic information about a single collection. - * - *

This API (GET /v2/collections/collectionName) is analogous to the v1 - * /admin/collections?action=CLUSTERSTATUS&collection=collectionName command. - */ -public class CollectionStatusAPI { - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private final CollectionsHandler collectionsHandler; - - public CollectionStatusAPI(CollectionsHandler collectionsHandler) { - this.collectionsHandler = collectionsHandler; - } - - @EndPoint( - path = {"/c/{collection}", "/collections/{collection}"}, - method = GET, - permission = COLL_READ_PERM) - public void getCollectionStatus(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception { - req = - wrapParams( - req, // 'req' can have a 'shard' param - ACTION, - CollectionParams.CollectionAction.CLUSTERSTATUS.toString(), - COLLECTION, - req.getPathTemplateValues().get(ZkStateReader.COLLECTION_PROP)); - collectionsHandler.handleRequestBody(req, rsp); - } -} diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/GetSegmentData.java b/solr/core/src/java/org/apache/solr/handler/admin/api/GetSegmentData.java new file mode 100644 index 00000000000..ceec55ea33f --- /dev/null +++ b/solr/core/src/java/org/apache/solr/handler/admin/api/GetSegmentData.java @@ -0,0 +1,501 @@ +/* + * 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.lucene.index.IndexOptions.DOCS; +import static org.apache.lucene.index.IndexOptions.DOCS_AND_FREQS; +import static org.apache.lucene.index.IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS; + +import jakarta.inject.Inject; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.lucene.index.DocValuesType; +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.FieldInfos; +import org.apache.lucene.index.FilterLeafReader; +import org.apache.lucene.index.IndexOptions; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.LeafMetaData; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.LiveIndexWriterConfig; +import org.apache.lucene.index.MergePolicy; +import org.apache.lucene.index.MergeTrigger; +import org.apache.lucene.index.SegmentCommitInfo; +import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.index.SegmentReader; +import org.apache.lucene.index.Terms; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.RamUsageEstimator; +import org.apache.lucene.util.Version; +import org.apache.solr.api.JerseyResource; +import org.apache.solr.client.api.endpoint.SegmentsApi; +import org.apache.solr.client.api.model.GetSegmentDataResponse; +import org.apache.solr.common.luke.FieldFlag; +import org.apache.solr.common.util.Pair; +import org.apache.solr.core.SolrCore; +import org.apache.solr.handler.admin.IndexSizeEstimator; +import org.apache.solr.jersey.PermissionName; +import org.apache.solr.jersey.SolrJacksonMapper; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.response.SolrQueryResponse; +import org.apache.solr.schema.IndexSchema; +import org.apache.solr.schema.SchemaField; +import org.apache.solr.search.SolrIndexSearcher; +import org.apache.solr.security.PermissionNameProvider; +import org.apache.solr.update.SolrIndexWriter; +import org.apache.solr.util.RefCounted; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * V2 API implementation for {@link SegmentsApi} + * + *

Equivalent to the v1 /solr/coreName/admin/segments endpoint. + */ +public class GetSegmentData extends JerseyResource implements SegmentsApi { + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final double GB = 1024.0 * 1024.0 * 1024.0; + + private static final List FI_LEGEND = + Arrays.asList( + FieldFlag.INDEXED.toString(), + FieldFlag.DOC_VALUES.toString(), + "xxx - DocValues type", + FieldFlag.TERM_VECTOR_STORED.toString(), + FieldFlag.OMIT_NORMS.toString(), + FieldFlag.OMIT_TF.toString(), + FieldFlag.OMIT_POSITIONS.toString(), + FieldFlag.STORE_OFFSETS_WITH_POSITIONS.toString(), + "p - field has payloads", + "s - field uses soft deletes", + ":x:x:x - point data dim : index dim : num bytes"); + + protected final SolrCore solrCore; + protected final SolrQueryRequest solrQueryRequest; + protected final SolrQueryResponse solrQueryResponse; + + @Inject + public GetSegmentData(SolrCore solrCore, SolrQueryRequest req, SolrQueryResponse rsp) { + this.solrCore = solrCore; + this.solrQueryRequest = req; + this.solrQueryResponse = rsp; + } + + @Override + @PermissionName(PermissionNameProvider.Name.METRICS_READ_PERM) + public GetSegmentDataResponse getSegmentData( + Boolean coreInfo, + Boolean fieldInfo, + Boolean rawSize, + Boolean rawSizeSummary, + Boolean rawSizeDetails, + Float rawSizeSamplingPercent, + Boolean sizeInfo) + throws Exception { + boolean withFieldInfo = Boolean.TRUE.equals(fieldInfo); + boolean withCoreInfo = Boolean.TRUE.equals(coreInfo); + boolean withSizeInfo = Boolean.TRUE.equals(sizeInfo); + boolean withRawSizeInfo = Boolean.TRUE.equals(rawSize); + boolean withRawSizeSummary = Boolean.TRUE.equals(rawSizeSummary); + boolean withRawSizeDetails = Boolean.TRUE.equals(rawSizeDetails); + if (withRawSizeSummary || withRawSizeDetails) { + withRawSizeInfo = true; + } + SolrIndexSearcher searcher = solrQueryRequest.getSearcher(); + SolrCore core = solrQueryRequest.getCore(); + + final var response = new GetSegmentDataResponse(); + + SegmentInfos infos = SegmentInfos.readLatestCommit(searcher.getIndexReader().directory()); + response.info = new GetSegmentDataResponse.SegmentSummary(); + Version minVersion = infos.getMinSegmentLuceneVersion(); + if (minVersion != null) { + response.info.minSegmentLuceneVersion = minVersion.toString(); + } + Version commitVersion = infos.getCommitLuceneVersion(); + if (commitVersion != null) { + response.info.commitLuceneVersion = commitVersion.toString(); + } + response.info.numSegments = infos.size(); + response.info.segmentsFileName = infos.getSegmentsFileName(); + response.info.totalMaxDoc = infos.totalMaxDoc(); + response.info.userData = infos.userData; + + if (withCoreInfo) { + final var coreSummary = new GetSegmentDataResponse.CoreSummary(); + response.info.core = coreSummary; + coreSummary.startTime = + core.getStartTimeStamp().getTime() + "(" + core.getStartTimeStamp() + ")"; + coreSummary.dataDir = core.getDataDir(); + coreSummary.indexDir = core.getIndexDir(); + coreSummary.sizeInGB = (double) core.getIndexSize() / GB; + + RefCounted iwRef = core.getSolrCoreState().getIndexWriter(core); + if (iwRef != null) { + try { + IndexWriter iw = iwRef.get(); + coreSummary.indexWriterConfig = convertIndexWriterConfigToResponse(iw.getConfig()); + } finally { + iwRef.decref(); + } + } + } + + List sortable = new ArrayList<>(infos.asList()); + // Order by the number of live docs. The display is logarithmic so it is a little jumbled + // visually + sortable.sort( + (s1, s2) -> (s2.info.maxDoc() - s2.getDelCount()) - (s1.info.maxDoc() - s1.getDelCount())); + + List mergeCandidates = new ArrayList<>(); + final var runningMerges = getMergeInformation(solrQueryRequest, infos, mergeCandidates); + List leafContexts = searcher.getIndexReader().leaves(); + IndexSchema schema = solrQueryRequest.getSchema(); + response.segments = new HashMap<>(); + for (SegmentCommitInfo segmentCommitInfo : sortable) { + final var singleSegmentData = + getSegmentInfo(segmentCommitInfo, withSizeInfo, withFieldInfo, leafContexts, schema); + if (mergeCandidates.contains(segmentCommitInfo.info.name)) { + singleSegmentData.mergeCandidate = true; + } + response.segments.put(singleSegmentData.name, singleSegmentData); + } + + if (runningMerges.size() > 0) { + response.runningMerges = runningMerges; + } + if (withFieldInfo) { + response.fieldInfoLegend = FI_LEGEND; + } + + if (withRawSizeInfo) { + IndexSizeEstimator estimator = + new IndexSizeEstimator( + searcher.getRawReader(), 20, 100, withRawSizeSummary, withRawSizeDetails); + if (rawSizeSamplingPercent != null) { + estimator.setSamplingPercent(rawSizeSamplingPercent); + } + IndexSizeEstimator.Estimate estimate = estimator.estimate(); + final var rawSizeResponse = new GetSegmentDataResponse.RawSize(); + // make the units more user-friendly + rawSizeResponse.fieldsBySize = estimate.getHumanReadableFieldsBySize(); + rawSizeResponse.typesBySize = estimate.getHumanReadableTypesBySize(); + if (estimate.getSummary() != null) { + rawSizeResponse.summary = estimate.getSummary(); + } + if (estimate.getDetails() != null) { + rawSizeResponse.details = estimate.getDetails(); + } + response.rawSize = rawSizeResponse; + } + + return response; + } + + /** + * Converts Lucene's IndexWriter configuration object into a response type fit for serialization + * + *

Based on {@link LiveIndexWriterConfig#toString()} for legacy reasons. + * + * @param iwConfig the Lucene configuration object to convert + */ + private GetSegmentDataResponse.IndexWriterConfigSummary convertIndexWriterConfigToResponse( + LiveIndexWriterConfig iwConfig) { + final var iwConfigResponse = new GetSegmentDataResponse.IndexWriterConfigSummary(); + iwConfigResponse.analyzer = + iwConfig.getAnalyzer() != null ? iwConfig.getAnalyzer().getClass().getName() : "null"; + iwConfigResponse.ramBufferSizeMB = iwConfig.getRAMBufferSizeMB(); + iwConfigResponse.maxBufferedDocs = iwConfig.getMaxBufferedDocs(); + iwConfigResponse.mergedSegmentWarmer = String.valueOf(iwConfig.getMergedSegmentWarmer()); + iwConfigResponse.delPolicy = iwConfig.getIndexDeletionPolicy().getClass().getName(); + iwConfigResponse.commit = String.valueOf(iwConfig.getIndexCommit()); + iwConfigResponse.openMode = String.valueOf(iwConfig.getOpenMode()); + iwConfigResponse.similarity = iwConfig.getSimilarity().getClass().getName(); + iwConfigResponse.mergeScheduler = String.valueOf(iwConfig.getMergeScheduler()); + iwConfigResponse.codec = String.valueOf(iwConfig.getCodec()); + iwConfigResponse.infoStream = iwConfig.getInfoStream().getClass().getName(); + iwConfigResponse.mergePolicy = String.valueOf(iwConfig.getMergePolicy()); + iwConfigResponse.readerPooling = iwConfig.getReaderPooling(); + iwConfigResponse.perThreadHardLimitMB = iwConfig.getRAMPerThreadHardLimitMB(); + iwConfigResponse.useCompoundFile = iwConfig.getUseCompoundFile(); + iwConfigResponse.commitOnClose = iwConfig.getCommitOnClose(); + iwConfigResponse.indexSort = String.valueOf(iwConfig.getIndexSort()); + iwConfigResponse.checkPendingFlushOnUpdate = iwConfig.isCheckPendingFlushOnUpdate(); + iwConfigResponse.softDeletesField = iwConfig.getSoftDeletesField(); + iwConfigResponse.maxFullFlushMergeWaitMillis = iwConfig.getMaxFullFlushMergeWaitMillis(); + iwConfigResponse.leafSorter = String.valueOf(iwConfig.getLeafSorter()); + iwConfigResponse.eventListener = String.valueOf(iwConfig.getIndexWriterEventListener()); + iwConfigResponse.parentField = iwConfig.getParentField(); + return iwConfigResponse; + } + + // returns a map of currently running merges, and populates a list of candidate segments for merge + private Map getMergeInformation( + SolrQueryRequest req, SegmentInfos infos, List mergeCandidates) throws IOException { + final var result = new HashMap(); + RefCounted refCounted = + req.getCore().getSolrCoreState().getIndexWriter(req.getCore()); + try { + IndexWriter indexWriter = refCounted.get(); + if (indexWriter instanceof SolrIndexWriter) { + result.putAll(((SolrIndexWriter) indexWriter).getRunningMerges()); + } + // get chosen merge policy + MergePolicy mp = indexWriter.getConfig().getMergePolicy(); + // Find merges + MergePolicy.MergeSpecification findMerges = + mp.findMerges(MergeTrigger.EXPLICIT, infos, indexWriter); + if (findMerges != null && findMerges.merges != null && findMerges.merges.size() > 0) { + for (MergePolicy.OneMerge merge : findMerges.merges) { + // TODO: add merge grouping + for (SegmentCommitInfo mergeSegmentInfo : merge.segments) { + mergeCandidates.add(mergeSegmentInfo.info.name); + } + } + } + + return result; + } finally { + refCounted.decref(); + } + } + + private GetSegmentDataResponse.SingleSegmentData getSegmentInfo( + SegmentCommitInfo segmentCommitInfo, + boolean withSizeInfo, + boolean withFieldInfos, + List leafContexts, + IndexSchema schema) + throws IOException { + final var segmentInfo = new GetSegmentDataResponse.SingleSegmentData(); + segmentInfo.name = segmentCommitInfo.info.name; + segmentInfo.delCount = segmentCommitInfo.getDelCount(); + segmentInfo.softDelCount = segmentCommitInfo.getSoftDelCount(); + segmentInfo.hasFieldUpdates = segmentCommitInfo.hasFieldUpdates(); + segmentInfo.sizeInBytes = segmentCommitInfo.sizeInBytes(); + segmentInfo.size = segmentCommitInfo.info.maxDoc(); + Long timestamp = Long.parseLong(segmentCommitInfo.info.getDiagnostics().get("timestamp")); + segmentInfo.age = new Date(timestamp); + segmentInfo.source = segmentCommitInfo.info.getDiagnostics().get("source"); + segmentInfo.version = segmentCommitInfo.info.getVersion().toString(); + + // don't open a new SegmentReader - try to find the right one from the leaf contexts + SegmentReader seg = null; + for (LeafReaderContext lrc : leafContexts) { + LeafReader leafReader = lrc.reader(); + leafReader = FilterLeafReader.unwrap(leafReader); + if (leafReader instanceof SegmentReader sr) { + if (sr.getSegmentInfo().info.equals(segmentCommitInfo.info)) { + seg = sr; + break; + } + } + } + if (seg != null) { + LeafMetaData metaData = seg.getMetaData(); + if (metaData != null) { + segmentInfo.createdVersionMajor = metaData.getCreatedVersionMajor(); + segmentInfo.minVersion = metaData.getMinVersion().toString(); + if (metaData.getSort() != null) { + segmentInfo.sort = metaData.getSort().toString(); + } + } + } + + if (!segmentCommitInfo.info.getDiagnostics().isEmpty()) { + segmentInfo.diagnostics = + SolrJacksonMapper.getObjectMapper() + .convertValue( + segmentCommitInfo.info.getDiagnostics(), + GetSegmentDataResponse.SegmentDiagnosticInfo.class); + } + if (!segmentCommitInfo.info.getAttributes().isEmpty()) { + segmentInfo.attributes = segmentCommitInfo.info.getAttributes(); + } + if (withSizeInfo) { + Directory dir = segmentCommitInfo.info.dir; + List> files = + segmentCommitInfo.files().stream() + .map( + f -> { + long size = -1; + try { + size = dir.fileLength(f); + } catch (IOException e) { + } + return new Pair(f, size); + }) + .sorted( + (p1, p2) -> { + if (p1.second() > p2.second()) { + return -1; + } else if (p1.second() < p2.second()) { + return 1; + } else { + return 0; + } + }) + .collect(Collectors.toList()); + if (!files.isEmpty()) { + final var topFiles = new HashMap(); + for (int i = 0; i < Math.min(files.size(), 5); i++) { + Pair p = files.get(i); + topFiles.put(p.first(), RamUsageEstimator.humanReadableUnits(p.second())); + } + segmentInfo.largestFilesByName = topFiles; + } + } + + if (withFieldInfos) { + if (seg == null) { + log.debug( + "Skipping segment info - not available as a SegmentReader: {}", segmentCommitInfo); + } else { + FieldInfos fis = seg.getFieldInfos(); + final var fields = new HashMap(); + for (FieldInfo fi : fis) { + fields.put(fi.name, getFieldInfo(seg, fi, schema)); + } + segmentInfo.fields = fields; + } + } + + return segmentInfo; + } + + private GetSegmentDataResponse.SegmentSingleFieldInfo getFieldInfo( + SegmentReader reader, FieldInfo fi, IndexSchema schema) { + final var responseFieldInfo = new GetSegmentDataResponse.SegmentSingleFieldInfo(); + StringBuilder flags = new StringBuilder(); + IndexOptions opts = fi.getIndexOptions(); + flags.append((opts != IndexOptions.NONE) ? FieldFlag.INDEXED.getAbbreviation() : '-'); + DocValuesType dvt = fi.getDocValuesType(); + if (dvt != DocValuesType.NONE) { + flags.append(FieldFlag.DOC_VALUES.getAbbreviation()); + switch (dvt) { + case NUMERIC: + flags.append("num"); + break; + case BINARY: + flags.append("bin"); + break; + case SORTED: + flags.append("srt"); + break; + case SORTED_NUMERIC: + flags.append("srn"); + break; + case SORTED_SET: + flags.append("srs"); + break; + default: + flags.append("???"); // should not happen + } + } else { + flags.append("----"); + } + flags.append((fi.hasVectors()) ? FieldFlag.TERM_VECTOR_STORED.getAbbreviation() : '-'); + flags.append((fi.omitsNorms()) ? FieldFlag.OMIT_NORMS.getAbbreviation() : '-'); + + flags.append((DOCS == opts) ? FieldFlag.OMIT_TF.getAbbreviation() : '-'); + + flags.append((DOCS_AND_FREQS == opts) ? FieldFlag.OMIT_POSITIONS.getAbbreviation() : '-'); + + flags.append( + (DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS == opts) + ? FieldFlag.STORE_OFFSETS_WITH_POSITIONS.getAbbreviation() + : '-'); + + flags.append((fi.hasPayloads() ? "p" : "-")); + flags.append((fi.isSoftDeletesField() ? "s" : "-")); + if (fi.getPointDimensionCount() > 0 || fi.getPointIndexDimensionCount() > 0) { + flags.append(":"); + flags.append(fi.getPointDimensionCount()).append(':'); + flags.append(fi.getPointIndexDimensionCount()).append(':'); + flags.append(fi.getPointNumBytes()); + } + + responseFieldInfo.flags = flags.toString(); + + try { + Terms terms = reader.terms(fi.name); + if (terms != null) { + responseFieldInfo.docCount = terms.getDocCount(); + responseFieldInfo.termCount = terms.size(); + responseFieldInfo.sumDocFreq = terms.getSumDocFreq(); + responseFieldInfo.sumTotalTermFreq = terms.getSumTotalTermFreq(); + } + } catch (Exception e) { + log.debug("Exception retrieving term stats for field {}", fi.name, e); + } + + // check compliance of the index with the current schema + SchemaField sf = schema.getFieldOrNull(fi.name); + boolean hasPoints = fi.getPointDimensionCount() > 0 || fi.getPointIndexDimensionCount() > 0; + + if (sf != null) { + responseFieldInfo.schemaType = sf.getType().getTypeName(); + final var nonCompliant = new HashMap(); + if (sf.hasDocValues() + && fi.getDocValuesType() == DocValuesType.NONE + && fi.getIndexOptions() != IndexOptions.NONE) { + nonCompliant.put( + "docValues", "schema=" + sf.getType().getUninversionType(sf) + ", segment=false"); + } + if (!sf.hasDocValues() && fi.getDocValuesType() != DocValuesType.NONE) { + nonCompliant.put("docValues", "schema=false, segment=" + fi.getDocValuesType().toString()); + } + if (!sf.isPolyField()) { // difficult to find all sub-fields in a general way + if (sf.indexed() != ((fi.getIndexOptions() != IndexOptions.NONE) || hasPoints)) { + nonCompliant.put( + "indexed", "schema=" + sf.indexed() + ", segment=" + fi.getIndexOptions()); + } + } + if (!hasPoints && (sf.omitNorms() != fi.omitsNorms())) { + nonCompliant.put("omitNorms", "schema=" + sf.omitNorms() + ", segment=" + fi.omitsNorms()); + } + if (sf.storeTermVector() != fi.hasVectors()) { + nonCompliant.put( + "termVectors", "schema=" + sf.storeTermVector() + ", segment=" + fi.hasVectors()); + } + if (sf.storeOffsetsWithPositions() + != (fi.getIndexOptions() == IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS)) { + nonCompliant.put( + "storeOffsetsWithPositions", + "schema=" + sf.storeOffsetsWithPositions() + ", segment=" + fi.getIndexOptions()); + } + + if (nonCompliant.size() > 0) { + nonCompliant.put("schemaField", sf.toString()); + responseFieldInfo.nonCompliant = nonCompliant; + } + } else { + responseFieldInfo.schemaType = "(UNKNOWN)"; + } + + return responseFieldInfo; + } +} diff --git a/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java b/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java index 9a96b34afc0..22f492abf92 100644 --- a/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java +++ b/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java @@ -91,6 +91,9 @@ public static void squashIntoNamedListWithoutHeader( } public static String getMediaTypeFromWtParam(SolrParams params, String defaultMediaType) { + if (params == null) { + return defaultMediaType; + } final String wtParam = params.get(WT); if (StrUtils.isBlank(wtParam)) return defaultMediaType; diff --git a/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java b/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java index 1110880529c..3760bfc4590 100644 --- a/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java +++ b/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java @@ -63,6 +63,7 @@ public Response toResponse(Exception exception) { // success/failure for AuditLogging, and other logic. final SolrQueryResponse solrQueryResponse = (SolrQueryResponse) containerRequestContext.getProperty(SOLR_QUERY_RESPONSE); + final SolrQueryRequest solrQueryRequest = (SolrQueryRequest) containerRequestContext.getProperty(SOLR_QUERY_REQUEST); if (exception instanceof WebApplicationException wae) { diff --git a/solr/core/src/java/org/apache/solr/jersey/MediaTypeOverridingFilter.java b/solr/core/src/java/org/apache/solr/jersey/MediaTypeOverridingFilter.java index e5a7f7150cc..44c08bff03e 100644 --- a/solr/core/src/java/org/apache/solr/jersey/MediaTypeOverridingFilter.java +++ b/solr/core/src/java/org/apache/solr/jersey/MediaTypeOverridingFilter.java @@ -63,9 +63,9 @@ public void filter( final SolrQueryRequest solrQueryRequest = (SolrQueryRequest) requestContext.getProperty(SOLR_QUERY_REQUEST); - final String mediaType = - V2ApiUtils.getMediaTypeFromWtParam( - solrQueryRequest.getParams(), MediaType.APPLICATION_JSON); + // TODO Is it valid for SQRequest to be null? + final var params = (solrQueryRequest != null) ? solrQueryRequest.getParams() : null; + final String mediaType = V2ApiUtils.getMediaTypeFromWtParam(params, MediaType.APPLICATION_JSON); if (mediaType != null) { responseContext.getHeaders().putSingle(CONTENT_TYPE, mediaType); } diff --git a/solr/core/src/test/org/apache/solr/cloud/CollectionsAPISolrJTest.java b/solr/core/src/test/org/apache/solr/cloud/CollectionsAPISolrJTest.java index 0fe1b755d62..5239deeaeac 100644 --- a/solr/core/src/test/org/apache/solr/cloud/CollectionsAPISolrJTest.java +++ b/solr/core/src/test/org/apache/solr/cloud/CollectionsAPISolrJTest.java @@ -21,6 +21,9 @@ import static org.apache.solr.common.cloud.ZkStateReader.NUM_SHARDS_PROP; import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION; import static org.apache.solr.common.params.CollectionAdminParams.DEFAULTS; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import java.io.IOException; import java.lang.invoke.MethodHandles; @@ -29,6 +32,7 @@ import java.nio.file.Paths; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; @@ -39,11 +43,14 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import org.apache.lucene.tests.util.TestUtil; +import org.apache.lucene.util.Version; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.SolrResponse; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.client.solrj.request.CollectionsApi; import org.apache.solr.client.solrj.request.CoreAdminRequest; import org.apache.solr.client.solrj.request.CoreStatus; import org.apache.solr.client.solrj.request.V2Request; @@ -569,14 +576,7 @@ private void checkCollectionProperty(String collection, String propertyName, Str fail("Timed out waiting for cluster property value"); } - @Test - public void testColStatus() throws Exception { - String collectionName = getSaferTestName(); - CollectionAdminRequest.createCollection(collectionName, "conf2", 2, 2) - .process(cluster.getSolrClient()); - - cluster.waitForActiveCollection(collectionName, 2, 4); - + private void indexSomeDocs(String collectionName) throws SolrServerException, IOException { SolrClient client = cluster.getSolrClient(); byte[] binData = collectionName.getBytes(StandardCharsets.UTF_8); // index some docs @@ -602,13 +602,97 @@ public void testColStatus() throws Exception { client.add(collectionName, doc); } client.commit(collectionName); + } + + private void assertRspPathNull(SolrResponse rsp, String... pathSegments) { + assertNull(Utils.getObjectByPath(rsp.getResponse(), false, Arrays.asList(pathSegments))); + } + + private void assertRspPathNotNull(SolrResponse rsp, String... pathSegments) { + assertNotNull(Utils.getObjectByPath(rsp.getResponse(), false, Arrays.asList(pathSegments))); + } + @Test + @SuppressWarnings("unchecked") + public void testColStatus() throws Exception { + String collectionName = getSaferTestName(); + CollectionAdminRequest.createCollection(collectionName, "conf2", 2, 2) + .process(cluster.getSolrClient()); + + cluster.waitForActiveCollection(collectionName, 2, 4); + indexSomeDocs(collectionName); + + // Returns basic info if no additional flags are set CollectionAdminRequest.ColStatus req = CollectionAdminRequest.collectionStatus(collectionName); + CollectionAdminResponse rsp = req.process(cluster.getSolrClient()); + assertEquals(0, rsp.getStatus()); + assertNotNull(rsp.getResponse().get(collectionName)); + assertNotNull(rsp.getResponse().findRecursive(collectionName, "properties")); + final var collPropMap = + (Map) rsp.getResponse().findRecursive(collectionName, "properties"); + assertEquals("conf2", collPropMap.get("configName")); + assertEquals(2L, collPropMap.get("nrtReplicas")); + assertEquals("0", collPropMap.get("tlogReplicas")); + assertEquals("0", collPropMap.get("pullReplicas")); + assertEquals( + 2, ((NamedList) rsp.getResponse().findRecursive(collectionName, "shards")).size()); + assertNotNull(rsp.getResponse().findRecursive(collectionName, "shards", "shard1", "leader")); + // Ensure more advanced info is not returned + assertNull( + rsp.getResponse().findRecursive(collectionName, "shards", "shard1", "leader", "segInfos")); + + // Returns segment metadata iff requested + req = CollectionAdminRequest.collectionStatus(collectionName); + req.setWithSegments(true); + rsp = req.process(cluster.getSolrClient()); + assertEquals(0, rsp.getStatus()); + assertNotNull(rsp.getResponse().get(collectionName)); + assertRspPathNotNull( + rsp, collectionName, "shards", "shard1", "leader", "segInfos", "segments", "_0"); + // Ensure field, size, etc. information isn't returned if only segment data was requested + assertRspPathNull( + rsp, collectionName, "shards", "shard1", "leader", "segInfos", "segments", "_0", "fields"); + assertRspPathNull( + rsp, + collectionName, + "shards", + "shard1", + "leader", + "segInfos", + "segments", + "_0", + "largestFiles"); + + // Returns segment metadata and file-size info iff requested + // (Note that 'sizeInfo=true' should implicitly enable segments=true) + req = CollectionAdminRequest.collectionStatus(collectionName); + req.setWithSizeInfo(true); + rsp = req.process(cluster.getSolrClient()); + assertEquals(0, rsp.getStatus()); + assertRspPathNotNull(rsp, collectionName); + assertRspPathNotNull( + rsp, collectionName, "shards", "shard1", "leader", "segInfos", "segments", "_0"); + assertRspPathNotNull( + rsp, + collectionName, + "shards", + "shard1", + "leader", + "segInfos", + "segments", + "_0", + "largestFiles"); + // Ensure field, etc. information isn't returned if only segment+size data was requested + assertRspPathNull( + rsp, collectionName, "shards", "shard1", "leader", "segInfos", "segments", "_0", "fields"); + + // Set all flags and ensure everything is returned as expected + req = CollectionAdminRequest.collectionStatus(collectionName); + req.setWithSegments(true); req.setWithFieldInfo(true); req.setWithCoreInfo(true); - req.setWithSegments(true); req.setWithSizeInfo(true); - CollectionAdminResponse rsp = req.process(cluster.getSolrClient()); + rsp = req.process(cluster.getSolrClient()); assertEquals(0, rsp.getStatus()); @SuppressWarnings({"unchecked"}) List nonCompliant = @@ -616,14 +700,22 @@ public void testColStatus() throws Exception { assertEquals(nonCompliant.toString(), 1, nonCompliant.size()); assertTrue(nonCompliant.toString(), nonCompliant.contains("(NONE)")); @SuppressWarnings({"unchecked"}) - NamedList segInfos = - (NamedList) - rsp.getResponse() - .findRecursive(collectionName, "shards", "shard1", "leader", "segInfos"); - assertNotNull(Utils.toJSONString(rsp), segInfos.findRecursive("info", "core", "startTime")); - assertNotNull(Utils.toJSONString(rsp), segInfos.get("fieldInfoLegend")); + final var segInfos = + (Map) + Utils.getObjectByPath( + rsp.getResponse(), + false, + List.of(collectionName, "shards", "shard1", "leader", "segInfos")); assertNotNull( - Utils.toJSONString(rsp), segInfos.findRecursive("segments", "_0", "fields", "id", "flags")); + Utils.toJSONString(rsp), + Utils.getObjectByPath(segInfos, false, List.of("info", "core", "startTime"))); + assertNotNull( + Utils.toJSONString(rsp), + Utils.getObjectByPath(segInfos, false, List.of("fieldInfoLegend"))); + assertNotNull( + Utils.toJSONString(rsp), + Utils.getObjectByPath(segInfos, false, List.of("segments", "_0", "fields", "id", "flags"))); + // test for replicas not active - SOLR-13882 DocCollection coll = cluster.getSolrClient().getClusterState().getCollection(collectionName); Replica firstReplica = coll.getSlice("shard1").getReplicas().iterator().next(); @@ -637,7 +729,10 @@ public void testColStatus() throws Exception { assertEquals(0, rsp.getStatus()); Number down = (Number) - rsp.getResponse().findRecursive(collectionName, "shards", "shard1", "replicas", "down"); + Utils.getObjectByPath( + rsp.getResponse(), + false, + List.of(collectionName, "shards", "shard1", "replicas", "down")); assertTrue( "should be some down replicas, but there were none in shard1:" + rsp, down.intValue() > 0); @@ -652,10 +747,8 @@ public void testColStatus() throws Exception { req = CollectionAdminRequest.collectionStatus(implicitColl); rsp = req.process(cluster.getSolrClient()); assertNotNull(rsp.getResponse().get(implicitColl)); - assertNotNull( - rsp.toString(), rsp.getResponse().findRecursive(implicitColl, "shards", "shardA")); - assertNotNull( - rsp.toString(), rsp.getResponse().findRecursive(implicitColl, "shards", "shardB")); + assertRspPathNotNull(rsp, implicitColl, "shards", "shardA"); + assertRspPathNotNull(rsp, implicitColl, "shards", "shardB"); } @Test @@ -697,6 +790,69 @@ public void testColStatusCollectionName() throws Exception { assertNotNull(rsp.getResponse().get(collectionNames[0])); } + /** + * Unit test for the v2 API: GET /api/collections/$collName + * + *

Uses the OAS-generated SolrRequest/SolrResponse API binding. + */ + @Test + public void testV2BasicCollectionStatus() throws Exception { + final String simpleCollName = "simpleCollection"; + CollectionAdminRequest.createCollection(simpleCollName, "conf2", 2, 1, 1, 1) + .process(cluster.getSolrClient()); + cluster.waitForActiveCollection(simpleCollName, 2, 6); + indexSomeDocs(simpleCollName); + + final var simpleResponse = + new CollectionsApi.GetCollectionStatus(simpleCollName) + .process(cluster.getSolrClient()) + .getParsed(); + assertEquals(simpleCollName, simpleResponse.name); + assertEquals(2, simpleResponse.shards.size()); + assertEquals(Integer.valueOf(2), simpleResponse.activeShards); + assertEquals(Integer.valueOf(0), simpleResponse.inactiveShards); + assertEquals(Integer.valueOf(1), simpleResponse.properties.nrtReplicas); + assertEquals(Integer.valueOf(1), simpleResponse.properties.replicationFactor); + assertEquals(Integer.valueOf(1), simpleResponse.properties.pullReplicas); + assertEquals(Integer.valueOf(1), simpleResponse.properties.tlogReplicas); + assertNotNull(simpleResponse.shards.get("shard1").leader); + assertNull(simpleResponse.shards.get("shard1").leader.segInfos); + + // Ensure segment data present when request sets 'segments=true' flag + final var segmentDataRequest = new CollectionsApi.GetCollectionStatus(simpleCollName); + segmentDataRequest.setSegments(true); + final var segmentDataResponse = segmentDataRequest.process(cluster.getSolrClient()).getParsed(); + var segmentData = segmentDataResponse.shards.get("shard1").leader.segInfos; + assertNotNull(segmentData); + assertTrue(segmentData.info.numSegments > 0); // Expect at least one segment + assertEquals(segmentData.info.numSegments.intValue(), segmentData.segments.size()); + assertEquals(Version.LATEST.toString(), segmentData.info.commitLuceneVersion); + // Ensure field, size, etc. data not provided + assertNull(segmentData.segments.get("_0").fields); + assertNull(segmentData.segments.get("_0").largestFilesByName); + + // Ensure file-size data present when request sets sizeInfo flag + final var segmentFileSizeRequest = new CollectionsApi.GetCollectionStatus(simpleCollName); + segmentFileSizeRequest.setSizeInfo(true); + final var segmentFileSizeResponse = + segmentFileSizeRequest.process(cluster.getSolrClient()).getParsed(); + segmentData = segmentFileSizeResponse.shards.get("shard1").leader.segInfos; + assertNotNull(segmentData); + final var largeFileList = segmentData.segments.get("_0").largestFilesByName; + assertNotNull(largeFileList); + // Hard to assert what the largest index files should be, but: + // - there should be at least 1 entry and... + // - all keys/values should be non-empty + assertTrue(largeFileList.size() > 0); + largeFileList.forEach( + (fileName, size) -> { + assertThat(fileName, is(not(emptyString()))); + assertThat(size, is(not(emptyString()))); + }); + // Ensure field, etc. data not provided + assertNull(segmentData.segments.get("_0").fields); + } + private static final int NUM_DOCS = 10; @Test diff --git a/solr/core/src/test/org/apache/solr/handler/admin/IndexSizeEstimatorTest.java b/solr/core/src/test/org/apache/solr/handler/admin/IndexSizeEstimatorTest.java index 5523ad23c15..54aa6394902 100644 --- a/solr/core/src/test/org/apache/solr/handler/admin/IndexSizeEstimatorTest.java +++ b/solr/core/src/test/org/apache/solr/handler/admin/IndexSizeEstimatorTest.java @@ -19,6 +19,7 @@ import java.lang.invoke.MethodHandles; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -32,6 +33,7 @@ import org.apache.lucene.tests.util.TestUtil; import org.apache.lucene.util.Bits; import org.apache.lucene.util.RamUsageEstimator; +import org.apache.solr.client.api.model.CollectionStatusResponse; import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.request.CollectionAdminRequest; import org.apache.solr.client.solrj.request.UpdateRequest; @@ -39,10 +41,11 @@ import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.cloud.SolrCloudTestCase; import org.apache.solr.common.SolrInputDocument; -import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.TimeSource; +import org.apache.solr.common.util.Utils; import org.apache.solr.core.SolrCore; import org.apache.solr.embedded.JettySolrRunner; +import org.apache.solr.jersey.SolrJacksonMapper; import org.apache.solr.search.SolrIndexSearcher; import org.apache.solr.util.RefCounted; import org.apache.solr.util.TimeOut; @@ -177,56 +180,42 @@ public void testIntegration() throws Exception { assertEquals(0, sampledRsp.getStatus()); for (int i : Arrays.asList(1, 2)) { @SuppressWarnings({"unchecked"}) - NamedList segInfos = - (NamedList) - rsp.getResponse() - .findRecursive(collection, "shards", "shard" + i, "leader", "segInfos"); - @SuppressWarnings({"unchecked"}) - NamedList rawSize = (NamedList) segInfos.get("rawSize"); + final var segInfosRaw = + Utils.getObjectByPath( + rsp.getResponse(), + false, + List.of(collection, "shards", "shard" + i, "leader", "segInfos")); + final var segInfos = + SolrJacksonMapper.getObjectMapper() + .convertValue(segInfosRaw, CollectionStatusResponse.SegmentInfo.class); + + final var rawSize = segInfos.rawSize; assertNotNull("rawSize missing", rawSize); - @SuppressWarnings({"unchecked"}) - Map rawSizeMap = rawSize.asMap(10); - @SuppressWarnings({"unchecked"}) - Map fieldsBySize = - (Map) rawSizeMap.get(IndexSizeEstimator.FIELDS_BY_SIZE); + Map fieldsBySize = rawSize.fieldsBySize; assertNotNull("fieldsBySize missing", fieldsBySize); assertEquals(fieldsBySize.toString(), fields.size(), fieldsBySize.size()); fields.forEach(field -> assertNotNull("missing field " + field, fieldsBySize.get(field))); - @SuppressWarnings({"unchecked"}) - Map typesBySize = - (Map) rawSizeMap.get(IndexSizeEstimator.TYPES_BY_SIZE); + Map typesBySize = rawSize.typesBySize; assertNotNull("typesBySize missing", typesBySize); assertTrue("expected at least 8 types: " + typesBySize, typesBySize.size() >= 8); - @SuppressWarnings({"unchecked"}) - Map summary = - (Map) rawSizeMap.get(IndexSizeEstimator.SUMMARY); + Map summary = rawSize.summary; assertNotNull("summary missing", summary); assertEquals(summary.toString(), fields.size(), summary.size()); fields.forEach(field -> assertNotNull("missing field " + field, summary.get(field))); @SuppressWarnings({"unchecked"}) - Map details = - (Map) rawSizeMap.get(IndexSizeEstimator.DETAILS); + Map details = (Map) rawSize.details; assertNotNull("details missing", summary); assertEquals(details.keySet().toString(), 6, details.size()); // compare with sampled - @SuppressWarnings({"unchecked"}) - NamedList sampledRawSize = - (NamedList) - rsp.getResponse() - .findRecursive( - collection, "shards", "shard" + i, "leader", "segInfos", "rawSize"); + final var sampledRawSize = rawSize; assertNotNull("sampled rawSize missing", sampledRawSize); - @SuppressWarnings({"unchecked"}) - Map sampledRawSizeMap = rawSize.asMap(10); - @SuppressWarnings({"unchecked"}) - Map sampledFieldsBySize = - (Map) sampledRawSizeMap.get(IndexSizeEstimator.FIELDS_BY_SIZE); + Map sampledFieldsBySize = sampledRawSize.fieldsBySize; assertNotNull("sampled fieldsBySize missing", sampledFieldsBySize); fieldsBySize.forEach( (k, v) -> { - double size = fromHumanReadableUnits((String) v); - double sampledSize = fromHumanReadableUnits((String) sampledFieldsBySize.get(k)); + double size = fromHumanReadableUnits(v); + double sampledSize = fromHumanReadableUnits(sampledFieldsBySize.get(k)); double delta = size * 0.5; assertEquals("sampled size of " + k + " is wildly off", size, sampledSize, delta); }); diff --git a/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java b/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java index 265968b23c8..8600ed8236b 100644 --- a/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java +++ b/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java @@ -114,7 +114,6 @@ public void testFramework() { methodNames.add(rsp.getValues()._getStr("/spec[1]/methods[0]", null)); methodNames.add(rsp.getValues()._getStr("/spec[2]/methods[0]", null)); assertTrue(methodNames.contains("POST")); - assertTrue(methodNames.contains("GET")); methodNames = new HashSet<>(); diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java index 98d000773c7..1a61b6516fd 100644 --- a/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java +++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java @@ -21,9 +21,7 @@ import static org.apache.solr.common.params.CollectionAdminParams.COLL_CONF; import static org.apache.solr.common.params.CommonAdminParams.ASYNC; import static org.apache.solr.common.params.CommonParams.ACTION; -import static org.apache.solr.common.params.CoreAdminParams.SHARD; -import java.util.Map; import org.apache.solr.common.cloud.ZkStateReader; import org.apache.solr.common.params.CollectionParams; import org.apache.solr.common.params.SolrParams; @@ -56,7 +54,6 @@ public void populateApiBag() { apiBag.registerObject(new ModifyCollectionAPI(collectionsHandler)); apiBag.registerObject(new MoveReplicaAPI(collectionsHandler)); apiBag.registerObject(new RebalanceLeadersAPI(collectionsHandler)); - apiBag.registerObject(new CollectionStatusAPI(collectionsHandler)); } @Override @@ -69,17 +66,6 @@ public boolean isCoreSpecific() { return false; } - @Test - public void testGetCollectionStatus() throws Exception { - final SolrParams v1Params = - captureConvertedV1Params( - "/collections/collName", "GET", Map.of(SHARD, new String[] {"shard2"})); - - assertEquals(CollectionParams.CollectionAction.CLUSTERSTATUS.toString(), v1Params.get(ACTION)); - assertEquals("collName", v1Params.get(COLLECTION)); - assertEquals("shard2", v1Params.get(SHARD)); - } - @Test public void testModifyCollectionAllProperties() throws Exception { final SolrParams v1Params = 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 f4811158ef7..6f89932a3a6 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 @@ -1050,11 +1050,9 @@ http://localhost:8983/solr/admin/collections?action=COLSTATUS&collection=techpro V2 API:: + ==== -The closest V2 API is this one, but doesn't support all the features of the V1 equivalent. - [source,bash] ---- -curl -X GET http://localhost:8983/api/collections/techproducts_v2 +curl -X GET "http://localhost:8983/api/collections/techproducts_v2?coreInfo=true&segments=true&fieldInfo=true&sizeInfo=true" ---- ==== ====== @@ -1072,7 +1070,8 @@ Such incompatibilities may result from incompatible schema changes or after migr |=== + Collection name. -If missing then it means all collections. +Provided as a query-parameter in v1 requests, and as a path-parameter in v2. +If missing then information is returned about all collections (supported by v1 requests only). `coreInfo`:: +