/* * 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 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 java.util.concurrent.ExecutionException; import com.google.common.base.Throwables; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import org.apache.lucene.document.Field; import org.apache.lucene.document.StoredField; import org.apache.lucene.index.IndexableField; import org.apache.lucene.queries.function.FunctionQuery; 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.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.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<T extends SpatialStrategy> 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<String, T> fieldStrategyCache = CacheBuilder.newBuilder().build(); protected DistanceUnits distanceUnits; protected final Set<String> supportedScoreModes; protected AbstractSpatialFieldType() { this(Collections.emptySet()); } protected AbstractSpatialFieldType(Set<String> moreScoreModes) { Set<String> 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<String, String> 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<String, String> 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; } // Warn about using old Spatial4j class names if (argEntry.getValue().contains(OLD_SPATIAL4J_PREFIX)) { log.warn("Replace '" + OLD_SPATIAL4J_PREFIX + "' with '" + NEW_SPATIAL4J_PREFIX + "' in your schema."); argEntry.setValue(argEntry.getValue().replace(OLD_SPATIAL4J_PREFIX, NEW_SPATIAL4J_PREFIX)); } } //Solr expects us to remove the parameters we've used. MapListener<String, String> 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<IndexableField> 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<IndexableField> 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 <code>format={WKT|GeoJSON}</code> */ 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 public Query getRangeQuery(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 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. ValueSource valueSource = getValueSourceFromSpatialArgs(parser, field, spatialArgs, scoreParam, strategy); if (valueSource == null) { return strategy.makeQuery(spatialArgs); //assumed constant scoring } FunctionQuery functionQuery = new FunctionQuery(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<String> getSupportedScoreModes() { return supportedScoreModes; } protected ValueSource 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) { try { return fieldStrategyCache.get(fieldName, () -> newSpatialStrategy(fieldName)); } catch (ExecutionException e) { throw Throwables.propagate(e.getCause()); } } /** * @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; } }