org.apache.solr.schema.AbstractSpatialFieldType Maven / Gradle / Ivy
Show all versions of solr-core Show documentation
/*
* 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 com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.Term;
import org.apache.lucene.queries.function.FunctionScoreQuery;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.DoubleValuesSource;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
import org.apache.lucene.spatial.SpatialStrategy;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialArgsParser;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.apache.lucene.spatial.spatial4j.Geo3dSpatialContextFactory;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.response.TextResponseWriter;
import org.apache.solr.search.QParser;
import org.apache.solr.search.SpatialOptions;
import org.apache.solr.uninverting.UninvertingReader.Type;
import org.apache.solr.util.DistanceUnits;
import org.apache.solr.util.MapListener;
import org.apache.solr.util.SpatialUtils;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.context.SpatialContextFactory;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.io.ShapeReader;
import org.locationtech.spatial4j.io.ShapeWriter;
import org.locationtech.spatial4j.io.SupportedFormats;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.Shape;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract base class for Solr FieldTypes based on a Lucene 4 {@link SpatialStrategy}.
*
* @lucene.experimental
*/
public abstract class AbstractSpatialFieldType extends FieldType
implements SpatialQueryable {
/**
* A local-param with one of "none" (default), "distance", "recipDistance" or supported values in
* ({@link DistanceUnits#getSupportedUnits()}.
*/
public static final String SCORE_PARAM = "score";
/**
* A local-param boolean that can be set to false to only return the FunctionQuery (score), and
* thus not do filtering.
*/
public static final String FILTER_PARAM = "filter";
// score param values:
public static final String DISTANCE = "distance";
public static final String RECIP_DISTANCE = "recipDistance";
public static final String NONE = "none";
/** Optional param to pick the string conversion */
public static final String FORMAT = "format";
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
protected SpatialContext ctx;
protected SpatialArgsParser argsParser;
protected ShapeWriter shapeWriter;
protected ShapeReader shapeReader;
private final Cache fieldStrategyCache = Caffeine.newBuilder().build();
protected DistanceUnits distanceUnits;
protected final Set supportedScoreModes;
protected AbstractSpatialFieldType() {
this(Collections.emptySet());
}
protected AbstractSpatialFieldType(Set moreScoreModes) {
Set set = new TreeSet<>(); // sorted for consistent display order
set.add(NONE);
set.add(DISTANCE);
set.add(RECIP_DISTANCE);
set.addAll(DistanceUnits.getSupportedUnits());
set.addAll(moreScoreModes);
supportedScoreModes = Collections.unmodifiableSet(set);
}
@Override
protected void init(IndexSchema schema, Map args) {
super.init(schema, args);
if (ctx == null) { // subclass can set this directly
final String CTX_PARAM = "spatialContextFactory";
final String OLD_SPATIAL4J_PREFIX = "com.spatial4j.core";
final String NEW_SPATIAL4J_PREFIX = "org.locationtech.spatial4j";
for (Map.Entry argEntry : args.entrySet()) {
// "JTS" is a convenience alias
if (argEntry.getKey().equals(CTX_PARAM) && argEntry.getValue().equals("JTS")) {
argEntry.setValue("org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory");
continue;
}
if (argEntry.getKey().equals(CTX_PARAM) && argEntry.getValue().equals("Geo3D")) {
argEntry.setValue(Geo3dSpatialContextFactory.class.getName());
continue;
}
// Warn about using old Spatial4j class names
if (argEntry.getValue().contains(OLD_SPATIAL4J_PREFIX)) {
log.warn(
"Replace '{}' with '{}' in your schema", OLD_SPATIAL4J_PREFIX, NEW_SPATIAL4J_PREFIX);
argEntry.setValue(
argEntry.getValue().replace(OLD_SPATIAL4J_PREFIX, NEW_SPATIAL4J_PREFIX));
}
}
// Solr expects us to remove the parameters we've used.
MapListener argsWrap = new MapListener<>(args);
ctx =
SpatialContextFactory.makeSpatialContext(
argsWrap, schema.getResourceLoader().getClassLoader());
args.keySet().removeAll(argsWrap.getSeenKeys());
}
final String distanceUnitsStr = args.remove("distanceUnits");
if (distanceUnitsStr == null) {
this.distanceUnits = ctx.isGeo() ? DistanceUnits.KILOMETERS : DistanceUnits.DEGREES;
} else {
this.distanceUnits = parseDistanceUnits(distanceUnitsStr);
if (this.distanceUnits == null)
throw new SolrException(
SolrException.ErrorCode.SERVER_ERROR,
"Must specify distanceUnits as one of "
+ DistanceUnits.getSupportedUnits()
+ " on field types with class "
+ getClass().getSimpleName());
}
final SupportedFormats fmts = ctx.getFormats();
String format = args.remove(FORMAT);
if (format == null) {
format = "WKT";
}
shapeWriter = fmts.getWriter(format);
shapeReader = fmts.getReader(format);
if (shapeWriter == null) {
throw new SolrException(
SolrException.ErrorCode.SERVER_ERROR, "Unknown Shape Format: " + format);
}
if (shapeReader == null) {
throw new SolrException(
SolrException.ErrorCode.SERVER_ERROR, "Unknown Shape Format: " + format);
}
argsParser = newSpatialArgsParser();
}
/**
* if {@code str} is non-null, returns {@link org.apache.solr.util.DistanceUnits#valueOf(String)}
* (which will return null if not found), else returns {@link #distanceUnits} (only null before
* initialized in {@code init()}.
*
* @param str maybe null
* @return maybe null
*/
public DistanceUnits parseDistanceUnits(String str) {
if (str == null) {
return this.distanceUnits;
} else {
return DistanceUnits.valueOf(str);
}
}
protected SpatialArgsParser newSpatialArgsParser() {
return new SpatialArgsParser() {
@Override
protected Shape parseShape(String str, SpatialContext ctx) throws ParseException {
return AbstractSpatialFieldType.this.parseShape(str);
}
};
}
// --------------------------------------------------------------
// Indexing
// --------------------------------------------------------------
@Override
public final Field createField(SchemaField field, Object val) {
throw new IllegalStateException("instead call createFields() because isPolyField() is true");
}
@Override
public Type getUninversionType(SchemaField sf) {
return null;
}
@Override
public List createFields(SchemaField field, Object val) {
String shapeStr = null;
Shape shape;
if (val instanceof Shape) {
shape = ((Shape) val);
} else {
shapeStr = val.toString();
shape = parseShape(shapeStr);
}
if (shape == null) {
log.debug("Field {}: null shape for input: {}", field, val);
return Collections.emptyList();
}
List result = new ArrayList<>();
if (field.indexed() || field.hasDocValues()) {
T strategy = getStrategy(field.getName());
result.addAll(Arrays.asList(strategy.createIndexableFields(shape)));
}
if (field.stored()) {
result.add(new StoredField(field.getName(), getStoredValue(shape, shapeStr)));
}
return result;
}
/** Called by {@link #createFields(SchemaField, Object)} to get the stored value. */
protected String getStoredValue(Shape shape, String shapeStr) {
return (shapeStr == null) ? shapeToString(shape) : shapeStr;
}
/** Create a {@link Shape} from the input string */
public Shape parseShape(String str) {
str = str.trim();
if (str.length() == 0)
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "empty string shape");
// If the first char is promising, try to parse with SpatialUtils.parsePoint
char firstChar = str.charAt(0);
if (firstChar == '+' || firstChar == '-' || (firstChar >= '0' && firstChar <= '9')) {
try {
return SpatialUtils.parsePoint(str, ctx);
} catch (Exception e) { // ignore
}
}
try {
return shapeReader.read(str);
} catch (Exception e) {
String msg =
"Unable to parse shape given formats"
+ " \"lat,lon\", \"x y\" or as "
+ shapeReader.getFormatName()
+ " because "
+ e;
if (!msg.contains(str)) {
msg += " input: " + str;
}
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, msg, e);
}
}
/**
* Returns a String version of a shape to be used for the stored value.
*
* The format can be selected using the initParam format={WKT|GeoJSON}
*/
public String shapeToString(Shape shape) {
return shapeWriter.toString(shape);
}
/** Called from {@link #getStrategy(String)} upon first use by fieldName. } */
protected abstract T newSpatialStrategy(String fieldName);
@Override
public final boolean isPolyField() {
return true;
}
// --------------------------------------------------------------
// Query Support
// --------------------------------------------------------------
/**
* Implemented for compatibility with geofilt & bbox query parsers: {@link SpatialQueryable}.
*/
@Override
public Query createSpatialQuery(QParser parser, SpatialOptions options) {
Point pt = SpatialUtils.parsePointSolrException(options.pointStr, ctx);
double distDeg = DistanceUtils.dist2Degrees(options.distance, options.radius);
Shape shape = ctx.makeCircle(pt, distDeg);
if (options.bbox) shape = shape.getBoundingBox();
SpatialArgs spatialArgs = new SpatialArgs(SpatialOperation.Intersects, shape);
return getQueryFromSpatialArgs(parser, options.field, spatialArgs);
}
@Override
protected Query getSpecializedRangeQuery(
QParser parser,
SchemaField field,
String part1,
String part2,
boolean minInclusive,
boolean maxInclusive) {
if (!minInclusive || !maxInclusive)
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST,
"Both sides of spatial range query must be inclusive: " + field.getName());
Point p1 = SpatialUtils.parsePointSolrException(part1, ctx);
Point p2 = SpatialUtils.parsePointSolrException(part2, ctx);
Rectangle bbox = ctx.makeRectangle(p1, p2);
SpatialArgs spatialArgs = new SpatialArgs(SpatialOperation.Intersects, bbox);
return getQueryFromSpatialArgs(parser, field, spatialArgs); // won't score by default
}
@Override
public ValueSource getValueSource(SchemaField field, QParser parser) {
// This is different from Solr 3 LatLonType's approach which uses the MultiValueSource concept
// to directly expose the x & y pair of FieldCache value sources.
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST,
"A ValueSource isn't directly available from this field. Instead try a query using the distance as the score.");
}
@Override
protected Query getSpecializedExistenceQuery(QParser parser, SchemaField field) {
PrefixQuery query = new PrefixQuery(new Term(field.getName(), ""));
query.setRewriteMethod(field.getType().getRewriteMethod(parser, field));
return query;
}
@Override
public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) {
return getQueryFromSpatialArgs(parser, field, parseSpatialArgs(parser, externalVal));
}
protected SpatialArgs parseSpatialArgs(QParser parser, String externalVal) {
try {
SpatialArgs args = argsParser.parse(externalVal, ctx);
// Convert parsed args.distErr to degrees (using distanceUnits)
if (args.getDistErr() != null) {
args.setDistErr(args.getDistErr() * distanceUnits.multiplierFromThisUnitToDegrees());
}
return args;
} catch (SolrException e) {
throw e;
} catch (Exception e) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
}
}
protected Query getQueryFromSpatialArgs(
QParser parser, SchemaField field, SpatialArgs spatialArgs) {
T strategy = getStrategy(field.getName());
SolrParams localParams = parser.getLocalParams();
// See SOLR-2883 needScore
String scoreParam = (localParams == null ? null : localParams.get(SCORE_PARAM));
// We get the valueSource for the score then the filter and combine them.
DoubleValuesSource valueSource =
getValueSourceFromSpatialArgs(parser, field, spatialArgs, scoreParam, strategy);
if (valueSource == null) {
return strategy.makeQuery(spatialArgs); // assumed constant scoring
}
FunctionScoreQuery functionQuery = new FunctionScoreQuery(new MatchAllDocsQuery(), valueSource);
if (localParams != null && !localParams.getBool(FILTER_PARAM, true)) return functionQuery;
Query filterQuery = strategy.makeQuery(spatialArgs);
return new BooleanQuery.Builder()
.add(functionQuery, Occur.MUST) // matches everything and provides score
.add(filterQuery, Occur.FILTER) // filters (score isn't used)
.build();
}
@Override
public double getSphereRadius() {
return distanceUnits.getEarthRadius();
}
/** The set of values supported for the score local-param. Not null. */
public Set getSupportedScoreModes() {
return supportedScoreModes;
}
protected DoubleValuesSource getValueSourceFromSpatialArgs(
QParser parser, SchemaField field, SpatialArgs spatialArgs, String score, T strategy) {
if (score == null) {
return null;
}
final double multiplier; // default multiplier for degrees
switch (score) {
case "":
case NONE:
return null;
case RECIP_DISTANCE:
return strategy.makeRecipDistanceValueSource(spatialArgs.getShape());
case DISTANCE:
multiplier = distanceUnits.multiplierFromDegreesToThisUnit();
break;
default:
DistanceUnits du = parseDistanceUnits(score);
if (du != null) {
multiplier = du.multiplierFromDegreesToThisUnit();
} else {
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST,
"'score' local-param must be one of " + supportedScoreModes + ", it was: " + score);
}
}
return strategy.makeDistanceValueSource(spatialArgs.getShape().getCenter(), multiplier);
}
/**
* Gets the cached strategy for this field, creating it if necessary via {@link
* #newSpatialStrategy(String)}.
*
* @param fieldName Mandatory reference to the field name
* @return Non-null.
*/
public T getStrategy(final String fieldName) {
return fieldStrategyCache.get(fieldName, k -> newSpatialStrategy(fieldName));
}
/**
* @return The Spatial Context for this field type
*/
public SpatialContext getSpatialContext() {
return ctx;
}
@Override
public void write(TextResponseWriter writer, String name, IndexableField f) throws IOException {
writer.writeStr(name, f.stringValue(), true);
}
@Override
public SortField getSortField(SchemaField field, boolean top) {
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST,
"Sorting not supported on SpatialField: "
+ field.getName()
+ ", instead try sorting by query.");
}
public DistanceUnits getDistanceUnits() {
return this.distanceUnits;
}
}