blob: cbc7ed4c7e1610489f53c071c42b47826e712639 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.handler.admin;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.function.BiConsumer;
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.Accountable;
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.handler.RequestHandlerBase;
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.update.SolrIndexWriter;
import org.apache.solr.util.RefCounted;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
/**
* 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";
public static final String SIZE_INFO_PARAM = "sizeInfo";
public static final String RAW_SIZE_PARAM = "rawSize";
public static final String RAW_SIZE_SUMMARY_PARAM = "rawSizeSummary";
public static final String RAW_SIZE_DETAILS_PARAM = "rawSizeDetails";
public static final String RAW_SIZE_SAMPLING_PERCENT_PARAM = "rawSizeSamplingPercent";
private static final List<String> 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<Object> segmentInfos = new SimpleOrderedMap<>();
SolrCore core = req.getCore();
SimpleOrderedMap<Object> 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<Object> 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<IndexWriter> iwRef = core.getSolrCoreState().getIndexWriter(core);
if (iwRef != null) {
try {
IndexWriter iw = iwRef.get();
String iwConfigStr = iw.getConfig().toString();
SimpleOrderedMap<Object> 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<Object> segmentInfo = null;
List<SegmentCommitInfo> 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<String> mergeCandidates = new ArrayList<>();
SimpleOrderedMap<Object> runningMerges = getMergeInformation(req, infos, mergeCandidates);
List<LeafReaderContext> 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<Object> 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<Object> getSegmentInfo(
SegmentCommitInfo segmentCommitInfo, boolean withSizeInfo, boolean withFieldInfos,
List<LeafReaderContext> leafContexts, IndexSchema schema) throws IOException {
SimpleOrderedMap<Object> 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) {
SegmentReader sr = (SegmentReader)leafReader;
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<Pair<String, Long>> files = segmentCommitInfo.files().stream()
.map(f -> {
long size = -1;
try {
size = dir.fileLength(f);
} catch (IOException e) {
}
return new Pair<String, Long>(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<Object> topFiles = new SimpleOrderedMap<>();
for (int i = 0; i < Math.min(files.size(), 5); i++) {
Pair<String, Long> p = files.get(i);
topFiles.add(p.first(), RamUsageEstimator.humanReadableUnits(p.second()));
}
segmentInfoMap.add("largestFiles", topFiles);
}
}
if (seg != null && withSizeInfo) {
SimpleOrderedMap<Object> ram = new SimpleOrderedMap<>();
ram.add("total", seg.ramBytesUsed());
for (Accountable ac : seg.getChildResources()) {
accountableToMap(ac, ram::add);
}
segmentInfoMap.add("ramBytesUsed", ram);
}
if (withFieldInfos) {
if (seg == null) {
log.debug("Skipping segment info - not available as a SegmentReader: {}", segmentCommitInfo);
} else {
FieldInfos fis = seg.getFieldInfos();
SimpleOrderedMap<Object> fields = new SimpleOrderedMap<>();
for (FieldInfo fi : fis) {
fields.add(fi.name, getFieldInfo(seg, fi, schema));
}
segmentInfoMap.add("fields", fields);
}
}
return segmentInfoMap;
}
private void accountableToMap(Accountable accountable, BiConsumer<String, Object> consumer) {
Collection<Accountable> children = accountable.getChildResources();
if (children != null && !children.isEmpty()) {
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put("total", accountable.ramBytesUsed());
for (Accountable child : children) {
accountableToMap(child, map::put);
}
consumer.accept(accountable.toString(), map);
} else {
consumer.accept(accountable.toString(), accountable.ramBytesUsed());
}
}
private SimpleOrderedMap<Object> getFieldInfo(SegmentReader reader, FieldInfo fi, IndexSchema schema) {
SimpleOrderedMap<Object> 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("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<String, String> 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<Object> 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<Object> getMergeInformation(SolrQueryRequest req, SegmentInfos infos, List<String> mergeCandidates) throws IOException {
SimpleOrderedMap<Object> result = new SimpleOrderedMap<>();
RefCounted<IndexWriter> 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();
}
}
@Override
public String getDescription() {
return "Lucene segments info.";
}
@Override
public Category getCategory() {
return Category.ADMIN;
}
}