/*
* 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.search.similarities;
import java.util.HashMap;
import org.apache.lucene.search.similarities.ClassicSimilarity;
import org.apache.lucene.search.similarities.BM25Similarity;
import org.apache.lucene.search.similarities.PerFieldSimilarityWrapper;
import org.apache.lucene.search.similarities.Similarity;
import org.apache.lucene.util.Version;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.core.SolrCore;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.SimilarityFactory;
import org.apache.solr.util.PayloadDecoder;
import org.apache.solr.util.PayloadUtils;
import org.apache.solr.util.plugin.SolrCoreAware;
/**
* <p>
* <code>SimilarityFactory</code> that returns a global {@link PerFieldSimilarityWrapper}
* that delegates to the field type, if it's configured. For field types that
* do not have a <code>Similarity</code> explicitly configured, the global <code>Similarity</code>
* will use per fieldtype defaults -- either based on an explicitly configured
* <code>defaultSimFromFieldType</code> a sensible default depending on the {@link Version}
* matching configured:
* </p>
* <ul>
* <li><code>luceneMatchVersion < 6.0</code> = {@link ClassicSimilarity}</li>
* <li><code>luceneMatchVersion >= 6.0</code> = {@link BM25Similarity}</li>
* </ul>
* <p>
* The <code>defaultSimFromFieldType</code> option accepts the name of any fieldtype, and uses
* whatever <code>Similarity</code> is explicitly configured for that fieldType as the default for
* all other field types. For example:
* </p>
* <pre class="prettyprint">
* <similarity class="solr.SchemaSimilarityFactory" >
* <str name="defaultSimFromFieldType">type-using-custom-dfr</str>
* </similarity>
* ...
* <fieldType name="type-using-custom-dfr" class="solr.TextField">
* ...
* <similarity class="solr.DFRSimilarityFactory">
* <str name="basicModel">I(F)</str>
* <str name="afterEffect">B</str>
* <str name="normalization">H3</str>
* <float name="mu">900</float>
* </similarity>
* </fieldType>
* </pre>
* <p>
* In the example above, any fieldtypes that do not define their own <code></similarity/></code>
* will use the <code>Similarity</code> configured for the <code>type-using-custom-dfr</code>.
* </p>
*
* <p>
* <b>NOTE:</b> Users should be aware that even when this factory uses a single default
* <code>Similarity</code> for some or all fields in a Query, the behavior can be inconsistent
* with the behavior of explicitly configuring that same <code>Similarity</code> globally, because
* of differences in how some multi-field / multi-clause behavior is defined in
* <code>PerFieldSimilarityWrapper</code>.
* </p>
*
* @see FieldType#getSimilarity
*/
public class SchemaSimilarityFactory extends SimilarityFactory implements SolrCoreAware {
private static final String INIT_OPT = "defaultSimFromFieldType";
private String defaultSimFromFieldType; // set by init, if null use sensible implicit default
private volatile SolrCore core; // set by inform(SolrCore)
private volatile Similarity similarity; // lazy instantiated
@Override
public void inform(SolrCore core) {
this.core = core;
}
@Override
public void init(SolrParams args) {
defaultSimFromFieldType = args.get(INIT_OPT, null);
super.init(args);
}
@Override
public Similarity getSimilarity() {
if (null == core) {
throw new IllegalStateException("SchemaSimilarityFactory can not be used until SolrCoreAware.inform has been called");
}
if (null == similarity) {
// Need to instantiate lazily, can't do this in inform(SolrCore) because of chicken/egg
// circular initialization hell with core.getLatestSchema() to lookup defaultSimFromFieldType
Similarity defaultSim = null;
if (null == defaultSimFromFieldType) {
// nothing configured, choose a sensible implicit default...
defaultSim = this.core.getSolrConfig().luceneMatchVersion.onOrAfter(Version.LUCENE_6_0_0)
? new BM25Similarity()
: new ClassicSimilarity();
} else {
FieldType defSimFT = core.getLatestSchema().getFieldTypeByName(defaultSimFromFieldType);
if (null == defSimFT) {
throw new SolrException(ErrorCode.SERVER_ERROR,
"SchemaSimilarityFactory configured with " + INIT_OPT + "='" +
defaultSimFromFieldType + "' but that <fieldType> does not exist");
}
defaultSim = defSimFT.getSimilarity();
if (null == defaultSim) {
throw new SolrException(ErrorCode.SERVER_ERROR,
"SchemaSimilarityFactory configured with " + INIT_OPT + "='" +
defaultSimFromFieldType +
"' but that <fieldType> does not define a <similarity>");
}
}
similarity = new SchemaSimilarity(defaultSim);
}
return similarity;
}
private class SchemaSimilarity extends PerFieldSimilarityWrapper {
private Similarity defaultSimilarity;
private HashMap<FieldType,PayloadDecoder> decoders; // cache to avoid scanning token filters repeatedly, unnecessarily
public SchemaSimilarity(Similarity defaultSimilarity) {
this.defaultSimilarity = defaultSimilarity;
}
@Override
public Similarity get(String name) {
FieldType fieldType = core.getLatestSchema().getFieldTypeNoEx(name);
if (fieldType == null) {
return defaultSimilarity;
} else {
Similarity similarity = fieldType.getSimilarity();
similarity = similarity == null ? defaultSimilarity : similarity;
// Payload score handling: if field type has index-time payload encoding, wrap and computePayloadFactor accordingly
if (decoders == null) decoders = new HashMap<>();
PayloadDecoder decoder;
if (!decoders.containsKey(fieldType)) {
decoders.put(fieldType, PayloadUtils.getPayloadDecoder(fieldType));
}
decoder = decoders.get(fieldType);
if (decoder != null) similarity = new PayloadScoringSimilarityWrapper(similarity, decoder);
return similarity;
}
}
@Override
public String toString() {
return "SchemaSimilarity. Default: " + ((get("") == null) ? "null" : get("").toString());
}
}
}