blob: c008c708b8729d4edf9035587329a7c31afbf464 [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.schema;
import net.sf.saxon.om.AttributeMap;
import net.sf.saxon.om.NodeInfo;
import net.sf.saxon.tree.tiny.TinyAttributeImpl;
import net.sf.saxon.tree.tiny.TinyElementImpl;
import net.sf.saxon.tree.tiny.TinyTextualElement;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.DelegatingAnalyzerWrapper;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.queries.payloads.PayloadDecoder;
import org.apache.lucene.search.similarities.Similarity;
import org.apache.lucene.util.Version;
import org.apache.solr.common.MapSerializable;
import org.apache.solr.common.ParWork;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.Cache;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.Pair;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.common.util.StopWatch;
import org.apache.solr.core.ConfigXpathExpressions;
import org.apache.solr.core.SolrCore;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.core.XmlConfigFile;
import org.apache.solr.request.LocalSolrQueryRequest;
import org.apache.solr.response.SchemaXmlWriter;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.search.similarities.SchemaSimilarityFactory;
import org.apache.solr.uninverting.UninvertingReader;
import org.apache.solr.util.ConcurrentLRUCache;
import org.apache.solr.util.DOMUtil;
import org.apache.solr.util.PayloadUtils;
import org.apache.solr.util.plugin.SolrCoreAware;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import java.io.IOException;
import java.io.Writer;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* <code>IndexSchema</code> contains information about the valid fields in an index
* and the types of those fields.
*
*
*/
public class IndexSchema {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final String COPY_FIELD = "copyField";
public static final String COPY_FIELDS = COPY_FIELD + "s";
public static final String DEFAULT_SCHEMA_FILE = "schema.xml";
public static final String DESTINATION = "dest";
public static final String DYNAMIC_FIELD = "dynamicField";
public static final String DYNAMIC_FIELDS = DYNAMIC_FIELD + "s";
public static final String FIELD = "field";
public static final String FIELDS = FIELD + "s";
public static final String FIELD_TYPE = "fieldType";
public static final String FIELD_TYPES = FIELD_TYPE + "s";
public static final String INTERNAL_POLY_FIELD_PREFIX = "*" + FieldType.POLY_FIELD_SEPARATOR;
public static final String LUCENE_MATCH_VERSION_PARAM = "luceneMatchVersion";
public static final String MAX_CHARS = "maxChars";
public static final String NAME = "name";
public static final String NEST_PARENT_FIELD_NAME = "_nest_parent_";
public static final String NEST_PATH_FIELD_NAME = "_nest_path_";
public static final String REQUIRED = "required";
public static final String SCHEMA = "schema";
public static final String SIMILARITY = "similarity";
public static final String SLASH = "/";
public static final String SOURCE = "source";
public static final String TYPE = "type";
public static final String TYPES = "types";
public static final String ROOT_FIELD_NAME = "_root_";
public static final String UNIQUE_KEY = "uniqueKey";
public static final String VERSION = "version";
public static final String AT = "@";
private static final String DESTINATION_DYNAMIC_BASE = "destDynamicBase";
private static final String SOLR_CORE_NAME = "solr.core.name";
private static final String SOURCE_DYNAMIC_BASE = "sourceDynamicBase";
private static final String SOURCE_EXPLICIT_FIELDS = "sourceExplicitFields";
public static final String TEXT_FUNCTION = "text()";
public static final String XPATH_OR = " | ";
public static final DynamicField[] EMPTY_DYNAMIC_FIELDS = new DynamicField[0];
public static final DynamicCopy[] EMPTY_DYNAMIC_COPY_FIELDS = {};
public static final DynamicCopy[] EMPTY_DYNAMIC_COPIES = {};
public static final List<DynamicField> EMPTY_DYNAMIC_FIELDS1 = Collections.emptyList();
protected volatile String resourceName;
protected volatile String name;
protected final Version luceneVersion;
protected float version;
protected final SolrResourceLoader loader;
protected final Properties substitutableProperties;
protected volatile Map<String,SchemaField> fields = Collections.emptyMap();
protected volatile Map<String,FieldType> fieldTypes = Collections.emptyMap();
protected volatile List<SchemaField> fieldsWithDefaultValue = new ArrayList<>();
protected volatile Collection<SchemaField> requiredFields = new HashSet<>();
protected volatile DynamicField[] dynamicFields = EMPTY_DYNAMIC_FIELDS;
public DynamicField[] getDynamicFields() { return dynamicFields; }
protected Cache<String, SchemaField> dynamicFieldCache = new ConcurrentLRUCache(10000, 8000, 9000,100, false,false, null);
protected volatile Analyzer indexAnalyzer;
protected volatile Analyzer queryAnalyzer;
private final ReentrantLock analyzerLock = new ReentrantLock();
protected volatile Set<SchemaAware> schemaAware = ConcurrentHashMap.newKeySet(32);
protected volatile Map<String, List<CopyField>> copyFieldsMap = new HashMap<>();
public Map<String,List<CopyField>> getCopyFieldsMap() { return Collections.unmodifiableMap(copyFieldsMap); }
protected DynamicCopy[] dynamicCopyFields = EMPTY_DYNAMIC_COPIES;
public DynamicCopy[] getDynamicCopyFields() { return dynamicCopyFields; }
private Map<FieldType, PayloadDecoder> decoders = new HashMap<>(); // cache to avoid scanning token filters repeatedly, unnecessarily
static {
DynamicReplacement.DynamicPattern.createPattern(""); // early init
}
/**
* keys are all fields copied to, count is num of copyField
* directives that target them.
*/
protected Map<SchemaField, Integer> copyFieldTargetCounts = new HashMap<>();
/**
* Constructs a schema using the specified resource name and stream.
* By default, this follows the normal config path directory searching rules.
* @see SolrResourceLoader#openResource
*/
public IndexSchema(String name, InputSource is, Version luceneVersion, SolrResourceLoader resourceLoader, Properties substitutableProperties) {
this(luceneVersion, resourceLoader, substitutableProperties);
this.resourceName = Objects.requireNonNull(name);
StopWatch timeReadSchema = new StopWatch(name + "-readSchema");
readSchema(is);
timeReadSchema.done();
StopWatch timeInform = new StopWatch(name + "-informAfterSchemaRead");
loader.inform(loader);
timeInform.done();
}
protected IndexSchema(Version luceneVersion, SolrResourceLoader loader, Properties substitutableProperties) {
this.luceneVersion = Objects.requireNonNull(luceneVersion);
this.loader = loader;
this.substitutableProperties = substitutableProperties;
}
/**
* The resource loader to be used to load components related to the schema when the schema is loading
* / initialising.
* It should <em>not</em> be used for any other purpose or time;
* consider {@link SolrCore#getResourceLoader()} instead.
* @since solr 1.4
*/
public SolrResourceLoader getResourceLoader() {
//TODO consider asserting the schema has not finished loading somehow?
return loader;
}
/** Gets the name of the resource used to instantiate this schema. */
public String getResourceName() {
return resourceName;
}
/** Sets the name of the resource used to instantiate this schema. */
public void setResourceName(String resourceName) {
this.resourceName = resourceName;
}
/** Gets the name of the schema as specified in the schema resource. */
public String getSchemaName() {
return name;
}
/** The Default Lucene Match Version for this IndexSchema */
public Version getDefaultLuceneMatchVersion() {
return luceneVersion;
}
public float getVersion() {
return version;
}
/**
* Provides direct access to the Map containing all explicit
* (ie: non-dynamic) fields in the index, keyed on field name.
*
* <p>
* Modifying this Map (or any item in it) will affect the real schema
* </p>
*
* <p>
* NOTE: this function is not thread safe. However, it is safe to use within the standard
* <code>inform( SolrCore core )</code> function for <code>SolrCoreAware</code> classes.
* Outside <code>inform</code>, this could potentially throw a ConcurrentModificationException
* </p>
*/
public Map<String,SchemaField> getFields() { return fields; }
public void setFields(Map<String,SchemaField> fields) { this.fields = fields; }
/**
* Provides direct access to the Map containing all Field Types
* in the index, keyed on field type name.
*
* <p>
* Modifying this Map (or any item in it) will affect the real schema. However if you
* make any modifications, be sure to call {@link IndexSchema#refreshAnalyzers()} to
* update the Analyzers for the registered fields.
* </p>
*
* <p>
* NOTE: this function is not thread safe. However, it is safe to use within the standard
* <code>inform( SolrCore core )</code> function for <code>SolrCoreAware</code> classes.
* Outside <code>inform</code>, this could potentially throw a ConcurrentModificationException
* </p>
*/
public Map<String,FieldType> getFieldTypes() { return fieldTypes; }
/**
* Provides direct access to the List containing all fields with a default value
*/
public List<SchemaField> getFieldsWithDefaultValue() { return fieldsWithDefaultValue; }
/**
* Provides direct access to the List containing all required fields. This
* list contains all fields with default values.
*/
public Collection<SchemaField> getRequiredFields() { return requiredFields; }
protected Similarity similarity;
/**
* Returns the Similarity used for this index
*/
public Similarity getSimilarity() {
if (null == similarity) {
similarity = similarityFactory.getSimilarity();
}
return similarity;
}
protected SimilarityFactory similarityFactory;
protected boolean isExplicitSimilarity = false;
/** Returns the SimilarityFactory that constructed the Similarity for this index */
public SimilarityFactory getSimilarityFactory() { return similarityFactory; }
/**
* Returns the Analyzer used when indexing documents for this index
*
* <p>
* This Analyzer is field (and dynamic field) name aware, and delegates to
* a field specific Analyzer based on the field type.
* </p>
*/
public Analyzer getIndexAnalyzer() { return indexAnalyzer; }
/**
* Returns the Analyzer used when searching this index
*
* <p>
* This Analyzer is field (and dynamic field) name aware, and delegates to
* a field specific Analyzer based on the field type.
* </p>
*/
public Analyzer getQueryAnalyzer() { return queryAnalyzer; }
protected SchemaField uniqueKeyField;
/**
* Unique Key field specified in the schema file
* @return null if this schema has no unique key field
*/
public SchemaField getUniqueKeyField() { return uniqueKeyField; }
protected String uniqueKeyFieldName;
protected FieldType uniqueKeyFieldType;
/**
* The raw (field type encoded) value of the Unique Key field for
* the specified Document
* @return null if this schema has no unique key field
* @see #printableUniqueKey
*/
public IndexableField getUniqueKeyField(org.apache.lucene.document.Document doc) {
return doc.getField(uniqueKeyFieldName); // this should return null if name is null
}
/**
* The printable value of the Unique Key field for
* the specified Document
* @return null if this schema has no unique key field
*/
public String printableUniqueKey(org.apache.lucene.document.Document doc) {
IndexableField f = doc.getField(uniqueKeyFieldName);
return f==null ? null : uniqueKeyFieldType.toExternal(f);
}
/** Like {@link #printableUniqueKey(org.apache.lucene.document.Document)} */
public String printableUniqueKey(SolrDocument solrDoc) {
Object val = solrDoc.getFieldValue(uniqueKeyFieldName);
if (val == null) {
return null;
} else if (val instanceof IndexableField) {
return uniqueKeyFieldType.toExternal((IndexableField) val);
} else {
return val.toString();
}
}
private SchemaField getIndexedField(String fname) {
SchemaField f = getFields().get(fname);
if (f==null) {
throw new RuntimeException("unknown field '" + fname + "'");
}
if (!f.indexed()) {
throw new RuntimeException("'"+fname+"' is not an indexed field:" + f);
}
return f;
}
/**
* This will re-create the Analyzers. If you make any modifications to
* the Field map ({@link IndexSchema#getFields()}, this function is required
* to synch the internally cached field analyzers.
*
* @since solr 1.3
*/
public void refreshAnalyzers() {
if (indexAnalyzer == null || queryAnalyzer == null) {
analyzerLock.lock();
try {
if (indexAnalyzer == null || queryAnalyzer == null) {
indexAnalyzer = new SolrIndexAnalyzer(fields, Arrays.asList(dynamicFields));
queryAnalyzer = new SolrQueryAnalyzer(fields, Arrays.asList(dynamicFields));
return;
}
} finally {
analyzerLock.unlock();
}
}
((SolrIndexAnalyzer) indexAnalyzer).setUpFields(fields, Arrays.asList(dynamicFields));
((SolrQueryAnalyzer) queryAnalyzer).setUpFields(fields, Arrays.asList(dynamicFields));
}
/** @see UninvertingReader */
public Function<String, UninvertingReader.Type> getUninversionMapper() {
return name -> {
SchemaField sf = getFieldOrNull(name);
if (sf == null) {
return null;
}
if (sf.isUninvertible()) {
return sf.getType().getUninversionType(sf);
}
// else...
// It would be nice to throw a helpful error here, with a good useful message for the user,
// but unfortunately, inspite of the UninvertingReader class jdoc claims that the uninversion
// process is lazy, that doesn't mean it's lazy as of "When a caller attempts ot use doc values"
//
// The *mapping* function is consulted on LeafReader init/wrap for every FieldInfos found w/o docValues.
//
// So if we throw an error here instead of returning null, the act of just opening a
// newSearcher will trigger that error for any field, even if no one ever attempts to uninvert it
return null;
};
}
/**
* Writes the schema in schema.xml format to the given writer
*/
void persist(Writer writer) throws IOException {
final SolrQueryResponse response = new SolrQueryResponse();
response.add(IndexSchema.SCHEMA, getNamedPropertyValues());
final SolrParams args = (new ModifiableSolrParams()).set("indent", "on");
final LocalSolrQueryRequest req = new LocalSolrQueryRequest(null, args);
final SchemaXmlWriter schemaXmlWriter = new SchemaXmlWriter(writer, req, response);
schemaXmlWriter.setEmitManagedSchemaDoNotEditWarning(true);
schemaXmlWriter.writeResponse();
schemaXmlWriter.close();
}
public boolean isMutable() {
return false;
}
public IndexSchemaFactory getSchemaFactory() {
return null;
}
private static class SolrIndexAnalyzer extends DelegatingAnalyzerWrapper {
protected volatile Map<String, Analyzer> analyzers;
protected volatile Map<String,SchemaField> fields;
protected volatile List<DynamicField> dynamicFields;
SolrIndexAnalyzer(Map<String,SchemaField> fields, List<DynamicField> dynamicFields) {
super(PER_FIELD_REUSE_STRATEGY);
setUpFields(fields, dynamicFields);
}
protected HashMap<String, Analyzer> analyzerCache(Map<String,SchemaField> fields) {
HashMap<String, Analyzer> cache = new HashMap<>();
for (SchemaField f : fields.values()) {
Analyzer analyzer = f.getType().getIndexAnalyzer();
cache.put(f.getName(), analyzer);
}
return cache;
}
public void setUpFields(Map<String,SchemaField> fields, List<DynamicField> dynamicFields) {
this.fields = fields;
this.dynamicFields = dynamicFields;
analyzers = analyzerCache(fields);
}
@Override
protected Analyzer getWrappedAnalyzer(String fieldName) {
Analyzer analyzer = analyzers.get(fieldName);
FieldType ft = getDynamicFieldType(fieldName);
if (ft != null) {
return analyzer != null ? analyzer : ft.getIndexAnalyzer();
}
SchemaField field = fields.get(fieldName);
if (field != null) {
ft = field.getType();
return ft.getIndexAnalyzer();
}
throw new SolrException(ErrorCode.BAD_REQUEST, "undefined field "+fieldName);
}
public FieldType getDynamicFieldType(String fieldName) {
FieldType[] fieldType = new FieldType[1];
dynamicFields.forEach(dynamicField -> {
if (dynamicField.matches(fieldName)) fieldType[0] = dynamicField.prototype.getType();
});
if (fieldType[0] != null) {
return fieldType[0];
}
return null;
}
}
private static class SolrQueryAnalyzer extends SolrIndexAnalyzer {
SolrQueryAnalyzer(Map<String,SchemaField> fields, List<DynamicField> dynamicFields) {
super(fields, dynamicFields);
}
@Override
protected HashMap<String, Analyzer> analyzerCache(Map<String,SchemaField> fields) {
HashMap<String,Analyzer> cache = new HashMap<>();
fields.forEach((s, f) -> {
Analyzer analyzer = f.getType().getQueryAnalyzer();
cache.put(f.getName(), analyzer);
});
return cache;
}
@Override
protected Analyzer getWrappedAnalyzer(String fieldName) {
Analyzer analyzer = analyzers.get(fieldName);
FieldType ft = getDynamicFieldType(fieldName);
if (ft != null) {
return analyzer != null ? analyzer : ft.getQueryAnalyzer();
}
SchemaField field = fields.get(fieldName);
if (field != null) {
ft = field.getType();
return ft.getQueryAnalyzer();
}
throw new SolrException(ErrorCode.BAD_REQUEST, "undefined field "+fieldName);
}
}
public static String normalize (String path, String prefix){
return (prefix == null || path.startsWith("/")) ? path : prefix + path;
}
protected void readSchema(InputSource is) {
assert null != is : "schema InputSource should never be null";
try {
// pass the config resource loader to avoid building an empty one for no reason:
// in the current case though, the stream is valid so we wont load the resource by name
XmlConfigFile schemaConf = new XmlConfigFile(loader, SCHEMA, is, SLASH+SCHEMA+SLASH, substitutableProperties);
StopWatch timeParseSchemaDom = new StopWatch(SCHEMA + "-parseSchemaDom");
NodeInfo document = schemaConf.getTree();
// Document domDoc = (Document) DocumentOverNodeInfo.wrap(document);
TinyAttributeImpl nd = (TinyAttributeImpl) loader.configXpathExpressions.schemaNameExp.evaluate(document, XPathConstants.NODE);
StringBuilder sb = new StringBuilder(32);
// Another case where the initialization from the test harness is different than the "real world"
if (nd==null) {
sb.append("schema has no name!");
log.warn("{}", sb);
} else {
name = nd.getStringValue();
sb.append("Schema ");
sb.append(NAME);
sb.append("=");
sb.append(name);
log.info("{}", sb);
}
// /schema/@version
String path = normalize(ConfigXpathExpressions.schemaVersionPath, schemaConf.getPrefix());
XPathExpression exp;
if (path.equals("/schema/@version")) {
exp = loader.configXpathExpressions.schemaVersionExp;
} else {
throw new UnsupportedOperationException();
}
version = schemaConf.getFloat(exp, path, 1.0f);
StopWatch timeLoadTypeFields = new StopWatch(SCHEMA + "-loadTypeFields");
Map<String,FieldType> fieldTypes = new HashMap<>(this.fieldTypes);
// load the Field Types
final FieldTypePluginLoader typeLoader = new FieldTypePluginLoader(this, fieldTypes, schemaAware);
ArrayList<NodeInfo> nodes = (ArrayList) loader.configXpathExpressions.fieldTypeXPathExpressionsExp.evaluate(document, XPathConstants.NODESET);
this.fieldTypes = fieldTypes;
typeLoader.load(loader, nodes);
timeLoadTypeFields.done();
// load the fields
StopWatch timeLoadFields = new StopWatch(SCHEMA + "-loadFields");
Map<String,Boolean> explicitRequiredProp = loadFields(document);
timeLoadFields.done();
StopWatch timeLoadSim = new StopWatch(SCHEMA + "-loadSim");
TinyElementImpl node = (TinyElementImpl) loader.configXpathExpressions.schemaSimExp.evaluate(document, XPathConstants.NODE);
similarityFactory = readSimilarity(loader, node);
if (similarityFactory == null) {
final Class<?> simClass = SchemaSimilarityFactory.class;
// use the loader to ensure proper SolrCoreAware handling
similarityFactory = loader.newInstance(simClass.getName(), SimilarityFactory.class);
similarityFactory.init(new ModifiableSolrParams());
} else {
isExplicitSimilarity = true;
}
if ( ! (similarityFactory instanceof SolrCoreAware)) {
// if the sim factory isn't SolrCoreAware (and hence schema aware),
// then we are responsible for erroring if a field type is trying to specify a sim.
for (FieldType ft : fieldTypes.values()) {
if (null != ft.getSimilarity()) {
String msg = "FieldType '" + ft.getTypeName()
+ "' is configured with a similarity, but the global similarity does not support it: "
+ similarityFactory.getClass();
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
}
}
timeLoadSim.done();
// /schema/defaultSearchField/text()
Object node2 = loader.configXpathExpressions.defaultSearchFieldExp.evaluate(document, XPathConstants.NODE);
if (node2 != null) {
throw new SolrException(ErrorCode.SERVER_ERROR, "Setting defaultSearchField in schema not supported since Solr 7");
}
// /schema/solrQueryParser/@defaultOperator
node2 = loader.configXpathExpressions.solrQueryParserDefaultOpExp.evaluate(document, XPathConstants.NODE);
if (node2 != null) {
throw new SolrException(ErrorCode.SERVER_ERROR, "Setting default operator in schema (solrQueryParser/@defaultOperator) not supported");
}
// /schema/uniqueKey/text()
TinyTextualElement.TinyTextualElementText tnode = (TinyTextualElement.TinyTextualElementText) loader.configXpathExpressions.schemaUniqueKeyExp.evaluate(document, XPathConstants.NODE);
if (tnode==null) {
log.warn("no {} specified in schema.", UNIQUE_KEY);
} else {
uniqueKeyField=getIndexedField(tnode.getStringValue());
uniqueKeyFieldName=uniqueKeyField.getName();
uniqueKeyFieldType=uniqueKeyField.getType();
// we fail on init if the ROOT field is *explicitly* defined as incompatible with uniqueKey
// we don't want ot fail if there happens to be a dynamicField matching ROOT, (ie: "*")
// because the user may not care about child docs at all. The run time code
// related to child docs can catch that if it happens
// MRM TODO:
if (fields.containsKey(ROOT_FIELD_NAME) && ! isUsableForChildDocs()) {
String msg = ROOT_FIELD_NAME + " field must be defined using the exact same fieldType as the " +
UNIQUE_KEY + " field ("+uniqueKeyFieldName+") uses: " + uniqueKeyFieldType.getTypeName();
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
if (null != uniqueKeyField.getDefaultValue()) {
String msg = UNIQUE_KEY + " field ("+uniqueKeyFieldName+
") can not be configured with a default value ("+
uniqueKeyField.getDefaultValue()+")";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
if (!uniqueKeyField.stored()) {
log.warn("{} is not stored - distributed search and MoreLikeThis will not work", UNIQUE_KEY);
}
if (uniqueKeyField.multiValued()) {
String msg = UNIQUE_KEY + " field ("+uniqueKeyFieldName+
") can not be configured to be multivalued";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
if (uniqueKeyField.getType().isPointField()) {
String msg = UNIQUE_KEY + " field ("+uniqueKeyFieldName+
") can not be configured to use a Points based FieldType: " + uniqueKeyField.getType().getTypeName();
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
// Unless the uniqueKeyField is marked 'required=false' then make sure it exists
if( Boolean.FALSE != explicitRequiredProp.get( uniqueKeyFieldName ) ) {
uniqueKeyField.required = true;
requiredFields.add(uniqueKeyField);
}
}
/////////////// parse out copyField commands ///////////////
// Map<String,ArrayList<SchemaField>> cfields = new HashMap<String,ArrayList<SchemaField>>();
// expression = "/schema/copyField";
dynamicCopyFields = EMPTY_DYNAMIC_COPY_FIELDS;
StopWatch timeLoadCopyFields = new StopWatch(SCHEMA + "-loadCopyFields");
loadCopyFields(document);
timeLoadCopyFields.done();
timeParseSchemaDom.done();
StopWatch timePostReadInform = new StopWatch(SCHEMA + "-postReadInform");
postReadInform();
timePostReadInform.done();
// create the field analyzers
StopWatch timeRefreshAnalyzers = new StopWatch(SCHEMA + "-refreshAnalyzers");
refreshAnalyzers();
timeRefreshAnalyzers.done();
} catch (SolrException e) {
log.error("readSchema Exception", e);
throw new SolrException(ErrorCode.getErrorCode(e.code()),
"Can't load schema " + loader.resourceLocation(resourceName) + ": " + e.getMessage(), e);
} catch(Exception e) {
log.error("readSchema Exception", e);
// unexpected exception...
throw new SolrException(ErrorCode.SERVER_ERROR,
"Can't load schema " + loader.resourceLocation(resourceName) + ": " + e.getMessage(), e);
}
log.info("Loaded schema {}/{} with uniqueid field {}", name, version, uniqueKeyFieldName);
}
public void postReadInform() {
//Run the callbacks on SchemaAware now that everything else is done
try (ParWork work = new ParWork(this)) {
for (SchemaAware aware : schemaAware) {
work.collect("", ()->{
aware.inform(this);
});
}
}
}
/**
* Loads fields and dynamic fields.
*
* @return a map from field name to explicit required value
*/
protected synchronized Map<String,Boolean> loadFields(NodeInfo document) throws XPathExpressionException {
// Hang on to the fields that say if they are required -- this lets us set a reasonable default for the unique key
Map<String,Boolean> explicitRequiredProp = new HashMap<>();
// /schema/field | /schema/dynamicField | /schema/fields/field | /schema/fields/dynamicField
ArrayList<DynamicField> dFields = new ArrayList<>();
Map<String,SchemaField> fields = new HashMap<>();
ArrayList<NodeInfo> nodes = (ArrayList) loader.configXpathExpressions.xpathOrExp.evaluate(document, XPathConstants.NODESET);
for (int i=0; i<nodes.size(); i++) {
NodeInfo node = nodes.get(i);
AttributeMap attrs = node.attributes();
String name = DOMUtil.getAttr(node, NAME, "field definition");
if (log.isTraceEnabled()) log.trace("reading field def {}", name);
String type = DOMUtil.getAttr(node, TYPE, "field " + name);
FieldType ft = fieldTypes.get(type);
if (ft==null) {
throw new SolrException
(ErrorCode.BAD_REQUEST, "Unknown " + FIELD_TYPE + " '" + type + "' specified on field " + name + " types:" + fieldTypes);
}
Map<String,String> args = DOMUtil.toMapExcept(attrs, NAME, TYPE);
if (null != args.get(REQUIRED)) {
explicitRequiredProp.put(name, Boolean.valueOf(args.get(REQUIRED)));
}
SchemaField f = SchemaField.create(name,ft,args);
String nodeValue = node.getDisplayName();
if (nodeValue.equals(FIELD)) {
SchemaField old = fields.put(f.getName(),f);
if( old != null ) {
String msg = "[schema.xml] Duplicate field definition for '"
+ f.getName() + "' [[["+old.toString()+"]]] and [[["+f.toString()+"]]]";
throw new SolrException(ErrorCode.SERVER_ERROR, msg );
}
if (log.isTraceEnabled()) log.trace("field defined: {}", f);
if( f.getDefaultValue() != null ) {
if (log.isTraceEnabled()) log.trace("{} contains default value {}", name, f.getDefaultValue());
fieldsWithDefaultValue.add( f );
}
if (f.isRequired()) {
if (log.isTraceEnabled()) log.trace("{} is required in this schema", name);
requiredFields.add(f);
}
} else if (nodeValue.equals(DYNAMIC_FIELD)) {
if (isValidDynamicField(dFields, f)) {
addDynamicFieldNoDupCheck(dFields, f);
}
} else {
// we should never get here
throw new RuntimeException("Unknown field type: " + nodeValue);
}
}
//fields with default values are by definition required
//add them to required fields, and we only have to loop once
// in DocumentBuilder.getDoc()
requiredFields.addAll(fieldsWithDefaultValue);
dynamicFields = dynamicFieldListToSortedArray(dFields);
this.fields = Collections.unmodifiableMap(fields);
return explicitRequiredProp;
}
/**
* Sort the dynamic fields and stuff them in a normal array for faster access.
*/
protected static DynamicField[] dynamicFieldListToSortedArray(List<DynamicField> dynamicFieldList) {
// Avoid creating the array twice by converting to an array first and using Arrays.sort(),
// rather than Collections.sort() then converting to an array, since Collections.sort()
// copies to an array first, then sets each collection member from the array.
DynamicField[] dFields = dynamicFieldList.toArray(new DynamicField[dynamicFieldList.size()]);
Arrays.sort(dFields);
if (log.isTraceEnabled()) {
log.trace("Dynamic Field Ordering: {}", Arrays.toString(dFields));
}
return dFields;
}
/**
* Loads the copy fields
*/
protected void loadCopyFields(NodeInfo document) throws XPathExpressionException {
ArrayList<NodeInfo> nodes = (ArrayList) loader.configXpathExpressions.copyFieldsExp.evaluate(document, XPathConstants.NODESET);
for (int i=0; i<nodes.size(); i++) {
NodeInfo node = nodes.get(i);
String source = DOMUtil.getAttr(node, SOURCE, COPY_FIELD + " definition");
String dest = DOMUtil.getAttr(node, DESTINATION, COPY_FIELD + " definition");
String maxChars = DOMUtil.getAttr(node, MAX_CHARS, null);
int maxCharsInt = CopyField.UNLIMITED;
if (maxChars != null) {
try {
maxCharsInt = Integer.parseInt(maxChars);
} catch (NumberFormatException e) {
log.warn("Couldn't parse {} attribute for '{}' from '{}' to '{}' as integer. The whole field will be copied."
, MAX_CHARS, COPY_FIELD, source, dest);
}
}
if (dest.equals(uniqueKeyFieldName)) {
String msg = UNIQUE_KEY + " field ("+uniqueKeyFieldName+
") can not be the " + DESTINATION + " of a " + COPY_FIELD + "(" + SOURCE + "=" +source+")";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
registerCopyField(source, dest, maxCharsInt);
}
for (Map.Entry<SchemaField, Integer> entry : copyFieldTargetCounts.entrySet()) {
if (entry.getValue() > 1 && !entry.getKey().multiValued()) {
log.warn("Field {} is not multivalued and destination for multiople {} ({})"
, entry.getKey().name, COPY_FIELDS, entry.getValue());
}
}
}
/**
* Converts a sequence of path steps into a rooted path, by inserting slashes in front of each step.
* @param steps The steps to join with slashes to form a path
* @return a rooted path: a leading slash followed by the given steps joined with slashes
*/
public static String stepsToPath(String... steps) {
StringBuilder builder = new StringBuilder();
for (String step : steps) { builder.append(SLASH).append(step); }
return builder.toString();
}
/** Returns true if the given name has exactly one asterisk either at the start or end of the name */
protected static boolean isValidFieldGlob(String name) {
if (name.startsWith("*") || name.endsWith("*")) {
int count = 0;
for (int pos = 0 ; pos < name.length() && -1 != (pos = name.indexOf('*', pos)) ; ++pos) ++count;
if (1 == count) return true;
}
return false;
}
protected boolean isValidDynamicField(List<DynamicField> dFields, SchemaField f) {
String glob = f.getName();
if (f.getDefaultValue() != null) {
throw new SolrException(ErrorCode.SERVER_ERROR,
DYNAMIC_FIELD + " can not have a default value: " + glob);
}
if (f.isRequired()) {
throw new SolrException(ErrorCode.SERVER_ERROR,
DYNAMIC_FIELD + " can not be required: " + glob);
}
if ( ! isValidFieldGlob(glob)) {
String msg = "Dynamic field name '" + glob
+ "' should have either a leading or a trailing asterisk, and no others.";
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
if (isDuplicateDynField(dFields, f)) {
String msg = "[schema.xml] Duplicate DynamicField definition for '" + glob + "'";
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
return true;
}
/**
* Register one or more new Dynamic Fields with the Schema.
* @param fields The sequence of {@link org.apache.solr.schema.SchemaField}
*/
public void registerDynamicFields(SchemaField... fields) {
List<DynamicField> dynFields = new ArrayList<>(Arrays.asList(dynamicFields));
for (SchemaField field : fields) {
if (isDuplicateDynField(dynFields, field)) {
if (log.isDebugEnabled()) {
log.debug("dynamic field already exists: dynamic field: [{}]", field.getName());
}
} else {
if (log.isDebugEnabled()) {
log.debug("dynamic field creation for schema field: {}", field.getName());
}
addDynamicFieldNoDupCheck(dynFields, field);
}
}
dynamicFields = dynamicFieldListToSortedArray(dynFields);
}
private void addDynamicFieldNoDupCheck(List<DynamicField> dFields, SchemaField f) {
dFields.add(new DynamicField(f));
if (log.isTraceEnabled()) log.trace("dynamic field defined: {}", f);
}
protected boolean isDuplicateDynField(List<DynamicField> dFields, SchemaField f) {
for (DynamicField df : dFields) {
if (df.getRegex().equals(f.name)) {
return true;
}
}
return false;
}
public void registerCopyField( String source, String dest ) {
registerCopyField(source, dest, CopyField.UNLIMITED);
}
public void registerCopyField(String source, String dest, int maxChars) {
log.debug("{} {}='{}' {}='{}' {}='{}'", COPY_FIELD, SOURCE, source, DESTINATION, dest
,MAX_CHARS, maxChars);
DynamicField destDynamicField = null;
SchemaField destSchemaField = fields.get(dest);
SchemaField sourceSchemaField = fields.get(source);
DynamicField sourceDynamicBase = null;
DynamicField destDynamicBase = null;
boolean sourceIsDynamicFieldReference = false;
boolean sourceIsExplicitFieldGlob = false;
final String invalidGlobMessage = "is an invalid glob: either it contains more than one asterisk,"
+ " or the asterisk occurs neither at the start nor at the end.";
final boolean sourceIsGlob = isValidFieldGlob(source);
if (source.contains("*") && ! sourceIsGlob) {
String msg = "copyField source :'" + source + "' " + invalidGlobMessage;
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
if (dest.contains("*") && ! isValidFieldGlob(dest)) {
String msg = "copyField dest :'" + dest + "' " + invalidGlobMessage;
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
if (null == sourceSchemaField && sourceIsGlob) {
Pattern pattern = Pattern.compile(source.replace("*", ".*")); // glob->regex
for (String field : fields.keySet()) {
if (pattern.matcher(field).matches()) {
sourceIsExplicitFieldGlob = true;
break;
}
}
}
if (null == destSchemaField || (null == sourceSchemaField && ! sourceIsExplicitFieldGlob)) {
// Go through dynamicFields array only once, collecting info for both source and dest fields, if needed
for (DynamicField dynamicField : dynamicFields) {
if (null == sourceSchemaField && ! sourceIsDynamicFieldReference && ! sourceIsExplicitFieldGlob) {
if (dynamicField.matches(source)) {
sourceIsDynamicFieldReference = true;
if ( ! source.equals(dynamicField.getRegex())) {
sourceDynamicBase = dynamicField;
}
}
}
if (null == destSchemaField) {
if (dest.equals(dynamicField.getRegex())) {
destDynamicField = dynamicField;
destSchemaField = dynamicField.prototype;
} else if (dynamicField.matches(dest)) {
destSchemaField = dynamicField.makeSchemaField(dest);
destDynamicField = new DynamicField(destSchemaField);
destDynamicBase = dynamicField;
}
}
if (null != destSchemaField
&& (null != sourceSchemaField || sourceIsDynamicFieldReference || sourceIsExplicitFieldGlob)) {
break;
}
}
}
if (null == sourceSchemaField && ! sourceIsGlob && ! sourceIsDynamicFieldReference) {
String msg = "copyField source :'" + source + "' is not a glob and doesn't match any explicit field or dynamicField.";
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
if (null == destSchemaField) {
String msg = "copyField dest :'" + dest + "' is not an explicit field and doesn't match a dynamicField. " + dynamicFields;
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
if (sourceIsGlob) {
if (null != destDynamicField) { // source: glob ; dest: dynamic field ref
registerDynamicCopyField(new DynamicCopy(source, destDynamicField, maxChars, sourceDynamicBase, destDynamicBase));
incrementCopyFieldTargetCount(destSchemaField);
} else { // source: glob ; dest: explicit field
destDynamicField = new DynamicField(destSchemaField);
registerDynamicCopyField(new DynamicCopy(source, destDynamicField, maxChars, sourceDynamicBase, null));
incrementCopyFieldTargetCount(destSchemaField);
}
} else if (sourceIsDynamicFieldReference) {
if (null != destDynamicField) { // source: no-asterisk dynamic field ref ; dest: dynamic field ref
registerDynamicCopyField(new DynamicCopy(source, destDynamicField, maxChars, sourceDynamicBase, destDynamicBase));
incrementCopyFieldTargetCount(destSchemaField);
} else { // source: no-asterisk dynamic field ref ; dest: explicit field
sourceSchemaField = getField(source);
registerExplicitSrcAndDestFields(source, maxChars, destSchemaField, sourceSchemaField);
}
} else {
if (null != destDynamicField) { // source: explicit field ; dest: dynamic field reference
if (destDynamicField.pattern instanceof DynamicReplacement.DynamicPattern.NameEquals) {
// Dynamic dest with no asterisk is acceptable
registerDynamicCopyField(new DynamicCopy(source, destDynamicField, maxChars, sourceDynamicBase, destDynamicBase));
incrementCopyFieldTargetCount(destSchemaField);
} else { // source: explicit field ; dest: dynamic field with an asterisk
String msg = "copyField only supports a dynamic destination with an asterisk "
+ "if the source also has an asterisk";
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
} else { // source & dest: explicit fields
registerExplicitSrcAndDestFields(source, maxChars, destSchemaField, sourceSchemaField);
}
}
}
protected void registerExplicitSrcAndDestFields(String source, int maxChars, SchemaField destSchemaField, SchemaField sourceSchemaField) {
List<CopyField> copyFieldList = copyFieldsMap.get(source);
if (copyFieldList == null) {
copyFieldList = new ArrayList<>();
copyFieldsMap.put(source, copyFieldList);
}
copyFieldList.add(new CopyField(sourceSchemaField, destSchemaField, maxChars));
incrementCopyFieldTargetCount(destSchemaField);
}
private void incrementCopyFieldTargetCount(SchemaField dest) {
copyFieldTargetCounts.put(dest, copyFieldTargetCounts.containsKey(dest) ? copyFieldTargetCounts.get(dest) + 1 : 1);
}
private void registerDynamicCopyField(DynamicCopy dcopy) {
DynamicCopy[] temp = new DynamicCopy[dynamicCopyFields.length + 1];
System.arraycopy(dynamicCopyFields, 0, temp, 0, dynamicCopyFields.length);
temp[temp.length - 1] = dcopy;
dynamicCopyFields = temp;
}
static SimilarityFactory readSimilarity(SolrResourceLoader loader, NodeInfo node) {
if (node==null) {
return null;
} else {
SimilarityFactory similarityFactory;
final String classArg = ((TinyElementImpl) node).attributes().get("", SimilarityFactory.CLASS_NAME).getValue();
final Object obj = loader.newInstance(classArg, Object.class, "search.similarities.");
if (obj instanceof SimilarityFactory) {
// configure a factory, get a similarity back
final NamedList<Object> namedList = DOMUtil.childNodesToNamedList(node);
namedList.add(SimilarityFactory.CLASS_NAME, classArg);
SolrParams params = namedList.toSolrParams();
similarityFactory = (SimilarityFactory)obj;
similarityFactory.init(params);
} else {
// just like always, assume it's a Similarity and get a ClassCastException - reasonable error handling
similarityFactory = new SimilarityFactory() {
@Override
public Similarity getSimilarity() {
return (Similarity) obj;
}
};
}
return similarityFactory;
}
}
public static abstract class DynamicReplacement implements Comparable<DynamicReplacement> {
abstract public static class DynamicPattern {
protected final String regex;
protected final String fixedStr;
protected DynamicPattern(String regex, String fixedStr) { this.regex = regex; this.fixedStr = fixedStr; }
static DynamicPattern createPattern(String regex) {
if (regex.startsWith("*")) { return new NameEndsWith(regex); }
else if (regex.endsWith("*")) { return new NameStartsWith(regex); }
else { return new NameEquals(regex);
}
}
/** Returns true if the given name matches this pattern */
abstract boolean matches(String name);
/** Returns the remainder of the given name after removing this pattern's fixed string component */
abstract String remainder(String name);
/** Returns the result of combining this pattern's fixed string component with the given replacement */
abstract String subst(String replacement);
/** Returns the length of the original regex, including the asterisk, if any. */
public int length() { return regex.length(); }
private static class NameStartsWith extends DynamicPattern {
NameStartsWith(String regex) { super(regex, regex.substring(0, regex.length() - 1)); }
boolean matches(String name) { return name.startsWith(fixedStr); }
String remainder(String name) { return name.substring(fixedStr.length()); }
String subst(String replacement) { return fixedStr + replacement; }
}
private static class NameEndsWith extends DynamicPattern {
NameEndsWith(String regex) { super(regex, regex.substring(1)); }
boolean matches(String name) { return name.endsWith(fixedStr); }
String remainder(String name) { return name.substring(0, name.length() - fixedStr.length()); }
String subst(String replacement) { return replacement + fixedStr; }
}
private static class NameEquals extends DynamicPattern {
NameEquals(String regex) { super(regex, regex); }
boolean matches(String name) { return regex.equals(name); }
String remainder(String name) { return ""; }
String subst(String replacement) { return fixedStr; }
}
}
protected DynamicPattern pattern;
public boolean matches(String name) { return pattern.matches(name); }
protected DynamicReplacement(String regex) {
pattern = DynamicPattern.createPattern(regex);
}
/**
* Sort order is based on length of regex. Longest comes first.
* @param other The object to compare to.
* @return a negative integer, zero, or a positive integer
* as this object is less than, equal to, or greater than
* the specified object.
*/
@Override
public int compareTo(DynamicReplacement other) {
return other.pattern.length() - pattern.length();
}
/** Returns the regex used to create this instance's pattern */
public String getRegex() {
return pattern.regex;
}
}
public final static class DynamicField extends DynamicReplacement {
private final SchemaField prototype;
public SchemaField getPrototype() { return prototype; }
DynamicField(SchemaField prototype) {
super(prototype.name);
this.prototype=prototype;
}
SchemaField makeSchemaField(String name) {
// could have a cache instead of returning a new one each time, but it might
// not be worth it.
// Actually, a higher level cache could be worth it to avoid too many
// .startsWith() and .endsWith() comparisons. it depends on how many
// dynamic fields there are.
return new SchemaField(prototype, name);
}
@Override
public String toString() {
return prototype.toString();
}
}
public static class DynamicCopy extends DynamicReplacement {
private final DynamicField destination;
private final int maxChars;
public int getMaxChars() { return maxChars; }
final DynamicField sourceDynamicBase;
public DynamicField getSourceDynamicBase() { return sourceDynamicBase; }
final DynamicField destDynamicBase;
public DynamicField getDestDynamicBase() { return destDynamicBase; }
DynamicCopy(String sourceRegex, DynamicField destination, int maxChars,
DynamicField sourceDynamicBase, DynamicField destDynamicBase) {
super(sourceRegex);
this.destination = destination;
this.maxChars = maxChars;
this.sourceDynamicBase = sourceDynamicBase;
this.destDynamicBase = destDynamicBase;
}
public DynamicField getDestination() { return destination; }
public String getDestFieldName() { return destination.getRegex(); }
/**
* Generates a destination field name based on this source pattern,
* by substituting the remainder of this source pattern into the
* the given destination pattern.
*/
public SchemaField getTargetField(String sourceField) {
String remainder = pattern.remainder(sourceField);
String targetFieldName = destination.pattern.subst(remainder);
return destination.makeSchemaField(targetFieldName);
}
@Override
public String toString() {
return destination.prototype.toString();
}
}
public SchemaField[] getDynamicFieldPrototypes() {
SchemaField[] df = new SchemaField[dynamicFields.length];
for (int i=0;i<dynamicFields.length;i++) {
df[i] = dynamicFields[i].prototype;
}
return df;
}
public String getDynamicPattern(String fieldName) {
for (DynamicField df : dynamicFields) {
if (df.matches(fieldName)) return df.getRegex();
}
return null;
}
/**
* Does the schema explicitly define the specified field, i.e. not as a result
* of a copyField declaration? We consider it explicitly defined if it matches
* a field name or a dynamicField name.
* @return true if explicitly declared in the schema.
*/
public boolean hasExplicitField(String fieldName) {
if (fields.containsKey(fieldName)) {
return true;
}
for (DynamicField df : dynamicFields) {
if (fieldName.equals(df.getRegex())) return true;
}
return false;
}
/**
* Is the specified field dynamic or not.
* @return true if the specified field is dynamic
*/
public boolean isDynamicField(String fieldName) {
if(fields.containsKey(fieldName)) {
return false;
}
for (DynamicField df : dynamicFields) {
if (df.matches(fieldName)) return true;
}
return false;
}
/**
* Returns the SchemaField that should be used for the specified field name, or
* null if none exists.
*
* @param fieldName may be an explicitly defined field or a name that
* matches a dynamic field.
* @see #getFieldType
* @see #getField(String)
* @return The {@link org.apache.solr.schema.SchemaField}
*/
public SchemaField getFieldOrNull(String fieldName) {
SchemaField f = fields.get(fieldName);
if (f != null) return f;
f = dynamicFieldCache.get(fieldName);
if (f != null) return f;
for (DynamicField df : dynamicFields) {
if (df.matches(fieldName)) {
dynamicFieldCache.put(fieldName, f = df.makeSchemaField(fieldName));
break;
}
}
return f;
}
/**
* Returns the SchemaField that should be used for the specified field name
*
* @param fieldName may be an explicitly defined field or a name that
* matches a dynamic field.
* @throws SolrException if no such field exists
* @see #getFieldType
* @see #getFieldOrNull(String)
* @return The {@link SchemaField}
*/
public SchemaField getField(String fieldName) {
SchemaField f = getFieldOrNull(fieldName);
if (f != null) return f;
// Hmmm, default field could also be implemented with a dynamic field of "*".
// It would have to be special-cased and only used if nothing else matched.
/*** REMOVED -YCS
if (defaultFieldType != null) return new SchemaField(fieldName,defaultFieldType, fields);
***/
throw new SolrException(ErrorCode.BAD_REQUEST,"undefined field: \""+fieldName+"\"");
}
/**
* Returns the FieldType for the specified field name.
*
* <p>
* This method exists because it can be more efficient then
* {@link #getField} for dynamic fields if a full SchemaField isn't needed.
* </p>
*
* @param fieldName may be an explicitly created field, or a name that
* excercises a dynamic field.
* @throws SolrException if no such field exists
* @see #getField(String)
* @see #getFieldTypeNoEx
*/
public FieldType getFieldType(String fieldName) {
SchemaField f = fields.get(fieldName);
if (f != null) return f.getType();
return getDynamicFieldType(fieldName);
}
/**
* Given the name of a {@link org.apache.solr.schema.FieldType} (not to be confused with {@link #getFieldType(String)} which
* takes in the name of a field), return the {@link org.apache.solr.schema.FieldType}.
* @param fieldTypeName The name of the {@link FieldType}
* @param fieldTypes
* @return The {@link org.apache.solr.schema.FieldType} or null.
*/
public FieldType getFieldTypeByName(String fieldTypeName, Map<String,FieldType> fieldTypes){
return fieldTypes.get(fieldTypeName);
}
/**
* Returns the FieldType for the specified field name.
*
* <p>
* This method exists because it can be more efficient then
* {@link #getField} for dynamic fields if a full SchemaField isn't needed.
* </p>
*
* @param fieldName may be an explicitly created field, or a name that
* exercises a dynamic field.
* @return null if field is not defined.
* @see #getField(String)
* @see #getFieldTypeNoEx
*/
public FieldType getFieldTypeNoEx(String fieldName) {
SchemaField f = fields.get(fieldName);
if (f != null) return f.getType();
return dynFieldType(fieldName);
}
/**
* Returns the FieldType of the best matching dynamic field for
* the specified field name
*
* @param fieldName may be an explicitly created field, or a name that
* exercises a dynamic field.
* @throws SolrException if no such field exists
* @see #getField(String)
* @see #getFieldTypeNoEx
*/
public FieldType getDynamicFieldType(String fieldName) {
for (DynamicField df : dynamicFields) {
if (df.matches(fieldName)) return df.prototype.getType();
}
throw new SolrException(ErrorCode.BAD_REQUEST,"undefined field "+fieldName);
}
private FieldType dynFieldType(String fieldName) {
for (DynamicField df : dynamicFields) {
if (df.matches(fieldName)) return df.prototype.getType();
}
return null;
}
/**
* Get all copy fields, both the static and the dynamic ones.
* @return Array of fields copied into this field
*/
public List<String> getCopySources(String destField) {
SchemaField f = getField(destField);
if (!isCopyFieldTarget(f)) {
return Collections.emptyList();
}
List<String> fieldNames = new ArrayList<>();
for (Map.Entry<String, List<CopyField>> cfs : copyFieldsMap.entrySet()) {
for (CopyField copyField : cfs.getValue()) {
if (copyField.getDestination().getName().equals(destField)) {
fieldNames.add(copyField.getSource().getName());
}
}
}
for (DynamicCopy dynamicCopy : dynamicCopyFields) {
if (dynamicCopy.getDestFieldName().equals(destField)) {
fieldNames.add(dynamicCopy.getRegex());
}
}
return fieldNames;
}
/**
* Get all copy fields for a specified source field, both static
* and dynamic ones.
* @return List of CopyFields to copy to.
* @since solr 1.4
*/
// This is useful when we need the maxSize param of each CopyField
public List<CopyField> getCopyFieldsList(final String sourceField){
final List<CopyField> result = new ArrayList<>();
for (DynamicCopy dynamicCopy : dynamicCopyFields) {
if (dynamicCopy.matches(sourceField)) {
result.add(new CopyField(getField(sourceField), dynamicCopy.getTargetField(sourceField), dynamicCopy.maxChars));
}
}
List<CopyField> fixedCopyFields = copyFieldsMap.get(sourceField);
if (null != fixedCopyFields) {
result.addAll(fixedCopyFields);
}
return result;
}
/**
* Check if a field is used as the destination of a copyField operation
*
* @since solr 1.3
*/
public boolean isCopyFieldTarget( SchemaField f ) {
return copyFieldTargetCounts.containsKey( f );
}
/**
* Get a map of property name -&gt; value for the whole schema.
*/
public Map getNamedPropertyValues() {
return getNamedPropertyValues(null, new MapSolrParams(Collections.EMPTY_MAP));
}
public static class SchemaProps implements MapSerializable {
private static final String SOURCE_FIELD_LIST = IndexSchema.SOURCE + "." + CommonParams.FL;
private static final String DESTINATION_FIELD_LIST = IndexSchema.DESTINATION + "." + CommonParams.FL;
public final String name;
private final SolrParams params;
private final IndexSchema schema;
boolean showDefaults, includeDynamic;
Set<String> requestedFields;
private Set<String> requestedSourceFields;
private Set<String> requestedDestinationFields;
public enum Handler {
NAME(IndexSchema.NAME, sp -> sp.schema.getSchemaName()),
VERSION(IndexSchema.VERSION, sp -> sp.schema.getVersion()),
UNIQUE_KEY(IndexSchema.UNIQUE_KEY, sp -> sp.schema.uniqueKeyFieldName),
SIMILARITY(IndexSchema.SIMILARITY, sp -> sp.schema.isExplicitSimilarity ?
sp.schema.similarityFactory.getNamedPropertyValues() :
null),
FIELD_TYPES(IndexSchema.FIELD_TYPES, sp -> new TreeMap<>(sp.schema.fieldTypes)
.values().stream()
.map(it -> it.getNamedPropertyValues(sp.showDefaults))
.collect(Collectors.toList())),
FIELDS(IndexSchema.FIELDS, sp -> {
List<SimpleOrderedMap> result = (sp.requestedFields != null ? sp.requestedFields : new TreeSet<>(sp.schema.fields.keySet()))
.stream()
.map(sp.schema::getFieldOrNull)
.filter(it -> it != null)
.filter(it -> sp.includeDynamic || !sp.schema.isDynamicField(it.name))
.map(sp::getProperties)
.collect(Collectors.toList());
if (sp.includeDynamic && sp.requestedFields == null) {
result.addAll(sp.applyDynamic());
}
return result;
}),
DYNAMIC_FIELDS(IndexSchema.DYNAMIC_FIELDS, sp -> Stream.of(sp.schema.dynamicFields)
.filter(it -> !it.getRegex().startsWith(INTERNAL_POLY_FIELD_PREFIX))
.filter(it -> sp.requestedFields == null || sp.requestedFields.contains(it.getPrototype().getName()))
.map(it -> sp.getProperties(it.getPrototype()))
.collect(Collectors.toList())),
COPY_FIELDS(IndexSchema.COPY_FIELDS, sp -> sp.schema.getCopyFieldProperties(false,
sp.requestedSourceFields, sp.requestedDestinationFields));
final Function<SchemaProps, Object> fun;
public final String realName, nameLower;
Handler(String name, Function<SchemaProps, Object> fun) {
this.fun = fun;
this.realName = name;
nameLower = name.toLowerCase(Locale.ROOT);
}
public String getRealName(){
return realName;
}
public String getNameLower(){
return nameLower;
}
}
SchemaProps(String name, SolrParams params, IndexSchema schema) {
this.name = name;
this.params = params;
this.schema = schema;
showDefaults = params.getBool("showDefaults", false);
includeDynamic = params.getBool("includeDynamic", false);
requestedSourceFields = readMultiVals(SOURCE_FIELD_LIST);
requestedDestinationFields = readMultiVals(DESTINATION_FIELD_LIST);
requestedFields = readMultiVals(CommonParams.FL);
}
public Collection applyDynamic(){
return (Collection) Handler.DYNAMIC_FIELDS.fun.apply(this);
}
private Set<String> readMultiVals(String name) {
String flParam = params.get(name);
if (null != flParam) {
String[] fields = flParam.trim().split("[,\\s]+");
if (fields.length > 0)
return new LinkedHashSet<>(Stream.of(fields)
.filter(it -> !it.trim().isEmpty())
.collect(Collectors.toList()));
}
return null;
}
SimpleOrderedMap getProperties(SchemaField sf) {
SimpleOrderedMap<Object> result = sf.getNamedPropertyValues(showDefaults);
if (schema.isDynamicField(sf.name)) {
String dynamicBase = schema.getDynamicPattern(sf.getName());
// Add dynamicBase property if it's different from the field name.
if (!sf.getName().equals(dynamicBase)) {
result.add("dynamicBase", dynamicBase);
}
}
return result;
}
@Override
public Map<String, Object> toMap(Map<String, Object> map) {
return Stream.of(Handler.values())
.filter(it -> name == null || it.nameLower.equals(name))
.map(it -> new Pair<>(it.realName, it.fun.apply(this)))
.filter(it->it.second() != null)
.collect(Collectors.toMap(
Pair::first,
Pair::second,
(v1, v2) -> v2,
LinkedHashMap::new));
}
}
public static Map<String,String> nameMapping = Collections.unmodifiableMap(Stream.of(SchemaProps.Handler.values())
.collect(Collectors.toMap(SchemaProps.Handler::getNameLower , SchemaProps.Handler::getRealName)));
public Map<String, Object> getNamedPropertyValues(String name, SolrParams params) {
return new SchemaProps(name, params, this).toMap(new LinkedHashMap<>());
}
/**
* Returns a list of copyField directives, with optional details and optionally restricting to those
* directives that contain the requested source and/or destination field names.
*
* @param showDetails If true, source and destination dynamic bases, and explicit fields matched by source globs,
* will be added to dynamic copyField directives where appropriate
* @param requestedSourceFields If not null, output is restricted to those copyField directives
* with the requested source field names
* @param requestedDestinationFields If not null, output is restricted to those copyField directives
* with the requested destination field names
* @return a list of copyField directives
*/
public List<SimpleOrderedMap<Object>> getCopyFieldProperties
(boolean showDetails, Set<String> requestedSourceFields, Set<String> requestedDestinationFields) {
List<SimpleOrderedMap<Object>> copyFieldProperties = new ArrayList<>();
SortedMap<String,List<CopyField>> sortedCopyFields = new TreeMap<>(copyFieldsMap);
for (List<CopyField> copyFields : sortedCopyFields.values()) {
copyFields = new ArrayList<>(copyFields);
Collections.sort(copyFields, (cf1, cf2) -> {
// sources are all the same, just sorting by destination here
return cf1.getDestination().getName().compareTo(cf2.getDestination().getName());
});
for (CopyField copyField : copyFields) {
final String source = copyField.getSource().getName();
final String destination = copyField.getDestination().getName();
if ( (null == requestedSourceFields || requestedSourceFields.contains(source))
&& (null == requestedDestinationFields || requestedDestinationFields.contains(destination))) {
SimpleOrderedMap<Object> props = new SimpleOrderedMap<>();
props.add(SOURCE, source);
props.add(DESTINATION, destination);
if (0 != copyField.getMaxChars()) {
props.add(MAX_CHARS, copyField.getMaxChars());
}
copyFieldProperties.add(props);
}
}
}
for (IndexSchema.DynamicCopy dynamicCopy : dynamicCopyFields) {
final String source = dynamicCopy.getRegex();
final String destination = dynamicCopy.getDestFieldName();
if ((null == requestedSourceFields || requestedSourceFields.contains(source))
&& (null == requestedDestinationFields || requestedDestinationFields.contains(destination))) {
SimpleOrderedMap<Object> dynamicCopyProps = new SimpleOrderedMap<>();
dynamicCopyProps.add(SOURCE, dynamicCopy.getRegex());
if (showDetails) {
IndexSchema.DynamicField sourceDynamicBase = dynamicCopy.getSourceDynamicBase();
if (null != sourceDynamicBase) {
dynamicCopyProps.add(SOURCE_DYNAMIC_BASE, sourceDynamicBase.getRegex());
} else if (source.contains("*")) {
List<String> sourceExplicitFields = new ArrayList<>();
Pattern pattern = Pattern.compile(source.replace("*", ".*")); // glob->regex
for (String field : fields.keySet()) {
if (pattern.matcher(field).matches()) {
sourceExplicitFields.add(field);
}
}
if (sourceExplicitFields.size() > 0) {
Collections.sort(sourceExplicitFields);
dynamicCopyProps.add(SOURCE_EXPLICIT_FIELDS, sourceExplicitFields);
}
}
}
dynamicCopyProps.add(DESTINATION, dynamicCopy.getDestFieldName());
if (showDetails) {
IndexSchema.DynamicField destDynamicBase = dynamicCopy.getDestDynamicBase();
if (null != destDynamicBase) {
dynamicCopyProps.add(DESTINATION_DYNAMIC_BASE, destDynamicBase.getRegex());
}
}
if (0 != dynamicCopy.getMaxChars()) {
dynamicCopyProps.add(MAX_CHARS, dynamicCopy.getMaxChars());
}
copyFieldProperties.add(dynamicCopyProps);
}
}
return copyFieldProperties;
}
/**
* Copies this schema, adds the given field to the copy
*
* @param newField the SchemaField to add
* @param persist to persist the schema or not
* @return a new IndexSchema based on this schema with newField added
* @see #newField(String, String, Map)
*/
public IndexSchema addField(SchemaField newField, boolean persist) {
return addFields(Collections.singletonList(newField), Collections.emptyMap(), persist);
}
public IndexSchema addField(SchemaField newField) {
return addField(newField, true);
}
/**
* Copies this schema, adds the given field to the copy
*
* @param newField the SchemaField to add
* @param copyFieldNames 0 or more names of targets to copy this field to. The targets must already exist.
* @return a new IndexSchema based on this schema with newField added
* @see #newField(String, String, Map)
*/
public IndexSchema addField(SchemaField newField, Collection<String> copyFieldNames) {
return addFields(singletonList(newField), singletonMap(newField.getName(), copyFieldNames), true);
}
/**
* Copies this schema, adds the given fields to the copy.
*
* @param newFields the SchemaFields to add
* @return a new IndexSchema based on this schema with newFields added
* @see #newField(String, String, Map)
*/
public IndexSchema addFields(Collection<SchemaField> newFields) {
return addFields(newFields, Collections.<String, Collection<String>>emptyMap(), true);
}
/**
* Copies this schema, adds the given fields to the copy
*
* @param newFields the SchemaFields to add
* @param copyFieldNames 0 or more names of targets to copy this field to. The target fields must already exist.
* @param persist Persist the schema or not
* @return a new IndexSchema based on this schema with newFields added
* @see #newField(String, String, Map)
*/
public IndexSchema addFields(Collection<SchemaField> newFields, Map<String, Collection<String>> copyFieldNames, boolean persist) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema, deletes the named fields from the copy.
* <p>
* The schema will not be persisted.
* <p>
*
* @param names the names of the fields to delete
* @return a new IndexSchema based on this schema with the named fields deleted
*/
public IndexSchema deleteFields(Collection<String> names) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema, deletes the named field from the copy, creates a new field
* with the same name using the given args, then rebinds any referring copy fields
* to the replacement field.
*
* <p>
* The schema will not be persisted.
* <p>
*
* @param fieldName The name of the field to be replaced
* @param replacementFieldType The field type of the replacement field
* @param replacementArgs Initialization params for the replacement field
* @return a new IndexSchema based on this schema with the named field replaced
*/
public IndexSchema replaceField(String fieldName, FieldType replacementFieldType, Map<String,?> replacementArgs) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema, adds the given dynamic fields to the copy,
* Requires synchronizing on the object returned by
*
* @param newDynamicFields the SchemaFields to add
* @param copyFieldNames 0 or more names of targets to copy this field to. The target fields must already exist.
* @param persist to persist the schema or not
* @return a new IndexSchema based on this schema with newDynamicFields added
* @see #newDynamicField(String, String, Map)
*/
public IndexSchema addDynamicFields
(Collection<SchemaField> newDynamicFields,
Map<String, Collection<String>> copyFieldNames,
boolean persist) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema, deletes the named dynamic fields from the copy.
* <p>
* The schema will not be persisted.
* <p>
* Requires synchronizing on the object returned by
*
* @param fieldNamePatterns the names of the dynamic fields to delete
* @return a new IndexSchema based on this schema with the named dynamic fields deleted
*/
public IndexSchema deleteDynamicFields(Collection<String> fieldNamePatterns) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema, deletes the named dynamic field from the copy, creates a new dynamic
* field with the same field name pattern using the given args, then rebinds any referring
* dynamic copy fields to the replacement dynamic field.
*
* <p>
* The schema will not be persisted.
* <p>
*
* @param fieldNamePattern The glob for the dynamic field to be replaced
* @param replacementFieldType The field type of the replacement dynamic field
* @param replacementArgs Initialization params for the replacement dynamic field
* @return a new IndexSchema based on this schema with the named dynamic field replaced
*/
public ManagedIndexSchema replaceDynamicField
(String fieldNamePattern, FieldType replacementFieldType, Map<String,?> replacementArgs) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema and adds the new copy fields to the copy
* @see #addCopyFields(String,Collection,int) to limit the number of copied characters.
*
* @param copyFields Key is the name of the source field name, value is a collection of target field names. Fields must exist.
* @param persist to persist the schema or not
* @return The new Schema with the copy fields added
*/
public IndexSchema addCopyFields(Map<String, Collection<String>> copyFields, boolean persist) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema and adds the new copy fields to the copy.
*
* @param source source field name
* @param destinations collection of target field names
* @param maxChars max number of characters to copy from the source to each
* of the destinations. Use {@link CopyField#UNLIMITED}
* if you don't want to limit the number of copied chars.
* @return The new Schema with the copy fields added
*/
public IndexSchema addCopyFields(String source, Collection<String> destinations, int maxChars) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema and deletes the given copy fields from the copy.
* <p>
* The schema will not be persisted.
* <p>
* @param copyFields Key is the name of the source field name, value is a collection of target field names.
* Each corresponding copy field directives must exist.
* @return The new Schema with the copy fields deleted
*/
public IndexSchema deleteCopyFields(Map<String, Collection<String>> copyFields) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Returns a SchemaField if the given fieldName does not already
* exist in this schema, and does not match any dynamic fields
* in this schema. The resulting SchemaField can be used in a call
* to {@link #addField(SchemaField)}.
*
* @param fieldName the name of the field to add
* @param fieldType the field type for the new field
* @param options the options to use when creating the SchemaField
* @return The created SchemaField
* @see #addField(SchemaField)
*/
public SchemaField newField(String fieldName, String fieldType, Map<String,?> options) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Returns a SchemaField if the given dynamic field glob does not already
* exist in this schema, and does not match any dynamic fields
* in this schema. The resulting SchemaField can be used in a call
* to {@link #addField(SchemaField)}.
*
* @param fieldNamePattern the pattern for the dynamic field to add
* @param fieldType the field type for the new field
* @param options the options to use when creating the SchemaField
* @return The created SchemaField
* @see #addField(SchemaField)
*/
public SchemaField newDynamicField(String fieldNamePattern, String fieldType, Map<String,?> options) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema, adds the given field type to the copy
*
* @param fieldTypeList a list of FieldTypes to add
* @param persist to persist the schema or not
* @return a new IndexSchema based on this schema with the new types added
* @see #newFieldType(String, String, Map, Map)
*/
public IndexSchema addFieldTypes(List<FieldType> fieldTypeList, boolean persist) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema, deletes the named field types from the copy.
* <p>
* The schema will not be persisted.
* <p>
*
* @param names the names of the field types to delete
* @return a new IndexSchema based on this schema with the named field types deleted
*/
public IndexSchema deleteFieldTypes(Collection<String> names) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema, deletes the named field type from the copy, creates a new field type
* with the same name using the given args, rebuilds fields and dynamic fields of the given
* type, then rebinds any referring copy fields to the rebuilt fields.
*
* <p>
* The schema will not be persisted.
* <p>
*
* @param typeName The name of the field type to be replaced
* @param replacementClassName The class name of the replacement field type
* @param replacementArgs Initialization params for the replacement field type
* @return a new IndexSchema based on this schema with the named field type replaced
*/
public IndexSchema replaceFieldType(String typeName, String replacementClassName, Map<String,Object> replacementArgs) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Returns a FieldType if the given typeName does not already
* exist in this schema. The resulting FieldType can be used in a call
* to {@link #addFieldTypes(java.util.List, boolean)}.
*
* @param typeName the name of the type to add
* @param className the name of the FieldType class
* @param options the options to use when creating the FieldType
* @return The created FieldType
* @see #addFieldTypes(java.util.List, boolean)
*/
public FieldType newFieldType(String typeName, String className, Map<String,?> options, Map<String,FieldType> fieldTypes) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
public static String getFieldTypeXPathExpressions() {
// /schema/fieldtype | /schema/fieldType | /schema/types/fieldtype | /schema/types/fieldType
String expression = stepsToPath(SCHEMA, FIELD_TYPE.toLowerCase(Locale.ROOT)) // backcompat(?)
+ XPATH_OR + stepsToPath(SCHEMA, FIELD_TYPE)
+ XPATH_OR + stepsToPath(SCHEMA, TYPES, FIELD_TYPE.toLowerCase(Locale.ROOT))
+ XPATH_OR + stepsToPath(SCHEMA, TYPES, FIELD_TYPE);
return expression;
}
/**
* Helper method that returns <code>true</code> if the {@link #ROOT_FIELD_NAME} uses the exact
* same 'type' as the {@link #getUniqueKeyField()}
*
* @lucene.internal
*/
public boolean isUsableForChildDocs() {
//TODO make this boolean a field so it needn't be looked up each time?
FieldType rootType = getFieldTypeNoEx(ROOT_FIELD_NAME);
return (null != uniqueKeyFieldType &&
null != rootType && rootType.getTypeName() != null &&
rootType.getTypeName().equals(uniqueKeyFieldType.getTypeName()));
}
/**
* Helper method that returns <code>true</code> if the {@link #ROOT_FIELD_NAME} uses the exact
* same 'type' as the {@link #getUniqueKeyField()} and has {@link #NEST_PATH_FIELD_NAME}
* defined as a {@link NestPathField}
* @lucene.internal
*/
public boolean savesChildDocRelations() {
//TODO make this boolean a field so it needn't be looked up each time?
if (!isUsableForChildDocs()) {
return false;
}
FieldType nestPathType = getFieldTypeNoEx(NEST_PATH_FIELD_NAME);
return nestPathType instanceof NestPathField;
}
/**
* Does this schema supports partial updates (aka atomic updates) and child docs as well.
*/
public boolean supportsPartialUpdatesOfChildDocs() {
if (savesChildDocRelations() == false) {
return false;
}
SchemaField rootField = getField(IndexSchema.ROOT_FIELD_NAME);
return rootField.stored() || rootField.hasDocValues();
}
public PayloadDecoder getPayloadDecoder(String field) {
FieldType ft = getFieldType(field);
if (ft == null)
return null;
return decoders.computeIfAbsent(ft, f -> PayloadUtils.getPayloadDecoder(ft));
}
}