| /* |
| * 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.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.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<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; |
| } |
| 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<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 |
| 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 |
| 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<String> 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) { |
| 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; |
| } |
| } |
| |
| |